mytec: after methods

This commit is contained in:
2026-02-02 01:55:09 +02:00
parent b5b2fd90d2
commit aa07fb5f02
10 changed files with 495 additions and 101 deletions

View File

@@ -472,7 +472,7 @@ export default function App() {
RF Coverage Planner
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 mr-4">
<ThemeToggle />
{/* Undo / Redo buttons */}
<div className="hidden sm:flex items-center gap-1">

View File

@@ -68,7 +68,7 @@ function FlyToSelected({ site, isSelected }: { site: Site; isSelected: boolean }
export default memo(function SiteMarker({ site, onEdit }: SiteMarkerProps) {
const selectedSiteId = useSitesStore((s) => s.selectedSiteId);
const selectSite = useSitesStore((s) => s.selectSite);
const updateSite = useSitesStore((s) => s.updateSite);
const moveSiteWithColocated = useSitesStore((s) => s.moveSiteWithColocated);
const isSelected = selectedSiteId === site.id;
@@ -84,7 +84,7 @@ export default memo(function SiteMarker({ site, onEdit }: SiteMarkerProps) {
dragend: (e) => {
const marker = e.target as L.Marker;
const pos = marker.getLatLng();
updateSite(site.id, { lat: pos.lat, lon: pos.lng });
moveSiteWithColocated(site.id, pos.lat, pos.lng);
},
}}
>

View File

@@ -89,18 +89,29 @@ class WebSocketService {
switch (msg.type) {
case 'progress':
pending?.onProgress?.({
phase: msg.phase,
progress: msg.progress,
eta_seconds: msg.eta_seconds,
});
if (pending?.onProgress) {
pending.onProgress({
phase: msg.phase,
progress: msg.progress,
eta_seconds: msg.eta_seconds,
});
} else {
console.warn('[WS] progress msg but no pending calc:', calcId, msg.phase, msg.progress);
}
break;
case 'result':
pending?.onResult(msg.data);
if (pending) {
pending.onResult(msg.data);
} else {
console.warn('[WS] result msg but no pending calc:', calcId);
}
if (calcId) this._pendingCalcs.delete(calcId);
break;
case 'error':
pending?.onError(msg.message);
console.error('[WS] error:', msg.message);
if (pending) {
pending.onError(msg.message);
}
if (calcId) this._pendingCalcs.delete(calcId);
break;
}

View File

@@ -24,6 +24,9 @@ const SITE_COLORS = [
'#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16',
];
/** Proximity threshold for co-located sector grouping (~11 meters). */
const COLOCATION_THRESHOLD = 0.0001;
interface SitesState {
sites: Site[];
selectedSiteId: string | null;
@@ -34,6 +37,7 @@ interface SitesState {
loadSites: () => Promise<void>;
addSite: (data: SiteFormData) => Promise<Site>;
updateSite: (id: string, data: Partial<Site>) => Promise<void>;
moveSiteWithColocated: (id: string, newLat: number, newLon: number) => Promise<void>;
deleteSite: (id: string) => Promise<void>;
selectSite: (id: string | null) => void;
setEditingSite: (id: string | null) => void;
@@ -124,13 +128,78 @@ export const useSitesStore = create<SitesState>((set, get) => ({
}));
},
moveSiteWithColocated: async (id: string, newLat: number, newLon: number) => {
const sites = get().sites;
const site = sites.find((s) => s.id === id);
if (!site) return;
const deltaLat = newLat - site.lat;
const deltaLon = newLon - site.lon;
// Find co-located sector siblings
const colocated = sites.filter(
(s) =>
s.id !== id &&
Math.abs(s.lat - site.lat) < COLOCATION_THRESHOLD &&
Math.abs(s.lon - site.lon) < COLOCATION_THRESHOLD,
);
if (colocated.length === 0) {
// No siblings — plain update
get().updateSite(id, { lat: newLat, lon: newLon });
return;
}
pushSnapshot('move site group', sites);
const now = new Date();
const idsToMove = new Set([id, ...colocated.map((s) => s.id)]);
const updatedSites = sites.map((s) => {
if (!idsToMove.has(s.id)) return s;
return {
...s,
lat: s.id === id ? newLat : s.lat + deltaLat,
lon: s.id === id ? newLon : s.lon + deltaLon,
updatedAt: now,
};
});
for (const s of updatedSites) {
if (!idsToMove.has(s.id)) continue;
await db.sites.put({
id: s.id,
data: JSON.stringify(s),
createdAt: s.createdAt.getTime(),
updatedAt: now.getTime(),
});
}
set({ sites: updatedSites });
},
deleteSite: async (id: string) => {
pushSnapshot('delete site', get().sites);
await db.sites.delete(id);
const sites = get().sites;
const site = sites.find((s) => s.id === id);
if (!site) return;
// Find co-located sector siblings to delete together
const colocated = sites.filter(
(s) =>
Math.abs(s.lat - site.lat) < COLOCATION_THRESHOLD &&
Math.abs(s.lon - site.lon) < COLOCATION_THRESHOLD,
);
const idsToDelete = new Set(colocated.map((s) => s.id));
pushSnapshot('delete site', sites);
for (const delId of idsToDelete) {
await db.sites.delete(delId);
}
set((state) => ({
sites: state.sites.filter((s) => s.id !== id),
selectedSiteId: state.selectedSiteId === id ? null : state.selectedSiteId,
editingSiteId: state.editingSiteId === id ? null : state.editingSiteId,
sites: state.sites.filter((s) => !idsToDelete.has(s.id)),
selectedSiteId: idsToDelete.has(state.selectedSiteId ?? '') ? null : state.selectedSiteId,
editingSiteId: idsToDelete.has(state.editingSiteId ?? '') ? null : state.editingSiteId,
}));
// Clear stale coverage data
useCoverageStore.getState().clearCoverage();