From aee28b676816527f422862dc352aac4b507ff8fa Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sun, 8 Mar 2026 00:57:16 +0200 Subject: [PATCH] feat: filter no-geom by IE status (hasLandbook), add checkIfIsIE + CF PDF APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QUALITY GATE TIGHTENED: No-geometry import now requires hasLandbook=1 (imobil electronic). This filters out immovables without carte funciara — they have no CF data, no owners, no parcel details to extract. For Cosbuc this reduces useful no-geom from ~1916 to ~468 (only IEs with real data). Three-tier quality gate: 1. Active (status=1) 2. Has landbook (hasLandbook=1) — is electronic immovable [NEW] 3. Has identification (cadRef/paperLbNo/paperCadNo) OR area CLEANUP also updated: DB cleanup now removes stale no-geom records that don't pass the tightened gate (existing non-IE records will be cleaned on next import run). NEW API METHODS (eterra-client): - checkIfIsIE(adminUnitId, paperCadNo, topNo, paperCfNo) → boolean Calls /api/immovable/checkIfIsIE — verifies IE status per-parcel Available for future per-item verification if needed - getCfExtractUrl(immovablePk, workspaceId) → string Returns URL for /api/cf/landbook/copycf/get/{pk}/{ws}/0/true Downloads the CF extract as PDF blob (future enrichment) UI updated: 'Filtrate' label now says 'fara CF/inactive/fara date' to reflect the new hasLandbook filter. --- .../components/parcel-sync-module.tsx | 4 +- .../parcel-sync/services/eterra-client.ts | 47 +++++++++++++++++++ .../parcel-sync/services/no-geom-sync.ts | 24 ++++++---- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 5a49848..e069780 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -2746,7 +2746,7 @@ export function ParcelSyncModule() { {noGeomScan.qualityBreakdown.empty > 0 && ( - Filtrate (inactive/fără date):{" "} + Filtrate (fără CF/inactive/fără date):{" "} {noGeomScan.qualityBreakdown.empty.toLocaleString( "ro-RO", @@ -2760,7 +2760,7 @@ export function ParcelSyncModule() { {includeNoGeom && (

{noGeomScan.qualityBreakdown.empty > 0 - ? `Din ${noGeomScan.noGeomCount.toLocaleString("ro-RO")} fără geometrie, ~${noGeomScan.qualityBreakdown.useful.toLocaleString("ro-RO")} vor fi importate (active, cu identificare sau suprafață). ${noGeomScan.qualityBreakdown.empty.toLocaleString("ro-RO")} vor fi filtrate (inactive sau fără date).` + ? `Din ${noGeomScan.noGeomCount.toLocaleString("ro-RO")} fără geometrie, ~${noGeomScan.qualityBreakdown.useful.toLocaleString("ro-RO")} vor fi importate (imobile electronice cu CF). ${noGeomScan.qualityBreakdown.empty.toLocaleString("ro-RO")} vor fi filtrate (fără carte funciară, inactive sau fără date).` : "Vor fi importate în DB și incluse în CSV + Magic GPKG (coloana HAS_GEOMETRY=0/1)."}{" "} În GPKG de bază apar doar cele cu geometrie.

