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
+181 -11
View File
@@ -540,6 +540,84 @@ export async function POST(req: Request) {
zip.file("terenuri.gpkg", terenuriGpkg);
zip.file("cladiri.gpkg", cladiriGpkg);
// ── Comprehensive quality analysis ──
const withGeomRecords = dbTerenuri.filter(
(r) =>
(r as unknown as { geometrySource: string | null }).geometrySource !==
"NO_GEOMETRY",
);
const noGeomRecords = dbTerenuri.filter(
(r) =>
(r as unknown as { geometrySource: string | null }).geometrySource ===
"NO_GEOMETRY",
);
const analyzeRecords = (records: typeof dbTerenuri) => {
let enriched = 0;
let withOwners = 0;
let withOldOwners = 0;
let withCF = 0;
let withAddress = 0;
let withArea = 0;
let withCategory = 0;
let withBuilding = 0;
let complete = 0;
let partial = 0;
let empty = 0;
for (const r of records) {
const e = r.enrichment as Record<string, unknown> | null;
if (!e) {
empty++;
continue;
}
enriched++;
const hasOwners = !!e.PROPRIETARI && e.PROPRIETARI !== "-";
const hasOldOwners =
!!e.PROPRIETARI_VECHI && String(e.PROPRIETARI_VECHI).trim() !== "";
const hasCF = !!e.NR_CF && e.NR_CF !== "-";
const hasAddr = !!e.ADRESA && e.ADRESA !== "-";
const hasArea =
e.SUPRAFATA_2D != null &&
e.SUPRAFATA_2D !== "" &&
Number(e.SUPRAFATA_2D) > 0;
const hasCat = !!e.CATEGORIE_FOLOSINTA && e.CATEGORIE_FOLOSINTA !== "-";
const hasBuild = Number(e.HAS_BUILDING ?? 0) === 1;
if (hasOwners) withOwners++;
if (hasOldOwners) withOldOwners++;
if (hasCF) withCF++;
if (hasAddr) withAddress++;
if (hasArea) withArea++;
if (hasCat) withCategory++;
if (hasBuild) withBuilding++;
// "Complete" = has at least owners + CF + area
if (hasOwners && hasCF && hasArea) complete++;
else if (hasOwners || hasCF || hasAddr || hasArea || hasCat) partial++;
else empty++;
}
return {
total: records.length,
enriched,
withOwners,
withOldOwners,
withCF,
withAddress,
withArea,
withCategory,
withBuilding,
complete,
partial,
empty,
};
};
const qualityAll = analyzeRecords(dbTerenuri);
const qualityGeom = analyzeRecords(withGeomRecords);
const qualityNoGeom = analyzeRecords(noGeomRecords);
const report: Record<string, unknown> = {
siruta: validated.siruta,
generatedAt: new Date().toISOString(),
@@ -547,11 +625,7 @@ export async function POST(req: Request) {
terenuri: {
count: terenuriGeoFeatures.length,
totalInDb: dbTerenuri.length,
noGeometryCount: dbTerenuri.filter(
(r) =>
(r as unknown as { geometrySource: string | null })
.geometrySource === "NO_GEOMETRY",
).length,
noGeometryCount: noGeomRecords.length,
},
cladiri: { count: cladiriGeoFeatures.length },
syncSkipped: {
@@ -560,6 +634,11 @@ export async function POST(req: Request) {
},
includeNoGeometry: hasNoGeom,
noGeomImported,
qualityAnalysis: {
all: qualityAll,
withGeometry: qualityGeom,
noGeometry: qualityNoGeom,
},
};
if (validated.mode === "magic" && magicGpkg && csvContent) {
@@ -570,6 +649,89 @@ export async function POST(req: Request) {
hasBuildingCount,
legalBuildingCount,
};
// Generate human-readable quality report (Romanian)
const pct = (n: number, total: number) =>
total > 0 ? `${((n / total) * 100).toFixed(1)}%` : "0%";
const fmt = (n: number) => n.toLocaleString("ro-RO");
const lines: string[] = [
`══════════════════════════════════════════════════════════`,
` RAPORT CALITATE DATE — UAT SIRUTA ${validated.siruta}`,
` Generat: ${new Date().toISOString().replace("T", " ").slice(0, 19)}`,
`══════════════════════════════════════════════════════════`,
``,
`REZUMAT GENERAL`,
`─────────────────────────────────────────────────────────`,
` Total parcele în baza de date: ${fmt(dbTerenuri.length)}`,
` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`,
` • Fără geometrie (doar date): ${fmt(noGeomRecords.length)}`,
` Clădiri: ${fmt(cladiriGeoFeatures.length)}`,
``,
`CALITATE ÎMBOGĂȚIRE — TOATE PARCELELE (${fmt(qualityAll.total)})`,
`─────────────────────────────────────────────────────────`,
` Îmbogățite: ${fmt(qualityAll.enriched)} (${pct(qualityAll.enriched, qualityAll.total)})`,
` Cu proprietari: ${fmt(qualityAll.withOwners)} (${pct(qualityAll.withOwners, qualityAll.total)})`,
` Cu prop. vechi: ${fmt(qualityAll.withOldOwners)} (${pct(qualityAll.withOldOwners, qualityAll.total)})`,
` Cu nr. CF: ${fmt(qualityAll.withCF)} (${pct(qualityAll.withCF, qualityAll.total)})`,
` Cu adresă: ${fmt(qualityAll.withAddress)} (${pct(qualityAll.withAddress, qualityAll.total)})`,
` Cu suprafață: ${fmt(qualityAll.withArea)} (${pct(qualityAll.withArea, qualityAll.total)})`,
` Cu categorie fol.: ${fmt(qualityAll.withCategory)} (${pct(qualityAll.withCategory, qualityAll.total)})`,
` Cu clădire: ${fmt(qualityAll.withBuilding)} (${pct(qualityAll.withBuilding, qualityAll.total)})`,
` ────────────────`,
` Complete (prop+CF+sup): ${fmt(qualityAll.complete)} (${pct(qualityAll.complete, qualityAll.total)})`,
` Parțiale: ${fmt(qualityAll.partial)} (${pct(qualityAll.partial, qualityAll.total)})`,
` Goale (fără date): ${fmt(qualityAll.empty)} (${pct(qualityAll.empty, qualityAll.total)})`,
``,
];
if (withGeomRecords.length > 0) {
lines.push(
`PARCELE CU GEOMETRIE (${fmt(qualityGeom.total)})`,
`─────────────────────────────────────────────────────────`,
` Îmbogățite: ${fmt(qualityGeom.enriched)} (${pct(qualityGeom.enriched, qualityGeom.total)})`,
` Cu proprietari: ${fmt(qualityGeom.withOwners)} (${pct(qualityGeom.withOwners, qualityGeom.total)})`,
` Cu nr. CF: ${fmt(qualityGeom.withCF)} (${pct(qualityGeom.withCF, qualityGeom.total)})`,
` Cu adresă: ${fmt(qualityGeom.withAddress)} (${pct(qualityGeom.withAddress, qualityGeom.total)})`,
` Cu suprafață: ${fmt(qualityGeom.withArea)} (${pct(qualityGeom.withArea, qualityGeom.total)})`,
` Cu categorie fol.: ${fmt(qualityGeom.withCategory)} (${pct(qualityGeom.withCategory, qualityGeom.total)})`,
` Complete: ${fmt(qualityGeom.complete)} (${pct(qualityGeom.complete, qualityGeom.total)})`,
` Parțiale: ${fmt(qualityGeom.partial)} (${pct(qualityGeom.partial, qualityGeom.total)})`,
` Goale: ${fmt(qualityGeom.empty)} (${pct(qualityGeom.empty, qualityGeom.total)})`,
``,
);
}
if (noGeomRecords.length > 0) {
lines.push(
`PARCELE FĂRĂ GEOMETRIE (${fmt(qualityNoGeom.total)})`,
`─────────────────────────────────────────────────────────`,
` Îmbogățite: ${fmt(qualityNoGeom.enriched)} (${pct(qualityNoGeom.enriched, qualityNoGeom.total)})`,
` Cu proprietari: ${fmt(qualityNoGeom.withOwners)} (${pct(qualityNoGeom.withOwners, qualityNoGeom.total)})`,
` Cu nr. CF: ${fmt(qualityNoGeom.withCF)} (${pct(qualityNoGeom.withCF, qualityNoGeom.total)})`,
` Cu adresă: ${fmt(qualityNoGeom.withAddress)} (${pct(qualityNoGeom.withAddress, qualityNoGeom.total)})`,
` Cu suprafață: ${fmt(qualityNoGeom.withArea)} (${pct(qualityNoGeom.withArea, qualityNoGeom.total)})`,
` Cu categorie fol.: ${fmt(qualityNoGeom.withCategory)} (${pct(qualityNoGeom.withCategory, qualityNoGeom.total)})`,
` Complete: ${fmt(qualityNoGeom.complete)} (${pct(qualityNoGeom.complete, qualityNoGeom.total)})`,
` Parțiale: ${fmt(qualityNoGeom.partial)} (${pct(qualityNoGeom.partial, qualityNoGeom.total)})`,
` Goale: ${fmt(qualityNoGeom.empty)} (${pct(qualityNoGeom.empty, qualityNoGeom.total)})`,
``,
);
}
lines.push(
`NOTE`,
`─────────────────────────────────────────────────────────`,
` • "Complete" = are proprietari + nr. CF + suprafață`,
` • "Parțiale" = are cel puțin un câmp util`,
` • "Goale" = niciun câmp de îmbogățire completat`,
` • Parcelele fără geometrie provin din lista de imobile`,
` eTerra și nu au contur desenat în layerul GIS.`,
` • Datele sunt extrase din ANCPI eTerra la data raportului.`,
`══════════════════════════════════════════════════════════`,
);
zip.file("raport_calitate.txt", lines.join("\n"));
}
zip.file("export_report.json", JSON.stringify(report, null, 2));
@@ -580,11 +742,7 @@ export async function POST(req: Request) {
finishPhase();
/* Done */
const noGeomInDb = dbTerenuri.filter(
(r) =>
(r as unknown as { geometrySource: string | null }).geometrySource ===
"NO_GEOMETRY",
).length;
const noGeomInDb = noGeomRecords.length;
message = `Finalizat 100% · Terenuri ${terenuriGeoFeatures.length} · Clădiri ${cladiriGeoFeatures.length}`;
if (noGeomInDb > 0) {
message += ` · Fără geometrie ${noGeomInDb}`;
@@ -592,9 +750,21 @@ export async function POST(req: Request) {
if (!terenuriNeedsSync && !cladiriNeedsSync) {
message += " (din cache local)";
}
// Quality summary in note (visible in UI progress card)
if (validated.mode === "magic") {
const qParts: string[] = [];
qParts.push(`Complete: ${qualityAll.complete}/${qualityAll.total}`);
if (qualityAll.partial > 0)
qParts.push(`parțiale: ${qualityAll.partial}`);
if (qualityAll.empty > 0) qParts.push(`goale: ${qualityAll.empty}`);
qParts.push(`prop: ${qualityAll.withOwners}`);
qParts.push(`CF: ${qualityAll.withCF}`);
qParts.push(`sup: ${qualityAll.withArea}`);
note = `Calitate: ${qParts.join(" · ")} — vezi raport_calitate.txt în ZIP`;
}
status = "done";
phase = "Finalizat";
note = undefined;
// note already set with quality summary above (or undefined for base mode)
pushProgress();
scheduleClear(jobId);