From abd00aecfbb18139bc2302731d3fda1aa84df11e Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 7 Mar 2026 12:00:20 +0200 Subject: [PATCH] ParcelSync: DB status card on Export tab + Baza de Date tab with per-category sync --- .../components/parcel-sync-module.tsx | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index c074234..6e2ea14 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -22,6 +22,8 @@ import { RefreshCw, Database, HardDrive, + Clock, + ArrowDownToLine, } from "lucide-react"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; @@ -813,6 +815,59 @@ export function ParcelSyncModule() { [siruta, exportingLocal], ); + // Sync multiple layers sequentially (for "sync all" / "sync category") + const [syncQueue, setSyncQueue] = useState([]); + const syncQueueRef = useRef([]); + + const handleSyncMultiple = useCallback( + async (layerIds: string[]) => { + if (!siruta || syncingLayer || syncQueue.length > 0) return; + syncQueueRef.current = [...layerIds]; + setSyncQueue([...layerIds]); + + for (const layerId of layerIds) { + setSyncingLayer(layerId); + setSyncProgress( + `Sincronizare ${LAYER_CATALOG.find((l) => l.id === layerId)?.label ?? layerId}…`, + ); + try { + const res = await fetch("/api/eterra/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + siruta, + layerId, + jobId: crypto.randomUUID(), + }), + }); + const data = (await res.json()) as { + error?: string; + newFeatures?: number; + removedFeatures?: number; + totalLocal?: number; + }; + if (data.error) { + setSyncProgress(`Eroare: ${data.error}`); + } + } catch { + setSyncProgress("Eroare rețea"); + } + // Remove from queue + syncQueueRef.current = syncQueueRef.current.filter( + (id) => id !== layerId, + ); + setSyncQueue([...syncQueueRef.current]); + } + // Done — refresh status + await fetchSyncStatus(); + setSyncingLayer(null); + setSyncProgress(""); + setSyncQueue([]); + syncQueueRef.current = []; + }, + [siruta, syncingLayer, syncQueue.length, fetchSyncStatus], + ); + /* ════════════════════════════════════════════════════════════ */ /* PostGIS setup (one-time) */ /* ════════════════════════════════════════════════════════════ */ @@ -1052,6 +1107,38 @@ export function ParcelSyncModule() { ? Math.round((exportProgress.downloaded / exportProgress.total) * 100) : 0; + // DB status: which layers have data for the current UAT + const dbLayersSummary = useMemo(() => { + if (!sirutaValid || syncingSiruta !== siruta) return []; + return LAYER_CATALOG.filter((l) => (syncLocalCounts[l.id] ?? 0) > 0).map( + (l) => { + const count = syncLocalCounts[l.id] ?? 0; + const lastRun = syncRuns.find( + (r) => r.layerId === l.id && r.status === "done", + ); + const lastSynced = lastRun?.completedAt + ? new Date(lastRun.completedAt) + : null; + const ageMs = lastSynced ? Date.now() - lastSynced.getTime() : null; + const isFresh = ageMs !== null ? ageMs < 168 * 60 * 60 * 1000 : false; + return { ...l, count, lastSynced, isFresh }; + }, + ); + }, [sirutaValid, syncingSiruta, siruta, syncLocalCounts, syncRuns]); + + const dbTotalFeatures = dbLayersSummary.reduce((sum, l) => sum + l.count, 0); + + const relativeTime = (date: Date | null) => { + if (!date) return "niciodată"; + const mins = Math.floor((Date.now() - date.getTime()) / 60_000); + if (mins < 1) return "acum"; + if (mins < 60) return `acum ${mins} min`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `acum ${hours}h`; + const days = Math.floor(hours / 24); + return `acum ${days}z`; + }; + /* ════════════════════════════════════════════════════════════ */ /* Render */ /* ════════════════════════════════════════════════════════════ */ @@ -1163,6 +1250,10 @@ export function ParcelSyncModule() { Export + + + Baza de Date + @@ -2042,6 +2133,67 @@ export function ParcelSyncModule() { {/* Tab 3: Export */} {/* ═══════════════════════════════════════════════════════ */} + {/* DB freshness status */} + {sirutaValid && dbLayersSummary.length > 0 && ( + + +
+ + + + {dbTotalFeatures.toLocaleString("ro-RO")} + {" "} + entități în DB din{" "} + + {dbLayersSummary.length} + {" "} + layere + + {(() => { + const freshCount = dbLayersSummary.filter( + (l) => l.isFresh, + ).length; + const staleCount = dbLayersSummary.length - freshCount; + const oldestSync = dbLayersSummary.reduce( + (oldest, l) => { + if (!l.lastSynced) return oldest; + if (!oldest || l.lastSynced < oldest) return l.lastSynced; + return oldest; + }, + null as Date | null, + ); + return ( + <> + {staleCount === 0 ? ( + + + Proaspete + + ) : ( + + + {staleCount} vechi + + )} + {oldestSync && ( + + Ultima sincronizare: {relativeTime(oldestSync)} + + )} + + ); + })()} +
+
+
+ )} + {/* Hero buttons */} {sirutaValid && session.connected ? (
@@ -2195,6 +2347,216 @@ export function ParcelSyncModule() { )} + + {/* ═══════════════════════════════════════════════════════ */} + {/* Tab 4: Baza de Date */} + {/* ═══════════════════════════════════════════════════════ */} + + {!sirutaValid ? ( + + + +

Selectează un UAT pentru a vedea datele locale.

+
+
+ ) : dbLayersSummary.length === 0 ? ( + + + +

Nicio dată în baza de date

+

+ Folosește tab-ul Export pentru a sincroniza date din eTerra. +

+
+
+ ) : ( + <> + {/* Summary + Sync All */} +
+
+ +
+

+ {dbTotalFeatures.toLocaleString("ro-RO")} entități din{" "} + {dbLayersSummary.length} layere +

+

+ {dbLayersSummary.filter((l) => l.isFresh).length} proaspete,{" "} + {dbLayersSummary.filter((l) => !l.isFresh).length} vechi +

+
+
+
+ + +
+
+ + {/* Sync progress banner */} + {(syncingLayer || syncQueue.length > 0) && syncProgress && ( + + +
+ + {syncProgress} + {syncQueue.length > 0 && ( + + {syncQueue.length} rămase + + )} +
+
+
+ )} + + {/* Layers grouped by category */} + {( + Object.entries(LAYER_CATEGORY_LABELS) as [LayerCategory, string][] + ).map(([cat, catLabel]) => { + const catLayers = dbLayersSummary.filter( + (l) => l.category === cat, + ); + if (catLayers.length === 0) return null; + + return ( +
+ {/* Category header */} +
+

+ {catLabel} + + ( + {catLayers + .reduce((s, l) => s + l.count, 0) + .toLocaleString("ro-RO")} + ) + +

+ {catLayers.length > 1 && ( + + )} +
+ + {/* Layer rows */} + + + {catLayers.map((layer) => ( +
+ {/* Layer info */} +
+
+ + {layer.label} + + + {layer.count.toLocaleString("ro-RO")} + +
+
+ + {relativeTime(layer.lastSynced)} + + {layer.isFresh ? ( + + ) : ( + + )} +
+
+ + {/* Actions */} +
+ + +
+
+ ))} +
+
+
+ ); + })} + + )} +
); }