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
@@ -391,6 +391,14 @@ export function ParcelSyncModule() {
withGeometry: number;
remoteGisCount: number;
noGeomCount: number;
qualityBreakdown: {
withCadRef: number;
withPaperCad: number;
withPaperCf: number;
withArea: number;
useful: number;
empty: number;
};
localDbTotal: number;
localDbWithGeom: number;
localDbNoGeom: number;
@@ -693,11 +701,20 @@ export function ParcelSyncModule() {
setNoGeomScanning(true);
setNoGeomScan(null);
setNoGeomScanSiruta(s);
const emptyQuality = {
withCadRef: 0,
withPaperCad: 0,
withPaperCf: 0,
withArea: 0,
useful: 0,
empty: 0,
};
const emptyResult = {
totalImmovables: 0,
withGeometry: 0,
remoteGisCount: 0,
noGeomCount: 0,
qualityBreakdown: emptyQuality,
localDbTotal: 0,
localDbWithGeom: 0,
localDbNoGeom: 0,
@@ -719,11 +736,20 @@ export function ParcelSyncModule() {
console.warn("[no-geom-scan]", data.error);
setNoGeomScan(emptyResult);
} else {
const qb = (data.qualityBreakdown ?? {}) as Record<string, unknown>;
setNoGeomScan({
totalImmovables: Number(data.totalImmovables ?? 0),
withGeometry: Number(data.withGeometry ?? 0),
remoteGisCount: Number(data.remoteGisCount ?? 0),
noGeomCount: Number(data.noGeomCount ?? 0),
qualityBreakdown: {
withCadRef: Number(qb.withCadRef ?? 0),
withPaperCad: Number(qb.withPaperCad ?? 0),
withPaperCf: Number(qb.withPaperCf ?? 0),
withArea: Number(qb.withArea ?? 0),
useful: Number(qb.useful ?? 0),
empty: Number(qb.empty ?? 0),
},
localDbTotal: Number(data.localDbTotal ?? 0),
localDbWithGeom: Number(data.localDbWithGeom ?? 0),
localDbNoGeom: Number(data.localDbNoGeom ?? 0),
@@ -2612,6 +2638,70 @@ export function ParcelSyncModule() {
Include și parcelele fără geometrie la export
</span>
</label>
{/* Quality breakdown of no-geom items */}
{scanDone && noGeomScan.noGeomCount > 0 && (
<div className="ml-7 p-2 rounded-md bg-muted/40 space-y-1">
<p className="text-[11px] font-medium text-muted-foreground">
Calitate date (din{" "}
{noGeomScan.noGeomCount.toLocaleString("ro-RO")} fără
geometrie):
</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5 text-[11px] text-muted-foreground">
<span>
Cu nr. cadastral eTerra:{" "}
<span className="font-medium text-foreground">
{noGeomScan.qualityBreakdown.withCadRef.toLocaleString(
"ro-RO",
)}
</span>
</span>
<span>
Cu nr. CF pe hârtie:{" "}
<span className="font-medium text-foreground">
{noGeomScan.qualityBreakdown.withPaperCf.toLocaleString(
"ro-RO",
)}
</span>
</span>
<span>
Cu nr. cad. pe hârtie:{" "}
<span className="font-medium text-foreground">
{noGeomScan.qualityBreakdown.withPaperCad.toLocaleString(
"ro-RO",
)}
</span>
</span>
<span>
Cu suprafață:{" "}
<span className="font-medium text-foreground">
{noGeomScan.qualityBreakdown.withArea.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>
Utilizabile:{" "}
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
{noGeomScan.qualityBreakdown.useful.toLocaleString(
"ro-RO",
)}
</span>
</span>
{noGeomScan.qualityBreakdown.empty > 0 && (
<span>
Fără date identificare:{" "}
<span className="font-semibold text-rose-600 dark:text-rose-400">
{noGeomScan.qualityBreakdown.empty.toLocaleString(
"ro-RO",
)}
</span>
</span>
)}
</div>
</div>
)}
{includeNoGeom && (
<p className="text-[11px] text-muted-foreground ml-7">
Vor fi importate în DB și incluse în CSV + Magic GPKG
@@ -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,