diff --git a/src/app/api/eterra/layers/summary/route.ts b/src/app/api/eterra/layers/summary/route.ts index c2c3f55..3d7acd2 100644 --- a/src/app/api/eterra/layers/summary/route.ts +++ b/src/app/api/eterra/layers/summary/route.ts @@ -5,6 +5,7 @@ import { } from "@/modules/parcel-sync/services/eterra-client"; import { LAYER_CATALOG } from "@/modules/parcel-sync/services/eterra-layers"; import { fetchUatGeometry } from "@/modules/parcel-sync/services/uat-geometry"; +import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -18,19 +19,17 @@ type Body = { /** * POST — Count features per layer on the remote eTerra server. + * Supports session-based auth (falls back to env vars). */ export async function POST(req: Request) { try { const body = (await req.json()) as Body; - const username = ( - body.username ?? - process.env.ETERRA_USERNAME ?? - "" + const session = getSessionCredentials(); + const username = String( + body.username || session?.username || process.env.ETERRA_USERNAME || "", ).trim(); - const password = ( - body.password ?? - process.env.ETERRA_PASSWORD ?? - "" + const password = String( + body.password || session?.password || process.env.ETERRA_PASSWORD || "", ).trim(); const siruta = String(body.siruta ?? "").trim(); diff --git a/src/app/api/eterra/search/route.ts b/src/app/api/eterra/search/route.ts index b012c93..31b7a28 100644 --- a/src/app/api/eterra/search/route.ts +++ b/src/app/api/eterra/search/route.ts @@ -131,7 +131,8 @@ function formatAddress(item?: any) { if (houseNo) parts.push(`Nr. ${houseNo}`); // Building details - if (address.buildingSectionNo) parts.push(`Bl. ${address.buildingSectionNo}`); + if (address.buildingSectionNo) + parts.push(`Bl. ${address.buildingSectionNo}`); if (address.buildingEntryNo) parts.push(`Sc. ${address.buildingEntryNo}`); if (address.buildingFloorNo) parts.push(`Et. ${address.buildingFloorNo}`); if (address.buildingUnitNo) parts.push(`Ap. ${address.buildingUnitNo}`); @@ -166,7 +167,9 @@ function formatAddress(item?: any) { // If we still have nothing, try addressDescription from first entry if (formatted.length === 0) { - const desc = addresses[0]?.address?.addressDescription ?? addresses[0]?.addressDescription; + const desc = + addresses[0]?.address?.addressDescription ?? + addresses[0]?.addressDescription; if (desc) { const s = String(desc).trim(); if (s.length > 2 && !s.includes("[object")) return s; diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 20d7225..9607d5e 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -299,6 +299,14 @@ export function ParcelSyncModule() { Record >({}); const [downloadingLayer, setDownloadingLayer] = useState(null); + const [layerCounts, setLayerCounts] = useState< + Record + >({}); + const [countingLayers, setCountingLayers] = useState(false); + const [layerCountSiruta, setLayerCountSiruta] = useState(""); // siruta for which counts were fetched + const [layerHistory, setLayerHistory] = useState< + { layerId: string; label: string; count: number; time: string; siruta: string }[] + >([]); /* ── Parcel search tab ──────────────────────────────────────── */ const [searchResults, setSearchResults] = useState([]); @@ -554,6 +562,84 @@ export function ParcelSyncModule() { [siruta, exporting, startPolling], ); + /* ════════════════════════════════════════════════════════════ */ + /* Layer feature counts */ + /* ════════════════════════════════════════════════════════════ */ + + // Load history from localStorage on mount + useEffect(() => { + try { + const raw = localStorage.getItem("parcel-sync:layer-history"); + if (raw) { + const parsed = JSON.parse(raw) as typeof layerHistory; + // Only keep today's entries + const today = new Date().toISOString().slice(0, 10); + const todayEntries = parsed.filter( + (e) => e.time.slice(0, 10) === today, + ); + setLayerHistory(todayEntries); + } + } catch { + // ignore + } + }, []); + + const fetchLayerCounts = useCallback(async () => { + if (!siruta || countingLayers) return; + setCountingLayers(true); + try { + const res = await fetch("/api/eterra/layers/summary", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ siruta }), + }); + const data = (await res.json()) as { + counts?: Record; + error?: string; + }; + if (data.counts) { + setLayerCounts(data.counts); + setLayerCountSiruta(siruta); + + // Save non-zero counts to history + const now = new Date().toISOString(); + const today = now.slice(0, 10); + const newEntries: typeof layerHistory = []; + for (const [layerId, info] of Object.entries(data.counts)) { + if (info.count > 0) { + const layer = LAYER_CATALOG.find((l) => l.id === layerId); + newEntries.push({ + layerId, + label: layer?.label ?? layerId, + count: info.count, + time: now, + siruta, + }); + } + } + setLayerHistory((prev) => { + // Keep today's entries only, add new batch + const kept = prev.filter( + (e) => e.time.slice(0, 10) === today && e.siruta !== siruta, + ); + const merged = [...kept, ...newEntries]; + try { + localStorage.setItem( + "parcel-sync:layer-history", + JSON.stringify(merged), + ); + } catch { + // quota + } + return merged; + }); + } + } catch { + // silent + } + setCountingLayers(false); + }, [siruta, countingLayers]); + /* ════════════════════════════════════════════════════════════ */ /* Export individual layer */ /* ════════════════════════════════════════════════════════════ */ @@ -692,7 +778,12 @@ export function ParcelSyncModule() { setSearchList((prev) => prev.filter((p) => p.nrCad !== nrCad)); }, []); - // CSV export + // CSV export — all fields quoted to handle commas in values (e.g. nrTopo) + const csvEscape = useCallback((val: string | number | null | undefined) => { + const s = val != null ? String(val) : ""; + return `"${s.replace(/"/g, '""')}"`; + }, []); + const downloadCSV = useCallback(() => { const items = searchList.length > 0 ? searchList : searchResults; if (items.length === 0) return; @@ -710,17 +801,17 @@ export function ParcelSyncModule() { "SOLICITANT", ]; const rows = items.map((p) => [ - p.nrCad, - p.nrCF, - p.nrCFVechi, - p.nrTopo, - p.suprafata != null ? String(p.suprafata) : "", - p.intravilan, - `"${(p.categorieFolosinta ?? "").replace(/"/g, '""')}"`, - `"${(p.adresa ?? "").replace(/"/g, '""')}"`, - `"${(p.proprietariActuali ?? p.proprietari ?? "").replace(/"/g, '""')}"`, - `"${(p.proprietariVechi ?? "").replace(/"/g, '""')}"`, - `"${(p.solicitant ?? "").replace(/"/g, '""')}"`, + csvEscape(p.nrCad), + csvEscape(p.nrCF), + csvEscape(p.nrCFVechi), + csvEscape(p.nrTopo), + csvEscape(p.suprafata), + csvEscape(p.intravilan), + csvEscape(p.categorieFolosinta), + csvEscape(p.adresa), + csvEscape(p.proprietariActuali ?? p.proprietari), + csvEscape(p.proprietariVechi), + csvEscape(p.solicitant), ]); const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n"); const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" }); @@ -730,7 +821,7 @@ export function ParcelSyncModule() { a.download = `parcele_${siruta}_${Date.now()}.csv`; a.click(); URL.revokeObjectURL(url); - }, [searchList, searchResults, siruta]); + }, [searchList, searchResults, siruta, csvEscape]); /* ════════════════════════════════════════════════════════════ */ /* Derived data */ @@ -1288,12 +1379,43 @@ export function ParcelSyncModule() { ) : (
+ {/* Count all button */} +
+

+ {layerCountSiruta === siruta && Object.keys(layerCounts).length > 0 + ? `Număr features pentru SIRUTA ${siruta}` + : "Apasă pentru a număra features-urile din fiecare layer."} +

+ +
+ {(Object.keys(LAYER_CATEGORY_LABELS) as LayerCategory[]).map( (cat) => { const layers = layersByCategory[cat]; if (!layers?.length) return null; const isExpanded = expandedCategories[cat] ?? false; + // Sum counts for category badge + const catTotal = + layerCountSiruta === siruta + ? layers.reduce( + (sum, l) => sum + (layerCounts[l.id]?.count ?? 0), + 0, + ) + : null; + return (
{isExpanded ? ( @@ -1328,6 +1455,10 @@ export function ParcelSyncModule() { {layers.map((layer) => { const isDownloading = downloadingLayer === layer.id; + const lc = + layerCountSiruta === siruta + ? layerCounts[layer.id] + : undefined; return (
-

- {layer.label} -

+
+

+ {layer.label} +

+ {lc != null && !lc.error && ( + + {lc.count.toLocaleString("ro-RO")} + + )} + {lc?.error && ( + + eroare + + )} +

{layer.id}

@@ -1370,6 +1521,60 @@ export function ParcelSyncModule() { ); }, )} + + {/* Drumul de azi — today's layer count history */} + {layerHistory.length > 0 && ( + +
+
+ + Drumul de azi + + {layerHistory.length} + +
+
+ +
+ {/* Group by siruta */} + {(() => { + const grouped = new Map(); + for (const e of layerHistory) { + if (!grouped.has(e.siruta)) grouped.set(e.siruta, []); + grouped.get(e.siruta)!.push(e); + } + return Array.from(grouped.entries()).map( + ([sir, entries]) => ( +
+

+ SIRUTA {sir}{" "} + + — {new Date(entries[0]!.time).toLocaleTimeString("ro-RO", { hour: "2-digit", minute: "2-digit" })} + +

+
+ {entries + .sort((a, b) => b.count - a.count) + .map((e) => ( +
+ {e.label} + + {e.count.toLocaleString("ro-RO")} + +
+ ))} +
+
+ ), + ); + })()} +
+
+
+ )}
)}