diff --git a/src/app/api/eterra/db-summary/route.ts b/src/app/api/eterra/db-summary/route.ts new file mode 100644 index 0000000..fef1d93 --- /dev/null +++ b/src/app/api/eterra/db-summary/route.ts @@ -0,0 +1,145 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/core/storage/prisma"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** + * GET /api/eterra/db-summary + * + * Returns a summary of ALL data in the GIS database, grouped by UAT. + * No siruta required — shows everything across all UATs. + * + * Response shape: + * { + * uats: [{ + * siruta, uatName, + * layers: [{ layerId, count, enrichedCount, lastSynced }], + * totalFeatures, totalEnriched + * }], + * totalFeatures, totalUats + * } + */ +export async function GET() { + try { + // Feature counts per siruta + layerId + const featureCounts = await prisma.gisFeature.groupBy({ + by: ["siruta", "layerId"], + _count: { id: true }, + }); + + // Enriched counts per siruta + layerId + const enrichedCounts = await prisma.gisFeature.groupBy({ + by: ["siruta", "layerId"], + where: { enrichedAt: { not: null } }, + _count: { id: true }, + }); + const enrichedMap = new Map(); + for (const e of enrichedCounts) { + enrichedMap.set(`${e.siruta}:${e.layerId}`, e._count.id); + } + + // Latest sync run per siruta + layerId + const latestRuns = await prisma.gisSyncRun.findMany({ + where: { status: "done" }, + orderBy: { completedAt: "desc" }, + select: { + siruta: true, + uatName: true, + layerId: true, + completedAt: true, + }, + }); + const latestRunMap = new Map< + string, + { completedAt: Date | null; uatName: string | null } + >(); + for (const r of latestRuns) { + const key = `${r.siruta}:${r.layerId}`; + if (!latestRunMap.has(key)) { + latestRunMap.set(key, { + completedAt: r.completedAt, + uatName: r.uatName, + }); + } + } + + // UAT names from GisUat table + const uatNames = await prisma.gisUat.findMany({ + select: { siruta: true, name: true, county: true }, + }); + const uatNameMap = new Map< + string, + { name: string; county: string | null } + >(); + for (const u of uatNames) { + uatNameMap.set(u.siruta, { name: u.name, county: u.county }); + } + + // Group by siruta + const uatMap = new Map< + string, + { + siruta: string; + uatName: string; + county: string | null; + layers: { + layerId: string; + count: number; + enrichedCount: number; + lastSynced: string | null; + }[]; + totalFeatures: number; + totalEnriched: number; + } + >(); + + for (const fc of featureCounts) { + if (!uatMap.has(fc.siruta)) { + const uatInfo = uatNameMap.get(fc.siruta); + const runInfo = latestRunMap.get(`${fc.siruta}:${fc.layerId}`); + uatMap.set(fc.siruta, { + siruta: fc.siruta, + uatName: uatInfo?.name ?? runInfo?.uatName ?? `UAT ${fc.siruta}`, + county: uatInfo?.county ?? null, + layers: [], + totalFeatures: 0, + totalEnriched: 0, + }); + } + + const uat = uatMap.get(fc.siruta)!; + const enriched = enrichedMap.get(`${fc.siruta}:${fc.layerId}`) ?? 0; + const runInfo = latestRunMap.get(`${fc.siruta}:${fc.layerId}`); + + uat.layers.push({ + layerId: fc.layerId, + count: fc._count.id, + enrichedCount: enriched, + lastSynced: runInfo?.completedAt?.toISOString() ?? null, + }); + uat.totalFeatures += fc._count.id; + uat.totalEnriched += enriched; + + // Update UAT name if we got one from sync runs + if (uat.uatName.startsWith("UAT ") && runInfo?.uatName) { + uat.uatName = runInfo.uatName; + } + } + + const uats = Array.from(uatMap.values()).sort( + (a, b) => b.totalFeatures - a.totalFeatures, + ); + + const totalFeatures = uats.reduce((s, u) => s + u.totalFeatures, 0); + + return NextResponse.json({ + uats, + totalFeatures, + totalUats: uats.length, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts index 6a2d235..e0be9af 100644 --- a/src/app/api/eterra/export-bundle/route.ts +++ b/src/app/api/eterra/export-bundle/route.ts @@ -370,6 +370,7 @@ export async function POST(req: Request) { "NR_TOPO", "ADRESA", "PROPRIETARI", + "PROPRIETARI_VECHI", "SUPRAFATA_2D", "SUPRAFATA_R", "SOLICITANT", @@ -390,6 +391,7 @@ export async function POST(req: Request) { "NR_TOPO", "ADRESA", "PROPRIETARI", + "PROPRIETARI_VECHI", "SUPRAFATA_2D", "SUPRAFATA_R", "SOLICITANT", @@ -423,6 +425,7 @@ export async function POST(req: Request) { e.NR_TOPO ?? "", e.ADRESA ?? "", e.PROPRIETARI ?? "", + e.PROPRIETARI_VECHI ?? "", e.SUPRAFATA_2D ?? "", e.SUPRAFATA_R ?? "", e.SOLICITANT ?? "", diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 6e2ea14..ea58283 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -47,6 +47,7 @@ import { cn } from "@/shared/lib/utils"; import { LAYER_CATALOG, LAYER_CATEGORY_LABELS, + findLayerById, type LayerCategory, type LayerCatalogItem, } from "../services/eterra-layers"; @@ -341,6 +342,28 @@ export function ParcelSyncModule() { const [exportingLocal, setExportingLocal] = useState(false); const refreshSyncRef = useRef<(() => void) | null>(null); + /* ── Global DB summary (all UATs) ────────────────────────────── */ + type DbUatSummary = { + siruta: string; + uatName: string; + county: string | null; + layers: { + layerId: string; + count: number; + enrichedCount: number; + lastSynced: string | null; + }[]; + totalFeatures: number; + totalEnriched: number; + }; + type DbSummary = { + uats: DbUatSummary[]; + totalFeatures: number; + totalUats: number; + }; + const [dbSummary, setDbSummary] = useState(null); + const [dbSummaryLoading, setDbSummaryLoading] = useState(false); + /* ── PostGIS setup ───────────────────────────────────────────── */ const [postgisRunning, setPostgisRunning] = useState(false); const [postgisResult, setPostgisResult] = useState<{ @@ -407,6 +430,23 @@ export function ParcelSyncModule() { }; }, [fetchSession]); + /* ── Fetch global DB summary ─────────────────────────────────── */ + const fetchDbSummary = useCallback(async () => { + setDbSummaryLoading(true); + try { + const res = await fetch("/api/eterra/db-summary"); + const data = (await res.json()) as DbSummary; + if (data.uats) setDbSummary(data); + } catch { + // silent + } + setDbSummaryLoading(false); + }, []); + + useEffect(() => { + void fetchDbSummary(); + }, [fetchDbSummary]); + /* ════════════════════════════════════════════════════════════ */ /* (Sync effect removed — POST seeds from uat.json, no */ /* eTerra nomenclature needed. Workspace resolved lazily.) */ @@ -2351,15 +2391,15 @@ export function ParcelSyncModule() { {/* ═══════════════════════════════════════════════════════ */} {/* Tab 4: Baza de Date */} {/* ═══════════════════════════════════════════════════════ */} - - {!sirutaValid ? ( + + {dbSummaryLoading && !dbSummary ? ( - -