diff --git a/src/modules/parcel-sync/services/eterra-client.ts b/src/modules/parcel-sync/services/eterra-client.ts index abd4076..a1e288a 100644 --- a/src/modules/parcel-sync/services/eterra-client.ts +++ b/src/modules/parcel-sync/services/eterra-client.ts @@ -508,6 +508,53 @@ export class EterraClient { return this.getRawJson(url); } + /** + * Check if an immovable is an "Imobil Electronic" (IE) in eTerra. + * Uses the same endpoint the eTerra UI calls when searching by + * topNo + paperCfNo. + * + * @returns true if the immovable is registered as IE, false otherwise + */ + async checkIfIsIE( + adminUnitId: string | number, + paperCadNo: number | null, + topNo: string | number, + paperCfNo: string | number, + ): Promise { + const url = `${BASE_URL}/api/immovable/checkIfIsIE`; + const payload = { + adminUnitId: Number(adminUnitId), + paperCadNo: Number(paperCadNo ?? 0), + topNo: typeof topNo === "string" ? Number(topNo.split(",")[0]) || 0 : Number(topNo), + paperCfNo: Number(paperCfNo), + }; + try { + const result = await this.requestRaw(() => + this.client.post(url, payload, { + headers: { "Content-Type": "application/json;charset=UTF-8" }, + timeout: this.timeoutMs, + }), + ); + // API may return boolean, number (1/0), or string + return result === true || result === 1 || String(result) === "true"; + } catch { + return false; + } + } + + /** + * Build the URL for downloading a CF (carte funciară) extract PDF. + * The eTerra UI calls this to get the landbook/CF PDF blob. + * + * @returns URL string (caller needs an authenticated session to fetch) + */ + getCfExtractUrl( + immovablePk: string | number, + workspaceId: string | number, + ): string { + return `${BASE_URL}/api/cf/landbook/copycf/get/${immovablePk}/${workspaceId}/0/true`; + } + /** * Search immovable list by exact cadastral number (identifierDetails). * This is the eTerra application API that the web UI uses when you type diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts index 1a9a73d..8e4d853 100644 --- a/src/modules/parcel-sync/services/no-geom-sync.ts +++ b/src/modules/parcel-sync/services/no-geom-sync.ts @@ -403,10 +403,10 @@ export async function scanNoGeometryParcels( if (hasLb) qWithLandbook++; if (hasArea) qWithArea++; if (isActive) qWithActiveStatus++; - // "Useful" = ACTIVE + has any form of identification OR area - // Matches the import quality gate + // "Useful" = ACTIVE + HAS_LANDBOOK (imobil electronic) + has identification OR area + // Matches the import quality gate — only IE items are worth importing const hasIdentification = hasCad || hasPaperLb || hasPaperCad; - if (isActive && (hasIdentification || hasArea)) qUseful++; + if (isActive && hasLb && (hasIdentification || hasArea)) qUseful++; else qEmpty++; } @@ -483,13 +483,14 @@ export async function syncNoGeometryParcels( const cadRef = (item.identifierDetails ?? "").toString().trim(); const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim(); const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim(); + const hasLandbook = typeof item.hasLandbook === "number" ? item.hasLandbook : 0; const hasArea = (typeof item.measuredArea === "number" && item.measuredArea > 0) || (typeof item.legalArea === "number" && item.legalArea > 0); const hasIdentification = !!cadRef || hasPaperLb || hasPaperCad; - // Only keep items that pass the quality gate (active + identification/area) - if (status === 1 && (hasIdentification || hasArea)) { + // Only keep items that pass the quality gate (active + hasLandbook + identification/area) + if (status === 1 && hasLandbook === 1 && (hasIdentification || hasArea)) { validImmPks.add(pk); } } @@ -536,8 +537,8 @@ export async function syncNoGeometryParcels( } // 4. Filter: not yet in DB + quality gate - // Quality: must be ACTIVE (status=1) AND have identification OR area. - // Items that are inactive, or have no identification AND no area = noise. + // Quality: must be ACTIVE (status=1) AND hasLandbook=1 (IE) AND have identification OR area. + // Items without landbook are not electronic immovables — no CF data to extract. let filteredOut = 0; const candidates = allImmovables.filter((item) => { const cadRef = normalizeCadRef(item.identifierDetails ?? ""); @@ -554,7 +555,14 @@ export async function syncNoGeometryParcels( return false; } - // Quality gate 2: must have identification OR area + // Quality gate 2: must be an electronic immovable (hasLandbook=1) + const hasLandbook = typeof item.hasLandbook === "number" ? item.hasLandbook : 0; + if (hasLandbook !== 1) { + filteredOut++; + return false; + } + + // Quality gate 3: must have identification OR area const hasCadRef = !!cadRef; const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim(); const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim();