import { create } from 'zustand'; import { v4 as uuidv4 } from 'uuid'; import type { Site, SiteFormData } from '@/types/index.ts'; import { db } from '@/db/schema.ts'; import { useCoverageStore } from '@/store/coverage.ts'; const SITE_COLORS = [ '#ef4444', '#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16', ]; interface SitesState { sites: Site[]; selectedSiteId: string | null; editingSiteId: string | null; isPlacingMode: boolean; selectedSiteIds: string[]; loadSites: () => Promise; addSite: (data: SiteFormData) => Promise; updateSite: (id: string, data: Partial) => Promise; deleteSite: (id: string) => Promise; selectSite: (id: string | null) => void; setEditingSite: (id: string | null) => void; togglePlacingMode: () => void; setPlacingMode: (val: boolean) => void; // Multi-sector cloneSiteAsSectors: (siteId: string, sectorCount: 2 | 3) => Promise; cloneSector: (siteId: string) => Promise; // Import/export importSites: (sitesData: SiteFormData[]) => Promise; // Batch operations toggleSiteSelection: (siteId: string) => void; selectAllSites: () => void; clearSelection: () => void; batchUpdateHeight: (adjustment: number) => Promise; batchSetHeight: (height: number) => Promise; } export const useSitesStore = create((set, get) => ({ sites: [], selectedSiteId: null, editingSiteId: null, isPlacingMode: false, selectedSiteIds: [], loadSites: async () => { const dbSites = await db.sites.toArray(); const sites: Site[] = dbSites.map((s) => ({ ...JSON.parse(s.data), createdAt: new Date(s.createdAt), updatedAt: new Date(s.updatedAt), })); set({ sites }); }, addSite: async (data: SiteFormData) => { const id = uuidv4(); const now = new Date(); const colorIndex = get().sites.length % SITE_COLORS.length; const site: Site = { ...data, id, color: data.color || SITE_COLORS[colorIndex], visible: true, createdAt: now, updatedAt: now, }; await db.sites.put({ id, data: JSON.stringify(site), createdAt: now.getTime(), updatedAt: now.getTime(), }); set((state) => ({ sites: [...state.sites, site] })); return site; }, updateSite: async (id: string, data: Partial) => { const sites = get().sites; const existing = sites.find((s) => s.id === id); if (!existing) return; const updated: Site = { ...existing, ...data, updatedAt: new Date(), }; await db.sites.put({ id, data: JSON.stringify(updated), createdAt: existing.createdAt.getTime(), updatedAt: updated.updatedAt.getTime(), }); set((state) => ({ sites: state.sites.map((s) => (s.id === id ? updated : s)), })); }, deleteSite: async (id: string) => { await db.sites.delete(id); set((state) => ({ sites: state.sites.filter((s) => s.id !== id), selectedSiteId: state.selectedSiteId === id ? null : state.selectedSiteId, editingSiteId: state.editingSiteId === id ? null : state.editingSiteId, })); // Clear stale coverage data useCoverageStore.getState().clearCoverage(); }, selectSite: (id: string | null) => set({ selectedSiteId: id }), setEditingSite: (id: string | null) => set({ editingSiteId: id }), togglePlacingMode: () => set((s) => ({ isPlacingMode: !s.isPlacingMode })), setPlacingMode: (val: boolean) => set({ isPlacingMode: val }), // Multi-sector: clone a site into 2 or 3 co-located sector sites cloneSiteAsSectors: async (siteId: string, sectorCount: 2 | 3) => { const source = get().sites.find((s) => s.id === siteId); if (!source) return; const spacing = 360 / sectorCount; const addSite = get().addSite; for (let i = 0; i < sectorCount; i++) { const azimuth = Math.round(i * spacing) % 360; const label = sectorCount === 2 ? ['Alpha', 'Beta'][i] : ['Alpha', 'Beta', 'Gamma'][i]; await addSite({ name: `${source.name}-${label}`, lat: source.lat, lon: source.lon, height: source.height, power: source.power, gain: source.gain >= 15 ? source.gain : 18, // sector gain default frequency: source.frequency, antennaType: 'sector', azimuth, beamwidth: sectorCount === 2 ? 90 : 65, color: '', visible: true, notes: `Sector ${i + 1} (${label}) of ${source.name}`, }); } }, // Clone a single sector: duplicate site at same location with 120° azimuth offset (tri-sector spacing) cloneSector: async (siteId: string) => { const source = get().sites.find((s) => s.id === siteId); if (!source) return; // Count existing sectors at this location to determine naming const colocated = get().sites.filter( (s) => s.lat === source.lat && s.lon === source.lon ); const sectorLabels = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta']; const sectorIndex = colocated.length; // 0-indexed, next sector const label = sectorLabels[sectorIndex] ?? `S${sectorIndex + 1}`; // Base name without any existing sector suffix const baseName = source.name.replace(/-(Alpha|Beta|Gamma|Delta|Epsilon|Zeta|S\d+|clone)$/i, ''); const addSite = get().addSite; const newAzimuth = ((source.azimuth ?? 0) + 120) % 360; // 120° for tri-sector await addSite({ name: `${baseName}-${label}`, lat: source.lat, lon: source.lon, height: source.height, power: source.power, gain: source.gain >= 15 ? source.gain : 18, // sector gain default frequency: source.frequency, antennaType: 'sector', azimuth: newAzimuth, beamwidth: source.beamwidth ?? 65, color: '', visible: true, notes: `Sector ${label} at ${baseName}`, }); // Clear coverage to force recalculation useCoverageStore.getState().clearCoverage(); }, // Import sites from parsed data importSites: async (sitesData: SiteFormData[]) => { const addSite = get().addSite; let count = 0; for (const data of sitesData) { await addSite(data); count++; } return count; }, // Batch operations toggleSiteSelection: (siteId: string) => { set((state) => { const isSelected = state.selectedSiteIds.includes(siteId); return { selectedSiteIds: isSelected ? state.selectedSiteIds.filter((id) => id !== siteId) : [...state.selectedSiteIds, siteId], }; }); }, selectAllSites: () => { set((state) => ({ selectedSiteIds: state.sites.map((s) => s.id), })); }, clearSelection: () => { set({ selectedSiteIds: [] }); }, batchUpdateHeight: async (adjustment: number) => { const { sites, selectedSiteIds } = get(); const selectedSet = new Set(selectedSiteIds); const now = new Date(); const updatedSites = sites.map((site) => { if (!selectedSet.has(site.id)) return site; return { ...site, height: Math.max(1, Math.min(100, site.height + adjustment)), updatedAt: now, }; }); // Persist to IndexedDB const toUpdate = updatedSites.filter((s) => selectedSet.has(s.id)); for (const site of toUpdate) { await db.sites.put({ id: site.id, data: JSON.stringify(site), createdAt: site.createdAt.getTime(), updatedAt: now.getTime(), }); } set({ sites: updatedSites }); }, batchSetHeight: async (height: number) => { const { sites, selectedSiteIds } = get(); const selectedSet = new Set(selectedSiteIds); const clampedHeight = Math.max(1, Math.min(100, height)); const now = new Date(); const updatedSites = sites.map((site) => { if (!selectedSet.has(site.id)) return site; return { ...site, height: clampedHeight, updatedAt: now, }; }); // Persist to IndexedDB const toUpdate = updatedSites.filter((s) => selectedSet.has(s.id)); for (const site of toUpdate) { await db.sites.put({ id: site.id, data: JSON.stringify(site), createdAt: site.createdAt.getTime(), updatedAt: now.getTime(), }); } set({ sites: updatedSites }); }, }));