fix: scan numbers always add up, match quality tracking, pipeline audit
SCAN DISPLAY: - Use matchedCount (withGeometry) for 'cu geometrie' — ALWAYS adds up with noGeomCount to equal totalImmovables (ground truth arithmetic) - Show remoteGisCount separately as 'Layer GIS: N features (se descarca toate)' - When remoteGisCount != matchedCount, show matching detail with breakdown (X potrivite + cadRef/ID split) so mismatches are transparent - Workflow preview step 1 still uses remoteGisCount (correct: all GIS features get downloaded regardless of matching) MATCH QUALITY TRACKING: - New fields: matchedByRef, matchedById in NoGeomScanResult - Track how many immovables matched by cadastral ref vs by IMMOVABLE_ID - Console log match quality for server-side debugging - scannedAt timestamp for audit trail PIPELINE AUDIT (export report): - New 'pipeline' section in export_report.json with full trace: syncedGis, noGeometry (imported/cleaned/skipped), enriched, finalDb - raport_calitate.txt now has PIPELINE section before quality analysis showing exactly what happened at each step - Capture noGeomCleaned + noGeomSkipped in addition to noGeomImported
This commit is contained in:
@@ -256,6 +256,8 @@ export async function POST(req: Request) {
|
||||
/* Phase 1b: Import no-geometry parcels (optional) */
|
||||
/* ══════════════════════════════════════════════════════════ */
|
||||
let noGeomImported = 0;
|
||||
let noGeomCleaned = 0;
|
||||
let noGeomSkipped = 0;
|
||||
if (hasNoGeom && weights.noGeom > 0) {
|
||||
setPhaseState("Import parcele fără geometrie", weights.noGeom, 1);
|
||||
const noGeomClient = await EterraClient.create(
|
||||
@@ -282,6 +284,8 @@ export async function POST(req: Request) {
|
||||
pushProgress();
|
||||
} else {
|
||||
noGeomImported = noGeomResult.imported;
|
||||
noGeomCleaned = noGeomResult.cleaned;
|
||||
noGeomSkipped = noGeomResult.skipped;
|
||||
const cleanedNote =
|
||||
noGeomResult.cleaned > 0
|
||||
? `, ${noGeomResult.cleaned} vechi șterse`
|
||||
@@ -626,6 +630,26 @@ export async function POST(req: Request) {
|
||||
siruta: validated.siruta,
|
||||
generatedAt: new Date().toISOString(),
|
||||
source: "local-db (sync-first)",
|
||||
pipeline: {
|
||||
syncedGis: {
|
||||
terenuri: terenuriNeedsSync ? "descărcat" : "din cache",
|
||||
cladiri: cladiriNeedsSync ? "descărcat" : "din cache",
|
||||
},
|
||||
noGeometry: hasNoGeom
|
||||
? {
|
||||
imported: noGeomImported,
|
||||
cleaned: noGeomCleaned,
|
||||
skipped: noGeomSkipped,
|
||||
}
|
||||
: "dezactivat",
|
||||
enriched: validated.mode === "magic" ? "da" : "nu",
|
||||
finalDb: {
|
||||
total: dbTerenuri.length,
|
||||
withGeometry: withGeomRecords.length,
|
||||
noGeometry: noGeomRecords.length,
|
||||
cladiri: cladiriGeoFeatures.length,
|
||||
},
|
||||
},
|
||||
terenuri: {
|
||||
count: terenuriGeoFeatures.length,
|
||||
totalInDb: dbTerenuri.length,
|
||||
@@ -665,7 +689,21 @@ export async function POST(req: Request) {
|
||||
` Generat: ${new Date().toISOString().replace("T", " ").slice(0, 19)}`,
|
||||
`══════════════════════════════════════════════════════════`,
|
||||
``,
|
||||
`REZUMAT GENERAL`,
|
||||
`PIPELINE — CE S-A ÎNTÂMPLAT`,
|
||||
`─────────────────────────────────────────────────────────`,
|
||||
` 1. Sync GIS terenuri: ${terenuriNeedsSync ? "descărcat din eTerra" : "din cache local (date proaspete)"}`,
|
||||
` 2. Sync GIS clădiri: ${cladiriNeedsSync ? "descărcat din eTerra" : "din cache local (date proaspete)"}`,
|
||||
...(hasNoGeom
|
||||
? [
|
||||
` 3. Import fără geometrie: ${fmt(noGeomImported)} noi importate` +
|
||||
(noGeomCleaned > 0 ? `, ${fmt(noGeomCleaned)} vechi șterse` : "") +
|
||||
(noGeomSkipped > 0 ? `, ${fmt(noGeomSkipped)} filtrate/skip` : ""),
|
||||
]
|
||||
: [` 3. Import fără geometrie: dezactivat`]),
|
||||
` 4. Îmbogățire (CF, prop.): da`,
|
||||
` 5. Generare fișiere: GPKG + CSV + raport`,
|
||||
``,
|
||||
`STARE FINALĂ BAZĂ DE DATE`,
|
||||
`─────────────────────────────────────────────────────────`,
|
||||
` Total parcele în baza de date: ${fmt(dbTerenuri.length)}`,
|
||||
` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`,
|
||||
|
||||
@@ -391,6 +391,8 @@ export function ParcelSyncModule() {
|
||||
withGeometry: number;
|
||||
remoteGisCount: number;
|
||||
noGeomCount: number;
|
||||
matchedByRef: number;
|
||||
matchedById: number;
|
||||
qualityBreakdown: {
|
||||
withCadRef: number;
|
||||
withPaperCad: number;
|
||||
@@ -407,6 +409,7 @@ export function ParcelSyncModule() {
|
||||
localDbEnriched: number;
|
||||
localDbEnrichedComplete: number;
|
||||
localSyncFresh: boolean;
|
||||
scannedAt: string;
|
||||
} | null>(null);
|
||||
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
|
||||
|
||||
@@ -718,6 +721,8 @@ export function ParcelSyncModule() {
|
||||
withGeometry: 0,
|
||||
remoteGisCount: 0,
|
||||
noGeomCount: 0,
|
||||
matchedByRef: 0,
|
||||
matchedById: 0,
|
||||
qualityBreakdown: emptyQuality,
|
||||
localDbTotal: 0,
|
||||
localDbWithGeom: 0,
|
||||
@@ -725,6 +730,7 @@ export function ParcelSyncModule() {
|
||||
localDbEnriched: 0,
|
||||
localDbEnrichedComplete: 0,
|
||||
localSyncFresh: false,
|
||||
scannedAt: "",
|
||||
};
|
||||
try {
|
||||
const res = await fetch("/api/eterra/no-geom-scan", {
|
||||
@@ -746,6 +752,8 @@ export function ParcelSyncModule() {
|
||||
withGeometry: Number(data.withGeometry ?? 0),
|
||||
remoteGisCount: Number(data.remoteGisCount ?? 0),
|
||||
noGeomCount: Number(data.noGeomCount ?? 0),
|
||||
matchedByRef: Number(data.matchedByRef ?? 0),
|
||||
matchedById: Number(data.matchedById ?? 0),
|
||||
qualityBreakdown: {
|
||||
withCadRef: Number(qb.withCadRef ?? 0),
|
||||
withPaperCad: Number(qb.withPaperCad ?? 0),
|
||||
@@ -762,6 +770,7 @@ export function ParcelSyncModule() {
|
||||
localDbEnriched: Number(data.localDbEnriched ?? 0),
|
||||
localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0),
|
||||
localSyncFresh: Boolean(data.localSyncFresh),
|
||||
scannedAt: String(data.scannedAt ?? new Date().toISOString()),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@@ -2601,7 +2610,7 @@ export function ParcelSyncModule() {
|
||||
</span>{" "}
|
||||
imobile în eTerra:{" "}
|
||||
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
|
||||
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
|
||||
{noGeomScan.withGeometry.toLocaleString("ro-RO")}
|
||||
</span>{" "}
|
||||
cu geometrie,{" "}
|
||||
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
||||
@@ -2609,15 +2618,25 @@ export function ParcelSyncModule() {
|
||||
</span>{" "}
|
||||
<span className="font-medium">fără geometrie</span>
|
||||
</p>
|
||||
{noGeomScan.withGeometry <
|
||||
noGeomScan.remoteGisCount && (
|
||||
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
|
||||
{noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
|
||||
din{" "}
|
||||
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "}
|
||||
features GIS au corespondent în lista de imobile
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
|
||||
Layer GIS:{" "}
|
||||
<span className="font-medium">
|
||||
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
|
||||
</span>
|
||||
{" features (se descarcă toate)"}
|
||||
{noGeomScan.remoteGisCount !== noGeomScan.withGeometry && (
|
||||
<>
|
||||
{" · "}
|
||||
{noGeomScan.withGeometry.toLocaleString("ro-RO")} potrivite
|
||||
cu lista de imobile
|
||||
{noGeomScan.matchedByRef > 0 && noGeomScan.matchedById > 0 && (
|
||||
<span className="text-muted-foreground/50">
|
||||
{" "}({noGeomScan.matchedByRef} cadRef + {noGeomScan.matchedById} ID)
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">
|
||||
Cele fără geometrie există în baza de date eTerra dar
|
||||
nu au contur desenat în layerul GIS.
|
||||
|
||||
@@ -118,6 +118,9 @@ export type NoGeomScanResult = {
|
||||
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer */
|
||||
remoteGisCount: number;
|
||||
noGeomCount: number;
|
||||
/** Match quality: how many matched by cadastral ref vs immovable ID */
|
||||
matchedByRef: number;
|
||||
matchedById: number;
|
||||
/** Quality breakdown of no-geometry items */
|
||||
qualityBreakdown: NoGeomQuality;
|
||||
/** Sample of immovable identifiers without geometry */
|
||||
@@ -142,6 +145,8 @@ export type NoGeomScanResult = {
|
||||
localDbEnrichedComplete: number;
|
||||
/** Whether local sync is fresh (< 7 days) */
|
||||
localSyncFresh: boolean;
|
||||
/** Timestamp of the scan (for audit trail) */
|
||||
scannedAt: string;
|
||||
/** Error message if workspace couldn't be resolved */
|
||||
error?: string;
|
||||
};
|
||||
@@ -181,6 +186,8 @@ export async function scanNoGeometryParcels(
|
||||
withGeometry: 0,
|
||||
remoteGisCount: 0,
|
||||
noGeomCount: 0,
|
||||
matchedByRef: 0,
|
||||
matchedById: 0,
|
||||
qualityBreakdown: {
|
||||
withCadRef: 0,
|
||||
withPaperCad: 0,
|
||||
@@ -198,6 +205,7 @@ export async function scanNoGeometryParcels(
|
||||
localDbEnriched: 0,
|
||||
localDbEnrichedComplete: 0,
|
||||
localSyncFresh: false,
|
||||
scannedAt: new Date().toISOString(),
|
||||
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
|
||||
};
|
||||
}
|
||||
@@ -248,16 +256,25 @@ export async function scanNoGeometryParcels(
|
||||
legalArea?: number;
|
||||
}> = [];
|
||||
|
||||
let matchedByRef = 0;
|
||||
let matchedById = 0;
|
||||
|
||||
for (const item of allImmovables) {
|
||||
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
|
||||
const immPk = Number(item.immovablePk ?? 0);
|
||||
const immId = normalizeId(item.immovablePk);
|
||||
|
||||
// Present in remote GIS layer by cadastral ref? → has geometry
|
||||
if (cadRef && remoteCadRefs.has(cadRef)) continue;
|
||||
if (cadRef && remoteCadRefs.has(cadRef)) {
|
||||
matchedByRef++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Present in remote GIS layer by IMMOVABLE_ID? → has geometry
|
||||
if (immId && remoteImmIds.has(immId)) continue;
|
||||
if (immId && remoteImmIds.has(immId)) {
|
||||
matchedById++;
|
||||
continue;
|
||||
}
|
||||
|
||||
noGeomItems.push({
|
||||
immovablePk: immPk,
|
||||
@@ -334,6 +351,12 @@ export async function scanNoGeometryParcels(
|
||||
// withGeometry = immovables that MATCHED a GIS feature (always adds up)
|
||||
const matchedCount = allImmovables.length - noGeomItems.length;
|
||||
|
||||
console.log(
|
||||
`[no-geom-scan] Match quality: ${matchedCount} total (${matchedByRef} by cadRef, ${matchedById} by immId)` +
|
||||
` | GIS layer: ${remoteFeatures.length} features | Immovables: ${allImmovables.length}` +
|
||||
` | Unmatched GIS: ${remoteFeatures.length - matchedCount}`,
|
||||
);
|
||||
|
||||
// Quality analysis of no-geom items
|
||||
let qWithCadRef = 0;
|
||||
let qWithPaperCad = 0;
|
||||
@@ -370,6 +393,8 @@ export async function scanNoGeometryParcels(
|
||||
withGeometry: matchedCount,
|
||||
remoteGisCount: remoteFeatures.length,
|
||||
noGeomCount: noGeomItems.length,
|
||||
matchedByRef,
|
||||
matchedById,
|
||||
qualityBreakdown: {
|
||||
withCadRef: qWithCadRef,
|
||||
withPaperCad: qWithPaperCad,
|
||||
@@ -387,6 +412,7 @@ export async function scanNoGeometryParcels(
|
||||
localDbEnriched: localEnriched,
|
||||
localDbEnrichedComplete: localEnrichedComplete,
|
||||
localSyncFresh: syncFresh,
|
||||
scannedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user