Selectează un UAT pentru a vedea datele locale.

+ +

Se încarcă datele din baza de date…

- ) : dbLayersSummary.length === 0 ? ( + ) : !dbSummary || dbSummary.totalFeatures === 0 ? ( @@ -2371,187 +2411,155 @@ export function ParcelSyncModule() { ) : ( <> - {/* 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 -

-
-
-
- - + {/* Header row */} +
+
+ + + {dbSummary.totalFeatures.toLocaleString("ro-RO")} entități + + + din {dbSummary.totalUats} UAT-uri +
+
- {/* 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; + {/* UAT cards */} + {dbSummary.uats.map((uat) => { + const catCounts: Record = {}; + let enrichedTotal = 0; + let oldestSync: Date | null = null; + for (const layer of uat.layers) { + const cat = + findLayerById(layer.layerId)?.category ?? "administrativ"; + catCounts[cat] = (catCounts[cat] ?? 0) + layer.count; + enrichedTotal += layer.enrichedCount; + if (layer.lastSynced) { + const d = new Date(layer.lastSynced); + if (!oldestSync || d < oldestSync) oldestSync = d; + } + } + const isCurrentUat = sirutaValid && uat.siruta === siruta; return ( -
- {/* Category header */} -
-

- {catLabel} - - ( - {catLayers - .reduce((s, l) => s + l.count, 0) - .toLocaleString("ro-RO")} - ) + + + {/* UAT header row */} +
+ + {uat.uatName} -

- {catLayers.length > 1 && ( - - )} -
- - {/* Layer rows */} - - - {catLayers.map((layer) => ( -
+ ({uat.county}) + + )} + + #{uat.siruta} + + {isCurrentUat && ( + - {/* Layer info */} -
-
- - {layer.label} - - - {layer.count.toLocaleString("ro-RO")} - -
-
- - {relativeTime(layer.lastSynced)} - - {layer.isFresh ? ( - - ) : ( - - )} -
-
+ selectat +
+ )} + + {oldestSync ? relativeTime(oldestSync) : "—"} + +
- {/* Actions */} -
- -
- ))} -
-
-
+ title={`${label}: ${layer.count} entități${isEnriched ? `, ${layer.enrichedCount} îmbogățite` : ""}${layer.lastSynced ? `, sync: ${new Date(layer.lastSynced).toLocaleDateString("ro-RO")}` : ""}`} + > + + {label} + + + {layer.count.toLocaleString("ro-RO")} + + {isEnriched && ( + + )} + + ); + })} +
+ + ); })} diff --git a/src/modules/parcel-sync/services/enrich-service.ts b/src/modules/parcel-sync/services/enrich-service.ts index f06ae52..35a0bdb 100644 --- a/src/modules/parcel-sync/services/enrich-service.ts +++ b/src/modules/parcel-sync/services/enrich-service.ts @@ -125,6 +125,7 @@ export type FeatureEnrichment = { NR_TOPO: string; ADRESA: string; PROPRIETARI: string; + PROPRIETARI_VECHI: string; SUPRAFATA_2D: number | string; SUPRAFATA_R: number | string; SOLICITANT: string; @@ -244,11 +245,14 @@ export async function enrichFeatures( const immovableListByCad = new Map(); const ownersByLandbook = new Map>(); - const addOwner = (landbook: string, name: string) => { + const cancelledOwnersByLandbook = new Map>(); + + const addOwner = (landbook: string, name: string, radiated = false) => { if (!landbook || !name) return; - const existing = ownersByLandbook.get(landbook) ?? new Set(); + const targetMap = radiated ? cancelledOwnersByLandbook : ownersByLandbook; + const existing = targetMap.get(landbook) ?? new Set(); existing.add(name); - ownersByLandbook.set(landbook, existing); + targetMap.set(landbook, existing); }; let listPage = 0; @@ -301,15 +305,32 @@ export async function enrichFeatures( const idKey = normalizeId(item?.immovablePk); if (idKey) docByImmovable.set(idKey, item); }); - (docResponse?.partTwoRegs ?? []).forEach((item: any) => { - if ( - String(item?.nodeType ?? "").toUpperCase() === "P" && - item?.landbookIE - ) { - const name = String(item?.nodeName ?? "").trim(); - if (name) addOwner(String(item.landbookIE), name); + // Build nodeId → entry map for radiated detection + const regs: any[] = docResponse?.partTwoRegs ?? []; + const nodeMap = new Map(); + for (const reg of regs) { + if (reg?.nodeId != null) nodeMap.set(Number(reg.nodeId), reg); + } + // Check if an entry or any ancestor "I" inscription is radiated + const isRadiated = (entry: any, depth = 0): boolean => { + if (!entry || depth > 10) return false; + if (entry?.nodeStatus === -1) return true; + const pid = entry?.parentId; + if (pid != null) { + const parent = nodeMap.get(Number(pid)); + if (parent) return isRadiated(parent, depth + 1); } - }); + return false; + }; + for (const reg of regs) { + if ( + String(reg?.nodeType ?? "").toUpperCase() !== "P" || + !reg?.landbookIE + ) + continue; + const name = String(reg?.nodeName ?? "").trim(); + if (name) addOwner(String(reg.landbookIE), name, isRadiated(reg)); + } } // ── Enrich each teren feature ── @@ -401,6 +422,22 @@ export async function enrichFeatures( Array.from(new Set([...owners, ...ownersByCad])).join("; ") || proprietari; + // Cancelled/old owners + const cancelledOwners = + landbookIE && cancelledOwnersByLandbook.get(String(landbookIE)) + ? Array.from(cancelledOwnersByLandbook.get(String(landbookIE)) ?? []) + : []; + const cancelledByCad = + cadRefRaw && cancelledOwnersByLandbook.get(String(cadRefRaw)) + ? Array.from(cancelledOwnersByLandbook.get(String(cadRefRaw)) ?? []) + : []; + const activeSet = new Set([...owners, ...ownersByCad]); + const proprietariVechi = Array.from( + new Set([...cancelledOwners, ...cancelledByCad]), + ) + .filter((n) => !activeSet.has(n)) + .join("; "); + nrCF = docItem?.landbookIE || listItem?.paperLbNo || @@ -435,6 +472,7 @@ export async function enrichFeatures( NR_TOPO: nrTopo, ADRESA: addressText, PROPRIETARI: proprietari, + PROPRIETARI_VECHI: proprietariVechi, SUPRAFATA_2D: areaValue !== null ? Number(areaValue.toFixed(2)) : "", SUPRAFATA_R: areaValue !== null ? Math.round(areaValue) : "", SOLICITANT: solicitant,