diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts index 0fa9180..f0aea75 100644 --- a/src/app/api/eterra/export-bundle/route.ts +++ b/src/app/api/eterra/export-bundle/route.ts @@ -256,6 +256,8 @@ export async function POST(req: Request) { /* Phase 1b: Import no-geometry parcels (optional) */ /* ══════════════════════════════════════════════════════════ */ let noGeomImported = 0; + let noGeomCleaned = 0; + let noGeomSkipped = 0; if (hasNoGeom && weights.noGeom > 0) { setPhaseState("Import parcele fără geometrie", weights.noGeom, 1); const noGeomClient = await EterraClient.create( @@ -282,6 +284,8 @@ export async function POST(req: Request) { pushProgress(); } else { noGeomImported = noGeomResult.imported; + noGeomCleaned = noGeomResult.cleaned; + noGeomSkipped = noGeomResult.skipped; const cleanedNote = noGeomResult.cleaned > 0 ? `, ${noGeomResult.cleaned} vechi șterse` @@ -626,6 +630,26 @@ export async function POST(req: Request) { siruta: validated.siruta, generatedAt: new Date().toISOString(), source: "local-db (sync-first)", + pipeline: { + syncedGis: { + terenuri: terenuriNeedsSync ? "descărcat" : "din cache", + cladiri: cladiriNeedsSync ? "descărcat" : "din cache", + }, + noGeometry: hasNoGeom + ? { + imported: noGeomImported, + cleaned: noGeomCleaned, + skipped: noGeomSkipped, + } + : "dezactivat", + enriched: validated.mode === "magic" ? "da" : "nu", + finalDb: { + total: dbTerenuri.length, + withGeometry: withGeomRecords.length, + noGeometry: noGeomRecords.length, + cladiri: cladiriGeoFeatures.length, + }, + }, terenuri: { count: terenuriGeoFeatures.length, totalInDb: dbTerenuri.length, @@ -665,7 +689,21 @@ export async function POST(req: Request) { ` Generat: ${new Date().toISOString().replace("T", " ").slice(0, 19)}`, `══════════════════════════════════════════════════════════`, ``, - `REZUMAT GENERAL`, + `PIPELINE — CE S-A ÎNTÂMPLAT`, + `─────────────────────────────────────────────────────────`, + ` 1. Sync GIS terenuri: ${terenuriNeedsSync ? "descărcat din eTerra" : "din cache local (date proaspete)"}`, + ` 2. Sync GIS clădiri: ${cladiriNeedsSync ? "descărcat din eTerra" : "din cache local (date proaspete)"}`, + ...(hasNoGeom + ? [ + ` 3. Import fără geometrie: ${fmt(noGeomImported)} noi importate` + + (noGeomCleaned > 0 ? `, ${fmt(noGeomCleaned)} vechi șterse` : "") + + (noGeomSkipped > 0 ? `, ${fmt(noGeomSkipped)} filtrate/skip` : ""), + ] + : [` 3. Import fără geometrie: dezactivat`]), + ` 4. Îmbogățire (CF, prop.): da`, + ` 5. Generare fișiere: GPKG + CSV + raport`, + ``, + `STARE FINALĂ BAZĂ DE DATE`, `─────────────────────────────────────────────────────────`, ` Total parcele în baza de date: ${fmt(dbTerenuri.length)}`, ` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`, diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 9bf8991..f33827c 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -391,6 +391,8 @@ export function ParcelSyncModule() { withGeometry: number; remoteGisCount: number; noGeomCount: number; + matchedByRef: number; + matchedById: number; qualityBreakdown: { withCadRef: number; withPaperCad: number; @@ -407,6 +409,7 @@ export function ParcelSyncModule() { localDbEnriched: number; localDbEnrichedComplete: number; localSyncFresh: boolean; + scannedAt: string; } | null>(null); const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done @@ -718,6 +721,8 @@ export function ParcelSyncModule() { withGeometry: 0, remoteGisCount: 0, noGeomCount: 0, + matchedByRef: 0, + matchedById: 0, qualityBreakdown: emptyQuality, localDbTotal: 0, localDbWithGeom: 0, @@ -725,6 +730,7 @@ export function ParcelSyncModule() { localDbEnriched: 0, localDbEnrichedComplete: 0, localSyncFresh: false, + scannedAt: "", }; try { const res = await fetch("/api/eterra/no-geom-scan", { @@ -746,6 +752,8 @@ export function ParcelSyncModule() { withGeometry: Number(data.withGeometry ?? 0), remoteGisCount: Number(data.remoteGisCount ?? 0), noGeomCount: Number(data.noGeomCount ?? 0), + matchedByRef: Number(data.matchedByRef ?? 0), + matchedById: Number(data.matchedById ?? 0), qualityBreakdown: { withCadRef: Number(qb.withCadRef ?? 0), withPaperCad: Number(qb.withPaperCad ?? 0), @@ -762,6 +770,7 @@ export function ParcelSyncModule() { localDbEnriched: Number(data.localDbEnriched ?? 0), localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0), localSyncFresh: Boolean(data.localSyncFresh), + scannedAt: String(data.scannedAt ?? new Date().toISOString()), }); } } catch { @@ -2601,7 +2610,7 @@ export function ParcelSyncModule() { {" "} imobile în eTerra:{" "} - {noGeomScan.remoteGisCount.toLocaleString("ro-RO")} + {noGeomScan.withGeometry.toLocaleString("ro-RO")} {" "} cu geometrie,{" "} @@ -2609,15 +2618,25 @@ export function ParcelSyncModule() { {" "} fără geometrie

- {noGeomScan.withGeometry < - noGeomScan.remoteGisCount && ( -

- {noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "} - din{" "} - {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "} - features GIS au corespondent în lista de imobile -

- )} +

+ Layer GIS:{" "} + + {noGeomScan.remoteGisCount.toLocaleString("ro-RO")} + + {" features (se descarcă toate)"} + {noGeomScan.remoteGisCount !== noGeomScan.withGeometry && ( + <> + {" · "} + {noGeomScan.withGeometry.toLocaleString("ro-RO")} potrivite + cu lista de imobile + {noGeomScan.matchedByRef > 0 && noGeomScan.matchedById > 0 && ( + + {" "}({noGeomScan.matchedByRef} cadRef + {noGeomScan.matchedById} ID) + + )} + + )} +

Cele fără geometrie există în baza de date eTerra dar nu au contur desenat în layerul GIS. diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts index 224bc1d..ac85ab7 100644 --- a/src/modules/parcel-sync/services/no-geom-sync.ts +++ b/src/modules/parcel-sync/services/no-geom-sync.ts @@ -118,6 +118,9 @@ export type NoGeomScanResult = { /** Total features in the remote ArcGIS TERENURI_ACTIVE layer */ remoteGisCount: number; noGeomCount: number; + /** Match quality: how many matched by cadastral ref vs immovable ID */ + matchedByRef: number; + matchedById: number; /** Quality breakdown of no-geometry items */ qualityBreakdown: NoGeomQuality; /** Sample of immovable identifiers without geometry */ @@ -142,6 +145,8 @@ export type NoGeomScanResult = { localDbEnrichedComplete: number; /** Whether local sync is fresh (< 7 days) */ localSyncFresh: boolean; + /** Timestamp of the scan (for audit trail) */ + scannedAt: string; /** Error message if workspace couldn't be resolved */ error?: string; }; @@ -181,6 +186,8 @@ export async function scanNoGeometryParcels( withGeometry: 0, remoteGisCount: 0, noGeomCount: 0, + matchedByRef: 0, + matchedById: 0, qualityBreakdown: { withCadRef: 0, withPaperCad: 0, @@ -198,6 +205,7 @@ export async function scanNoGeometryParcels( localDbEnriched: 0, localDbEnrichedComplete: 0, localSyncFresh: false, + scannedAt: new Date().toISOString(), error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`, }; } @@ -248,16 +256,25 @@ export async function scanNoGeometryParcels( legalArea?: number; }> = []; + let matchedByRef = 0; + let matchedById = 0; + for (const item of allImmovables) { const cadRef = normalizeCadRef(item.identifierDetails ?? ""); const immPk = Number(item.immovablePk ?? 0); const immId = normalizeId(item.immovablePk); // Present in remote GIS layer by cadastral ref? → has geometry - if (cadRef && remoteCadRefs.has(cadRef)) continue; + if (cadRef && remoteCadRefs.has(cadRef)) { + matchedByRef++; + continue; + } // Present in remote GIS layer by IMMOVABLE_ID? → has geometry - if (immId && remoteImmIds.has(immId)) continue; + if (immId && remoteImmIds.has(immId)) { + matchedById++; + continue; + } noGeomItems.push({ immovablePk: immPk, @@ -334,6 +351,12 @@ export async function scanNoGeometryParcels( // withGeometry = immovables that MATCHED a GIS feature (always adds up) const matchedCount = allImmovables.length - noGeomItems.length; + console.log( + `[no-geom-scan] Match quality: ${matchedCount} total (${matchedByRef} by cadRef, ${matchedById} by immId)` + + ` | GIS layer: ${remoteFeatures.length} features | Immovables: ${allImmovables.length}` + + ` | Unmatched GIS: ${remoteFeatures.length - matchedCount}`, + ); + // Quality analysis of no-geom items let qWithCadRef = 0; let qWithPaperCad = 0; @@ -370,6 +393,8 @@ export async function scanNoGeometryParcels( withGeometry: matchedCount, remoteGisCount: remoteFeatures.length, noGeomCount: noGeomItems.length, + matchedByRef, + matchedById, qualityBreakdown: { withCadRef: qWithCadRef, withPaperCad: qWithPaperCad, @@ -387,6 +412,7 @@ export async function scanNoGeometryParcels( localDbEnriched: localEnriched, localDbEnrichedComplete: localEnrichedComplete, localSyncFresh: syncFresh, + scannedAt: new Date().toISOString(), }; }