feat: quality analysis for no-geom parcels + raport_calitate.txt

Scan phase:
- qualityBreakdown on NoGeomScanResult: withCadRef, withPaperCad,
  withPaperCf, withArea, useful vs empty counts
- UI scan card shows quality grid before deciding to export

Export phase:
- Comprehensive enrichment quality analysis: owners, CF, address,
  area, category, building — split by with-geom vs no-geom
- raport_calitate.txt in ZIP: human-readable Romanian report with
  per-category breakdowns and percentage stats
- export_report.json includes full qualityAnalysis object
- Progress completion note shows quality summary inline
This commit is contained in:
AI Assistant
2026-03-07 19:23:57 +02:00
parent 53914c7fc3
commit 681b52e816
3 changed files with 334 additions and 11 deletions
@@ -91,6 +91,22 @@ const normalizeId = (value: unknown) => {
const normalizeCadRef = (value: unknown) =>
normalizeId(value).replace(/\s+/g, "").toUpperCase();
/** Quality breakdown of no-geometry immovables from scan */
export type NoGeomQuality = {
/** Have electronic cadRef (identifierDetails non-empty) */
withCadRef: number;
/** Have paper cadastral number */
withPaperCad: number;
/** Have paper CF (carte funciară) number */
withPaperCf: number;
/** Have area > 0 */
withArea: number;
/** "Useful" = have cadRef OR (paperCad AND paperCf) */
useful: number;
/** No cadRef, no paperCad, no paperCf — likely unusable */
empty: number;
};
export type NoGeomScanResult = {
totalImmovables: number;
/** Immovables that matched a remote GIS feature (have geometry) */
@@ -98,6 +114,8 @@ export type NoGeomScanResult = {
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer */
remoteGisCount: number;
noGeomCount: number;
/** Quality breakdown of no-geometry items */
qualityBreakdown: NoGeomQuality;
/** Sample of immovable identifiers without geometry */
samples: Array<{
immovablePk: number;
@@ -155,6 +173,14 @@ export async function scanNoGeometryParcels(
withGeometry: 0,
remoteGisCount: 0,
noGeomCount: 0,
qualityBreakdown: {
withCadRef: 0,
withPaperCad: 0,
withPaperCf: 0,
withArea: 0,
useful: 0,
empty: 0,
},
samples: [],
localDbTotal: 0,
localDbWithGeom: 0,
@@ -287,11 +313,48 @@ export async function scanNoGeometryParcels(
// withGeometry = immovables that MATCHED a GIS feature (always adds up)
const matchedCount = allImmovables.length - noGeomItems.length;
// Quality analysis of no-geom items
// Build a quick lookup for area data from the immovable list
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);
}
}
let qWithCadRef = 0;
let qWithPaperCad = 0;
let qWithPaperCf = 0;
let qWithArea = 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 hasArea = areaByPk.has(item.immovablePk);
if (hasCad) qWithCadRef++;
if (hasPaperCad) qWithPaperCad++;
if (hasPaperCf) qWithPaperCf++;
if (hasArea) qWithArea++;
if (hasCad || (hasPaperCad && hasPaperCf)) qUseful++;
else qEmpty++;
}
return {
totalImmovables: allImmovables.length,
withGeometry: matchedCount,
remoteGisCount: remoteFeatures.length,
noGeomCount: noGeomItems.length,
qualityBreakdown: {
withCadRef: qWithCadRef,
withPaperCad: qWithPaperCad,
withPaperCf: qWithPaperCf,
withArea: qWithArea,
useful: qUseful,
empty: qEmpty,
},
samples: noGeomItems.slice(0, 20),
localDbTotal: localTotal,
localDbWithGeom: localWithGeom,