From b01ea9fc37ff5b49b4d9426fc41512705c3e5760 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 7 Mar 2026 17:32:49 +0200 Subject: [PATCH] fix(parcel-sync): scan uses remote GIS layer instead of empty local DB - scanNoGeometryParcels now fetches TERENURI_ACTIVE features from remote ArcGIS (lightweight, no geometry) to cross-reference with eTerra immovable list - Cross-references by both NATIONAL_CADASTRAL_REFERENCE and IMMOVABLE_ID - Works correctly regardless of whether user has synced to local DB - Renamed totalInDb -> withGeometry in NoGeomScanResult, UI, and API - Extended fetchAllLayer() to forward outFields/returnGeometry options --- src/app/api/eterra/no-geom-scan/route.ts | 6 +- .../components/parcel-sync-module.tsx | 12 ++-- .../parcel-sync/services/eterra-client.ts | 2 + .../parcel-sync/services/no-geom-sync.ts | 57 ++++++++++++------- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/app/api/eterra/no-geom-scan/route.ts b/src/app/api/eterra/no-geom-scan/route.ts index 36b77e3..662edce 100644 --- a/src/app/api/eterra/no-geom-scan/route.ts +++ b/src/app/api/eterra/no-geom-scan/route.ts @@ -2,11 +2,11 @@ * POST /api/eterra/no-geom-scan * * Scans eTerra immovable list for a UAT and counts how many parcels - * exist in the eTerra database but have no geometry in the GIS layer - * (i.e., they are NOT in the local TERENURI_ACTIVE DB). + * exist in the eTerra database but have no geometry in the remote + * ArcGIS GIS layer (TERENURI_ACTIVE). Cross-references remotely. * * Body: { siruta: string } - * Returns: { totalImmovables, totalInDb, noGeomCount, samples } + * Returns: { totalImmovables, withGeometry, noGeomCount, samples } * * Requires active eTerra session. */ diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index d57bcb3..71293da 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -388,7 +388,7 @@ export function ParcelSyncModule() { const [noGeomScanning, setNoGeomScanning] = useState(false); const [noGeomScan, setNoGeomScan] = useState<{ totalImmovables: number; - totalInDb: number; + withGeometry: number; noGeomCount: number; } | null>(null); const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done @@ -697,23 +697,23 @@ export function ParcelSyncModule() { }); const data = (await res.json()) as { totalImmovables?: number; - totalInDb?: number; + withGeometry?: number; noGeomCount?: number; error?: string; }; if (data.error) { console.warn("[no-geom-scan]", data.error); - setNoGeomScan({ totalImmovables: 0, totalInDb: 0, noGeomCount: 0 }); + setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 }); } else { setNoGeomScan({ totalImmovables: data.totalImmovables ?? 0, - totalInDb: data.totalInDb ?? 0, + withGeometry: data.withGeometry ?? 0, noGeomCount: data.noGeomCount ?? 0, }); } } catch { // Show zero result on network error - setNoGeomScan({ totalImmovables: 0, totalInDb: 0, noGeomCount: 0 }); + setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 }); } setNoGeomScanning(false); }, @@ -2407,7 +2407,7 @@ export function ParcelSyncModule() { {" "} imobile în eTerra:{" "} - {noGeomScan.totalInDb.toLocaleString("ro-RO")} + {noGeomScan.withGeometry.toLocaleString("ro-RO")} {" "} cu geometrie,{" "} diff --git a/src/modules/parcel-sync/services/eterra-client.ts b/src/modules/parcel-sync/services/eterra-client.ts index 31d0a2e..1d69b61 100644 --- a/src/modules/parcel-sync/services/eterra-client.ts +++ b/src/modules/parcel-sync/services/eterra-client.ts @@ -309,6 +309,8 @@ export class EterraClient { total?: number; onProgress?: ProgressCallback; delayMs?: number; + outFields?: string; + returnGeometry?: boolean; }, ) { const where = await this.buildWhere(layer, siruta); diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts index d9a4c55..c90aa12 100644 --- a/src/modules/parcel-sync/services/no-geom-sync.ts +++ b/src/modules/parcel-sync/services/no-geom-sync.ts @@ -93,7 +93,8 @@ const normalizeCadRef = (value: unknown) => export type NoGeomScanResult = { totalImmovables: number; - totalInDb: number; + /** Features present in the remote ArcGIS TERENURI_ACTIVE layer (have geometry) */ + withGeometry: number; noGeomCount: number; /** Sample of immovable identifiers without geometry */ samples: Array<{ @@ -116,7 +117,11 @@ export type NoGeomSyncResult = { /** * Scan: count how many eTerra immovables for this UAT have no geometry - * in the local DB. + * in the remote GIS layer (TERENURI_ACTIVE). + * + * Cross-references the eTerra immovable list against the REMOTE ArcGIS + * layer (lightweight fetch, no geometry download). This works correctly + * regardless of whether the user has synced to local DB yet. * * This does NOT write anything — it's a read-only operation. */ @@ -133,14 +138,14 @@ export async function scanNoGeometryParcels( if (!wsPk) { return { totalImmovables: 0, - totalInDb: 0, + withGeometry: 0, noGeomCount: 0, samples: [], error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`, }; } - // 1. Fetch all immovables from eTerra + // 1. Fetch all immovables from eTerra immovable list API const allImmovables = await fetchAllImmovables( client, siruta, @@ -148,20 +153,33 @@ export async function scanNoGeometryParcels( options?.onProgress, ); - // 2. Get all existing cadastralRefs in DB for TERENURI_ACTIVE - const existingFeatures = await prisma.gisFeature.findMany({ - where: { layerId: "TERENURI_ACTIVE", siruta }, - select: { cadastralRef: true, objectId: true }, + // 2. Fetch remote GIS cadastral refs (lightweight — no geometry) + // This is the source of truth for "has geometry" regardless of local DB state. + // ~4 pages for 8k features with outFields only = very fast. + const terenuriLayer = { + id: "TERENURI_ACTIVE", + name: "TERENURI_ACTIVE", + endpoint: "aut" as const, + whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1", + }; + const remoteFeatures = await client.fetchAllLayer(terenuriLayer, siruta, { + returnGeometry: false, + outFields: "OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID", + pageSize: 2000, }); - const existingCadRefs = new Set(); - const existingObjIds = new Set(); - for (const f of existingFeatures) { - if (f.cadastralRef) existingCadRefs.add(normalizeCadRef(f.cadastralRef)); - existingObjIds.add(f.objectId); + const remoteCadRefs = new Set(); + const remoteImmIds = new Set(); + for (const f of remoteFeatures) { + const cadRef = normalizeCadRef( + f.attributes?.NATIONAL_CADASTRAL_REFERENCE ?? "", + ); + if (cadRef) remoteCadRefs.add(cadRef); + const immId = normalizeId(f.attributes?.IMMOVABLE_ID); + if (immId) remoteImmIds.add(immId); } - // 3. Find immovables not in DB + // 3. Cross-reference: immovables NOT in remote GIS = no geometry const noGeomItems: Array<{ immovablePk: number; identifierDetails: string; @@ -172,12 +190,13 @@ export async function scanNoGeometryParcels( for (const item of allImmovables) { const cadRef = normalizeCadRef(item.identifierDetails ?? ""); const immPk = Number(item.immovablePk ?? 0); + const immId = normalizeId(item.immovablePk); - // Already in DB by cadastral ref? - if (cadRef && existingCadRefs.has(cadRef)) continue; + // Present in remote GIS layer by cadastral ref? → has geometry + if (cadRef && remoteCadRefs.has(cadRef)) continue; - // Already in DB by negative objectId? - if (immPk > 0 && existingObjIds.has(-immPk)) continue; + // Present in remote GIS layer by IMMOVABLE_ID? → has geometry + if (immId && remoteImmIds.has(immId)) continue; noGeomItems.push({ immovablePk: immPk, @@ -189,7 +208,7 @@ export async function scanNoGeometryParcels( return { totalImmovables: allImmovables.length, - totalInDb: existingFeatures.length, + withGeometry: remoteFeatures.length, noGeomCount: noGeomItems.length, samples: noGeomItems.slice(0, 20), };