fix(parcel-sync): paperCfNo bug, status filter, enrichment robustness
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
This commit is contained in:
@@ -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() {
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
Cu nr. CF pe hârtie:{" "}
|
||||
Cu nr. CF/LB:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{noGeomScan.qualityBreakdown.withPaperCf.toLocaleString(
|
||||
{noGeomScan.qualityBreakdown.withPaperLb.toLocaleString(
|
||||
"ro-RO",
|
||||
)}
|
||||
</span>
|
||||
@@ -2686,6 +2692,22 @@ export function ParcelSyncModule() {
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
Active (status=1):{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{noGeomScan.qualityBreakdown.withActiveStatus.toLocaleString(
|
||||
"ro-RO",
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
Cu carte funciară:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{noGeomScan.qualityBreakdown.withLandbook.toLocaleString(
|
||||
"ro-RO",
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[11px] pt-0.5 border-t border-muted-foreground/10">
|
||||
<span>
|
||||
@@ -2698,7 +2720,7 @@ export function ParcelSyncModule() {
|
||||
</span>
|
||||
{noGeomScan.qualityBreakdown.empty > 0 && (
|
||||
<span>
|
||||
Fără date identificare:{" "}
|
||||
Filtrate (inactive/fără date):{" "}
|
||||
<span className="font-semibold text-rose-600 dark:text-rose-400">
|
||||
{noGeomScan.qualityBreakdown.empty.toLocaleString(
|
||||
"ro-RO",
|
||||
@@ -2712,7 +2734,7 @@ export function ParcelSyncModule() {
|
||||
{includeNoGeom && (
|
||||
<p className="text-[11px] text-muted-foreground ml-7">
|
||||
{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.
|
||||
</p>
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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<number, number>();
|
||||
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<string, unknown> = {
|
||||
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",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user