feat(parcel-sync): quality gate filter for no-geom import + diagnostic endpoint
- Filter no-geom items before import: must have identification (cadRef/CF/paperCad/paperLb) OR area - Multi-field area extraction: area, measuredArea, areaValue, suprafata - Scan quality breakdown: withCadRef, withPaperCf, withPaperCad, withArea, useful, empty - Added paperLbNo to quality analysis and samples - UI: quality breakdown grid in scan card - UI: filtered count in workflow preview (shows useful, not total) - UI: enrichment estimate uses useful count - New diagnostic endpoint /api/eterra/no-geom-debug for field inspection
This commit is contained in:
@@ -2525,12 +2525,19 @@ export function ParcelSyncModule() {
|
||||
Import parcele fără geometrie —{" "}
|
||||
<span className="font-medium text-amber-600 dark:text-amber-400">
|
||||
{(() => {
|
||||
// Only useful items will be imported (quality filter)
|
||||
const usefulNoGeom =
|
||||
noGeomScan.qualityBreakdown.useful;
|
||||
const newNoGeom = Math.max(
|
||||
0,
|
||||
noGeomScan.noGeomCount - noGeomScan.localDbNoGeom,
|
||||
usefulNoGeom - noGeomScan.localDbNoGeom,
|
||||
);
|
||||
const filtered = noGeomScan.qualityBreakdown.empty;
|
||||
return newNoGeom > 0
|
||||
? `~${newNoGeom.toLocaleString("ro-RO")} noi de importat`
|
||||
? `~${newNoGeom.toLocaleString("ro-RO")} noi de importat` +
|
||||
(filtered > 0
|
||||
? ` (${filtered.toLocaleString("ro-RO")} filtrate)`
|
||||
: "")
|
||||
: "deja importate";
|
||||
})()}
|
||||
</span>
|
||||
@@ -2540,13 +2547,13 @@ export function ParcelSyncModule() {
|
||||
Îmbogățire CF, proprietari, adrese —{" "}
|
||||
<span className="font-medium text-teal-600 dark:text-teal-400">
|
||||
{(() => {
|
||||
const usefulNoGeom = noGeomScan.qualityBreakdown.useful;
|
||||
const totalToEnrich =
|
||||
noGeomScan.localDbTotal +
|
||||
(includeNoGeom
|
||||
? Math.max(
|
||||
0,
|
||||
noGeomScan.noGeomCount -
|
||||
noGeomScan.localDbNoGeom,
|
||||
usefulNoGeom - noGeomScan.localDbNoGeom,
|
||||
)
|
||||
: 0);
|
||||
// Use enrichedComplete (not enriched) — stale
|
||||
@@ -2704,9 +2711,10 @@ export function ParcelSyncModule() {
|
||||
)}
|
||||
{includeNoGeom && (
|
||||
<p className="text-[11px] text-muted-foreground ml-7">
|
||||
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.
|
||||
{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.`
|
||||
: "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>
|
||||
)}
|
||||
{workflowPreview}
|
||||
|
||||
@@ -122,6 +122,7 @@ export type NoGeomScanResult = {
|
||||
identifierDetails: string;
|
||||
paperCadNo?: string;
|
||||
paperCfNo?: string;
|
||||
paperLbNo?: string;
|
||||
}>;
|
||||
/** Total features already in local DB (geometry + no-geom) */
|
||||
localDbTotal: number;
|
||||
@@ -232,6 +233,7 @@ export async function scanNoGeometryParcels(
|
||||
identifierDetails: string;
|
||||
paperCadNo?: string;
|
||||
paperCfNo?: string;
|
||||
paperLbNo?: string;
|
||||
}> = [];
|
||||
|
||||
for (const item of allImmovables) {
|
||||
@@ -250,6 +252,7 @@ export async function scanNoGeometryParcels(
|
||||
identifierDetails: String(item.identifierDetails ?? ""),
|
||||
paperCadNo: item.paperCadNo ?? undefined,
|
||||
paperCfNo: item.paperCfNo ?? undefined,
|
||||
paperLbNo: item.paperLbNo ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -314,12 +317,20 @@ 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
|
||||
// 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);
|
||||
if (pk > 0 && typeof item.area === "number" && item.area > 0) {
|
||||
areaByPk.set(pk, item.area);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,12 +344,16 @@ export async function scanNoGeometryParcels(
|
||||
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);
|
||||
if (hasCad) qWithCadRef++;
|
||||
if (hasPaperCad) qWithPaperCad++;
|
||||
if (hasPaperCf) qWithPaperCf++;
|
||||
if (hasArea) qWithArea++;
|
||||
if (hasCad || (hasPaperCad && hasPaperCf)) qUseful++;
|
||||
// "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++;
|
||||
else qEmpty++;
|
||||
}
|
||||
|
||||
@@ -409,17 +424,50 @@ export async function syncNoGeometryParcels(
|
||||
existingObjIds.add(f.objectId);
|
||||
}
|
||||
|
||||
// 3. Filter to only those not yet in DB
|
||||
// 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.
|
||||
let filteredOut = 0;
|
||||
const candidates = allImmovables.filter((item) => {
|
||||
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
|
||||
const immPk = Number(item.immovablePk ?? 0);
|
||||
|
||||
// Already in DB? → skip
|
||||
if (cadRef && existingCadRefs.has(cadRef)) return false;
|
||||
if (immPk > 0 && existingObjIds.has(-immPk)) return false;
|
||||
|
||||
// Quality gate: must have CF/identification reference
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
if (!hasIdentification && !hasArea) {
|
||||
filteredOut++;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return { imported: 0, skipped: 0, errors: 0, status: "done" };
|
||||
return {
|
||||
imported: 0,
|
||||
skipped: filteredOut,
|
||||
errors: 0,
|
||||
status: "done",
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Import candidates in batches with retry
|
||||
@@ -446,7 +494,11 @@ export async function syncNoGeometryParcels(
|
||||
}
|
||||
|
||||
const cadRef = String(item.identifierDetails ?? "").trim();
|
||||
const areaValue = typeof item.area === "number" ? item.area : null;
|
||||
// Extract area from any available field
|
||||
const areaValue =
|
||||
[item.area, item.measuredArea, item.areaValue, item.suprafata]
|
||||
.map((v) => (typeof v === "number" && v > 0 ? v : null))
|
||||
.find((v) => v != null) ?? null;
|
||||
|
||||
const attributes: Record<string, unknown> = {
|
||||
OBJECTID: -immPk,
|
||||
@@ -527,7 +579,7 @@ export async function syncNoGeometryParcels(
|
||||
options?.onProgress?.(done, total, "Import parcele fără geometrie");
|
||||
}
|
||||
|
||||
return { imported, skipped, errors, status: "done" };
|
||||
return { imported, skipped: skipped + filteredOut, errors, status: "done" };
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
return { imported: 0, skipped: 0, errors: 0, status: "error", error: msg };
|
||||
|
||||
Reference in New Issue
Block a user