From f9594fff7165ed51fa25c67de524c14fb5f45445 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 7 Mar 2026 20:25:05 +0200 Subject: [PATCH] fix(parcel-sync): paperCfNo bug, status filter, enrichment robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUGS FIXED: - paperCfNo does NOT exist in eTerra API — field is paperLbNo Renamed withPaperCf → withPaperLb everywhere (type, scan, UI) - Area fields: only measuredArea and legalArea exist on immovable/list Removed phantom area/areaValue/suprafata checks from import filter FILTERING TIGHTENED: - Quality gate now requires status=1 (active) in eTerra - Items with status≠1 are filtered out before import - Quality breakdown adds: withActiveStatus, withLandbook counters - Import attributes now store MEASURED_AREA, LEGAL_AREA, HAS_LANDBOOK - workspace.nomenPk used instead of workspacePk for accuracy ENRICHMENT ROBUSTNESS: - Area fallback: when AREA_VALUE is missing (no-geom), enrichment now falls back to listItem.measuredArea/legalArea from immovable list - Post-enrichment verification: logs 100% coverage or warns about gaps - EnrichResult type extended with totalFeatures + unenrichedCount UI UPDATES: - Quality grid shows 6 stats: cadRef, CF/LB, paperCad, area, active, landbook - Filter explanation updated: 'inactive sau fără date' instead of old text --- .../components/parcel-sync-module.tsx | 36 ++++- .../parcel-sync/services/enrich-service.ts | 32 ++++- .../parcel-sync/services/no-geom-sync.ts | 129 ++++++++++-------- 3 files changed, 132 insertions(+), 65 deletions(-) diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 309d402..f370060 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -394,8 +394,10 @@ export function ParcelSyncModule() { qualityBreakdown: { withCadRef: number; withPaperCad: number; - withPaperCf: number; + withPaperLb: number; + withLandbook: number; withArea: number; + withActiveStatus: number; useful: number; empty: number; }; @@ -704,8 +706,10 @@ export function ParcelSyncModule() { const emptyQuality = { withCadRef: 0, withPaperCad: 0, - withPaperCf: 0, + withPaperLb: 0, + withLandbook: 0, withArea: 0, + withActiveStatus: 0, useful: 0, empty: 0, }; @@ -745,8 +749,10 @@ export function ParcelSyncModule() { qualityBreakdown: { withCadRef: Number(qb.withCadRef ?? 0), withPaperCad: Number(qb.withPaperCad ?? 0), - withPaperCf: Number(qb.withPaperCf ?? 0), + withPaperLb: Number(qb.withPaperLb ?? 0), + withLandbook: Number(qb.withLandbook ?? 0), withArea: Number(qb.withArea ?? 0), + withActiveStatus: Number(qb.withActiveStatus ?? 0), useful: Number(qb.useful ?? 0), empty: Number(qb.empty ?? 0), }, @@ -2663,9 +2669,9 @@ export function ParcelSyncModule() { - Cu nr. CF pe hârtie:{" "} + Cu nr. CF/LB:{" "} - {noGeomScan.qualityBreakdown.withPaperCf.toLocaleString( + {noGeomScan.qualityBreakdown.withPaperLb.toLocaleString( "ro-RO", )} @@ -2686,6 +2692,22 @@ export function ParcelSyncModule() { )} + + Active (status=1):{" "} + + {noGeomScan.qualityBreakdown.withActiveStatus.toLocaleString( + "ro-RO", + )} + + + + Cu carte funciară:{" "} + + {noGeomScan.qualityBreakdown.withLandbook.toLocaleString( + "ro-RO", + )} + +
@@ -2698,7 +2720,7 @@ export function ParcelSyncModule() { {noGeomScan.qualityBreakdown.empty > 0 && ( - Fără date identificare:{" "} + Filtrate (inactive/fără date):{" "} {noGeomScan.qualityBreakdown.empty.toLocaleString( "ro-RO", @@ -2712,7 +2734,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 (cele cu identificare sau suprafață). ${noGeomScan.qualityBreakdown.empty.toLocaleString("ro-RO")} fără nicio dată de identificare vor fi filtrate.` + ? `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).` : "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/enrich-service.ts b/src/modules/parcel-sync/services/enrich-service.ts index 9bd918d..5601811 100644 --- a/src/modules/parcel-sync/services/enrich-service.ts +++ b/src/modules/parcel-sync/services/enrich-service.ts @@ -20,6 +20,8 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export type EnrichResult = { siruta: string; enrichedCount: number; + totalFeatures?: number; + unenrichedCount?: number; buildingCrossRefs: number; status: "done" | "error"; error?: string; @@ -545,8 +547,21 @@ export async function enrichFeatures( const buildLegal = build.has ? (build.legal ? 1 : 0) : 0; if (hasBuilding) buildingCrossRefs += 1; - const areaValue = + // Area: prefer GIS AREA_VALUE, fall back to measuredArea/legalArea from + // immovable list (important for no-geometry features where AREA_VALUE + // may have been stored from measuredArea at import time, or may be null). + let areaValue = typeof attrs.AREA_VALUE === "number" ? attrs.AREA_VALUE : null; + if (areaValue == null && listItem) { + areaValue = + (typeof listItem.measuredArea === "number" && + listItem.measuredArea > 0 + ? listItem.measuredArea + : null) ?? + (typeof listItem.legalArea === "number" && listItem.legalArea > 0 + ? listItem.legalArea + : null); + } const enrichment: FeatureEnrichment = { NR_CAD: cadRefRaw, @@ -585,6 +600,19 @@ export async function enrichFeatures( } } + // ── Post-enrichment verification ── + // Check that ALL features now have enrichment (no gaps) + const unenriched = terenuri.length - enrichedCount; + if (unenriched > 0) { + console.warn( + `[enrich] ${unenriched}/${terenuri.length} features remain unenriched for siruta=${siruta}`, + ); + } else { + console.log( + `[enrich] ✓ 100% enrichment: ${enrichedCount}/${terenuri.length} features for siruta=${siruta}`, + ); + } + push({ phase: "Îmbogățire completă", status: "done", @@ -596,6 +624,8 @@ export async function enrichFeatures( return { siruta, enrichedCount, + totalFeatures: terenuri.length, + unenrichedCount: unenriched, buildingCrossRefs, status: "done", }; diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts index 462f136..74bef8b 100644 --- a/src/modules/parcel-sync/services/no-geom-sync.ts +++ b/src/modules/parcel-sync/services/no-geom-sync.ts @@ -97,13 +97,17 @@ export type NoGeomQuality = { withCadRef: number; /** Have paper cadastral number */ withPaperCad: number; - /** Have paper CF (carte funciară) number */ - withPaperCf: number; - /** Have area > 0 */ + /** Have paper LB / CF (carte funciară) number — field is paperLbNo in API */ + withPaperLb: number; + /** Have hasLandbook=1 flag from eTerra */ + withLandbook: number; + /** Have area > 0 (measuredArea or legalArea) */ withArea: number; - /** "Useful" = have cadRef OR (paperCad AND paperCf) */ + /** status=1 (active) in eTerra */ + withActiveStatus: number; + /** "Useful" = active AND has identification or area */ useful: number; - /** No cadRef, no paperCad, no paperCf — likely unusable */ + /** Filtered out: inactive, or no identification AND no area */ empty: number; }; @@ -121,8 +125,10 @@ export type NoGeomScanResult = { immovablePk: number; identifierDetails: string; paperCadNo?: string; - paperCfNo?: string; paperLbNo?: string; + status?: number; + hasLandbook?: number; + measuredArea?: number; }>; /** Total features already in local DB (geometry + no-geom) */ localDbTotal: number; @@ -177,8 +183,10 @@ export async function scanNoGeometryParcels( qualityBreakdown: { withCadRef: 0, withPaperCad: 0, - withPaperCf: 0, + withPaperLb: 0, + withLandbook: 0, withArea: 0, + withActiveStatus: 0, useful: 0, empty: 0, }, @@ -232,8 +240,11 @@ export async function scanNoGeometryParcels( immovablePk: number; identifierDetails: string; paperCadNo?: string; - paperCfNo?: string; paperLbNo?: string; + status?: number; + hasLandbook?: number; + measuredArea?: number; + legalArea?: number; }> = []; for (const item of allImmovables) { @@ -251,8 +262,14 @@ export async function scanNoGeometryParcels( immovablePk: immPk, identifierDetails: String(item.identifierDetails ?? ""), paperCadNo: item.paperCadNo ?? undefined, - paperCfNo: item.paperCfNo ?? undefined, paperLbNo: item.paperLbNo ?? undefined, + status: typeof item.status === "number" ? item.status : undefined, + hasLandbook: + typeof item.hasLandbook === "number" ? item.hasLandbook : undefined, + measuredArea: + typeof item.measuredArea === "number" ? item.measuredArea : undefined, + legalArea: + typeof item.legalArea === "number" ? item.legalArea : undefined, }); } @@ -317,43 +334,33 @@ export async function scanNoGeometryParcels( const matchedCount = allImmovables.length - noGeomItems.length; // Quality analysis of no-geom items - // Build a quick lookup for area data from the immovable list (any area field) - const areaByPk = new Map(); - for (const item of allImmovables) { - const pk = Number(item.immovablePk ?? 0); - const areaVal = [ - item.area, - item.measuredArea, - item.areaValue, - item.suprafata, - ] - .map((v) => (typeof v === "number" && v > 0 ? v : null)) - .find((v) => v != null); - if (pk > 0 && areaVal != null) { - areaByPk.set(pk, areaVal); - } - } - let qWithCadRef = 0; let qWithPaperCad = 0; - let qWithPaperCf = 0; + let qWithPaperLb = 0; + let qWithLandbook = 0; let qWithArea = 0; + let qWithActiveStatus = 0; let qUseful = 0; let qEmpty = 0; for (const item of noGeomItems) { const hasCad = !!item.identifierDetails?.trim(); const hasPaperCad = !!item.paperCadNo?.trim(); - const hasPaperCf = !!item.paperCfNo?.trim(); const hasPaperLb = !!item.paperLbNo?.trim(); - const hasArea = areaByPk.has(item.immovablePk); + const hasArea = + (item.measuredArea != null && item.measuredArea > 0) || + (item.legalArea != null && item.legalArea > 0); + const isActive = item.status === 1; + const hasLb = item.hasLandbook === 1; if (hasCad) qWithCadRef++; if (hasPaperCad) qWithPaperCad++; - if (hasPaperCf) qWithPaperCf++; + if (hasPaperLb) qWithPaperLb++; + if (hasLb) qWithLandbook++; if (hasArea) qWithArea++; - // "Useful" = has any form of identification OR area - // Matches the import quality gate: !hasIdentification && !hasArea → filter out - const hasIdentification = hasCad || hasPaperCf || hasPaperLb || hasPaperCad; - if (hasIdentification || hasArea) qUseful++; + if (isActive) qWithActiveStatus++; + // "Useful" = ACTIVE + has any form of identification OR area + // Matches the import quality gate + const hasIdentification = hasCad || hasPaperLb || hasPaperCad; + if (isActive && (hasIdentification || hasArea)) qUseful++; else qEmpty++; } @@ -365,8 +372,10 @@ export async function scanNoGeometryParcels( qualityBreakdown: { withCadRef: qWithCadRef, withPaperCad: qWithPaperCad, - withPaperCf: qWithPaperCf, + withPaperLb: qWithPaperLb, + withLandbook: qWithLandbook, withArea: qWithArea, + withActiveStatus: qWithActiveStatus, useful: qUseful, empty: qEmpty, }, @@ -425,33 +434,34 @@ export async function syncNoGeometryParcels( } // 3. Filter: not yet in DB + quality gate - // Quality: Must have EITHER a valid cadRef/CF number AND area > 0. - // Items with no identification AND no area are noise — skip them. + // Quality: must be ACTIVE (status=1) AND have identification OR area. + // Items that are inactive, or have no identification AND no area = noise. let filteredOut = 0; const candidates = allImmovables.filter((item) => { const cadRef = normalizeCadRef(item.identifierDetails ?? ""); const immPk = Number(item.immovablePk ?? 0); - // Already in DB? → skip + // Already in DB? → skip (not counted as filtered) if (cadRef && existingCadRefs.has(cadRef)) return false; if (immPk > 0 && existingObjIds.has(-immPk)) return false; - // Quality gate: must have CF/identification reference + // Quality gate 1: must be active (status=1) + const status = typeof item.status === "number" ? item.status : 1; + if (status !== 1) { + filteredOut++; + return false; + } + + // Quality gate 2: must have identification OR area const hasCadRef = !!cadRef; - const hasPaperCf = !!(item.paperCfNo ?? "").toString().trim(); const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim(); const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim(); - const hasIdentification = - hasCadRef || hasPaperCf || hasPaperLb || hasPaperCad; + const hasIdentification = hasCadRef || hasPaperLb || hasPaperCad; - // Quality gate: must have area from any field - const areaFields = [ - item.area, - item.measuredArea, - item.areaValue, - item.suprafata, - ]; - const hasArea = areaFields.some((v) => typeof v === "number" && v > 0); + // Area: measuredArea and legalArea are the only area fields on immovable/list + const hasArea = + (typeof item.measuredArea === "number" && item.measuredArea > 0) || + (typeof item.legalArea === "number" && item.legalArea > 0); if (!hasIdentification && !hasArea) { filteredOut++; @@ -494,26 +504,31 @@ export async function syncNoGeometryParcels( } const cadRef = String(item.identifierDetails ?? "").trim(); - // Extract area from any available field + // Extract area — on immovable/list, the real fields are measuredArea and legalArea const areaValue = - [item.area, item.measuredArea, item.areaValue, item.suprafata] - .map((v) => (typeof v === "number" && v > 0 ? v : null)) - .find((v) => v != null) ?? null; + (typeof item.measuredArea === "number" && item.measuredArea > 0 + ? item.measuredArea + : null) ?? + (typeof item.legalArea === "number" && item.legalArea > 0 + ? item.legalArea + : null); const attributes: Record = { OBJECTID: -immPk, IMMOVABLE_ID: immPk, - WORKSPACE_ID: item.workspacePk ?? wsPk, + WORKSPACE_ID: item.workspace?.nomenPk ?? wsPk, APPLICATION_ID: item.applicationId ?? null, NATIONAL_CADASTRAL_REFERENCE: cadRef, AREA_VALUE: areaValue, - IS_ACTIVE: 1, + IS_ACTIVE: item.status === 1 ? 1 : 0, ADMIN_UNIT_ID: Number(siruta), PAPER_CAD_NO: item.paperCadNo ?? null, - PAPER_CF_NO: item.paperCfNo ?? null, PAPER_LB_NO: item.paperLbNo ?? null, + HAS_LANDBOOK: item.hasLandbook ?? null, TOP_NO: item.topNo ?? null, IMMOVABLE_TYPE: item.immovableType ?? "P", + MEASURED_AREA: item.measuredArea ?? null, + LEGAL_AREA: item.legalArea ?? null, NO_GEOMETRY_SOURCE: "ETERRA_IMMOVABLE_LIST", };