148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
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 { logger } from '@/utils/logger.ts';
|
|
|
|
/**
|
|
* 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) {
|
|
logger.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>
|
|
);
|
|
}
|