@mytec: iter7 ready for test

This commit is contained in:
2026-01-30 13:06:31 +02:00
parent baebd29e1a
commit 3e1061e369
12 changed files with 592 additions and 39 deletions

View File

@@ -0,0 +1,146 @@
import { useRef } from 'react';
import { useSitesStore } from '@/store/sites.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import Button from '@/components/ui/Button.tsx';
/**
* Import/Export site configurations as JSON.
* Export downloads the current site list; Import merges (appends) sites.
*/
export default function SiteImportExport() {
const sites = useSitesStore((s) => s.sites);
const importSites = useSitesStore((s) => s.importSites);
const addToast = useToastStore((s) => s.addToast);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleExport = () => {
if (sites.length === 0) {
addToast('No sites to export', 'warning');
return;
}
// Strip internal fields (id, createdAt, updatedAt) so import can reassign them
const exportData = sites.map((s) => ({
name: s.name,
lat: s.lat,
lon: s.lon,
height: s.height,
power: s.power,
gain: s.gain,
frequency: s.frequency,
antennaType: s.antennaType,
azimuth: s.azimuth,
beamwidth: s.beamwidth,
color: s.color,
visible: s.visible,
notes: s.notes,
equipment: s.equipment,
}));
const json = JSON.stringify(exportData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `rfcp-sites-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
addToast(`Exported ${sites.length} site(s) as JSON`, 'success');
};
const handleImport = async (file: File) => {
try {
const text = await file.text();
const parsed = JSON.parse(text);
if (!Array.isArray(parsed)) {
addToast('Invalid file format: expected an array of sites', 'error');
return;
}
// Basic validation
const valid = parsed.filter(
(s: Record<string, unknown>) =>
typeof s.name === 'string' &&
typeof s.lat === 'number' &&
typeof s.lon === 'number'
);
if (valid.length === 0) {
addToast('No valid sites found in file', 'error');
return;
}
// Map to SiteFormData shape
const sitesData = valid.map((s: Record<string, unknown>) => ({
name: s.name as string,
lat: s.lat as number,
lon: s.lon as number,
height: (s.height as number) ?? 30,
power: (s.power as number) ?? 43,
gain: (s.gain as number) ?? 8,
frequency: (s.frequency as number) ?? 1800,
antennaType: ((s.antennaType as string) === 'sector' ? 'sector' : 'omni') as 'omni' | 'sector',
azimuth: s.azimuth as number | undefined,
beamwidth: s.beamwidth as number | undefined,
color: (s.color as string) ?? '',
visible: (s.visible as boolean) ?? true,
notes: s.notes as string | undefined,
equipment: s.equipment as string | undefined,
}));
const count = await importSites(sitesData);
addToast(`Imported ${count} site(s)`, 'success');
} catch (error) {
console.error('Import failed:', error);
addToast('Invalid JSON file', 'error');
}
// Reset file input so same file can be re-imported
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm p-4 space-y-3">
<h3 className="text-sm font-semibold text-gray-800 dark:text-dark-text">
Site Import / Export
</h3>
<div className="flex gap-2">
<Button onClick={handleExport} size="sm" variant="secondary">
Export JSON
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => fileInputRef.current?.click()}
>
Import JSON
</Button>
</div>
{sites.length > 0 && (
<p className="text-xs text-gray-500 dark:text-dark-muted">
{sites.length} site(s) configured
</p>
)}
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
accept=".json"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleImport(file);
}}
/>
</div>
);
}