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) */
|
/* Phase 1b: Import no-geometry parcels (optional) */
|
||||||
/* ══════════════════════════════════════════════════════════ */
|
/* ══════════════════════════════════════════════════════════ */
|
||||||
let noGeomImported = 0;
|
let noGeomImported = 0;
|
||||||
|
let noGeomCleaned = 0;
|
||||||
|
let noGeomSkipped = 0;
|
||||||
if (hasNoGeom && weights.noGeom > 0) {
|
if (hasNoGeom && weights.noGeom > 0) {
|
||||||
setPhaseState("Import parcele fără geometrie", weights.noGeom, 1);
|
setPhaseState("Import parcele fără geometrie", weights.noGeom, 1);
|
||||||
const noGeomClient = await EterraClient.create(
|
const noGeomClient = await EterraClient.create(
|
||||||
@@ -282,6 +284,8 @@ export async function POST(req: Request) {
|
|||||||
pushProgress();
|
pushProgress();
|
||||||
} else {
|
} else {
|
||||||
noGeomImported = noGeomResult.imported;
|
noGeomImported = noGeomResult.imported;
|
||||||
|
noGeomCleaned = noGeomResult.cleaned;
|
||||||
|
noGeomSkipped = noGeomResult.skipped;
|
||||||
const cleanedNote =
|
const cleanedNote =
|
||||||
noGeomResult.cleaned > 0
|
noGeomResult.cleaned > 0
|
||||||
? `, ${noGeomResult.cleaned} vechi șterse`
|
? `, ${noGeomResult.cleaned} vechi șterse`
|
||||||
@@ -626,6 +630,26 @@ export async function POST(req: Request) {
|
|||||||
siruta: validated.siruta,
|
siruta: validated.siruta,
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
source: "local-db (sync-first)",
|
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: {
|
terenuri: {
|
||||||
count: terenuriGeoFeatures.length,
|
count: terenuriGeoFeatures.length,
|
||||||
totalInDb: dbTerenuri.length,
|
totalInDb: dbTerenuri.length,
|
||||||
@@ -665,7 +689,21 @@ export async function POST(req: Request) {
|
|||||||
` Generat: ${new Date().toISOString().replace("T", " ").slice(0, 19)}`,
|
` 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)}`,
|
` Total parcele în baza de date: ${fmt(dbTerenuri.length)}`,
|
||||||
` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`,
|
` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`,
|
||||||
|
|||||||
@@ -391,6 +391,8 @@ export function ParcelSyncModule() {
|
|||||||
withGeometry: number;
|
withGeometry: number;
|
||||||
remoteGisCount: number;
|
remoteGisCount: number;
|
||||||
noGeomCount: number;
|
noGeomCount: number;
|
||||||
|
matchedByRef: number;
|
||||||
|
matchedById: number;
|
||||||
qualityBreakdown: {
|
qualityBreakdown: {
|
||||||
withCadRef: number;
|
withCadRef: number;
|
||||||
withPaperCad: number;
|
withPaperCad: number;
|
||||||
@@ -407,6 +409,7 @@ export function ParcelSyncModule() {
|
|||||||
localDbEnriched: number;
|
localDbEnriched: number;
|
||||||
localDbEnrichedComplete: number;
|
localDbEnrichedComplete: number;
|
||||||
localSyncFresh: boolean;
|
localSyncFresh: boolean;
|
||||||
|
scannedAt: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
|
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
|
||||||
|
|
||||||
@@ -718,6 +721,8 @@ export function ParcelSyncModule() {
|
|||||||
withGeometry: 0,
|
withGeometry: 0,
|
||||||
remoteGisCount: 0,
|
remoteGisCount: 0,
|
||||||
noGeomCount: 0,
|
noGeomCount: 0,
|
||||||
|
matchedByRef: 0,
|
||||||
|
matchedById: 0,
|
||||||
qualityBreakdown: emptyQuality,
|
qualityBreakdown: emptyQuality,
|
||||||
localDbTotal: 0,
|
localDbTotal: 0,
|
||||||
localDbWithGeom: 0,
|
localDbWithGeom: 0,
|
||||||
@@ -725,6 +730,7 @@ export function ParcelSyncModule() {
|
|||||||
localDbEnriched: 0,
|
localDbEnriched: 0,
|
||||||
localDbEnrichedComplete: 0,
|
localDbEnrichedComplete: 0,
|
||||||
localSyncFresh: false,
|
localSyncFresh: false,
|
||||||
|
scannedAt: "",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/eterra/no-geom-scan", {
|
const res = await fetch("/api/eterra/no-geom-scan", {
|
||||||
@@ -746,6 +752,8 @@ export function ParcelSyncModule() {
|
|||||||
withGeometry: Number(data.withGeometry ?? 0),
|
withGeometry: Number(data.withGeometry ?? 0),
|
||||||
remoteGisCount: Number(data.remoteGisCount ?? 0),
|
remoteGisCount: Number(data.remoteGisCount ?? 0),
|
||||||
noGeomCount: Number(data.noGeomCount ?? 0),
|
noGeomCount: Number(data.noGeomCount ?? 0),
|
||||||
|
matchedByRef: Number(data.matchedByRef ?? 0),
|
||||||
|
matchedById: Number(data.matchedById ?? 0),
|
||||||
qualityBreakdown: {
|
qualityBreakdown: {
|
||||||
withCadRef: Number(qb.withCadRef ?? 0),
|
withCadRef: Number(qb.withCadRef ?? 0),
|
||||||
withPaperCad: Number(qb.withPaperCad ?? 0),
|
withPaperCad: Number(qb.withPaperCad ?? 0),
|
||||||
@@ -762,6 +770,7 @@ export function ParcelSyncModule() {
|
|||||||
localDbEnriched: Number(data.localDbEnriched ?? 0),
|
localDbEnriched: Number(data.localDbEnriched ?? 0),
|
||||||
localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0),
|
localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0),
|
||||||
localSyncFresh: Boolean(data.localSyncFresh),
|
localSyncFresh: Boolean(data.localSyncFresh),
|
||||||
|
scannedAt: String(data.scannedAt ?? new Date().toISOString()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -2601,7 +2610,7 @@ export function ParcelSyncModule() {
|
|||||||
</span>{" "}
|
</span>{" "}
|
||||||
imobile în eTerra:{" "}
|
imobile în eTerra:{" "}
|
||||||
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
|
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
|
||||||
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
|
{noGeomScan.withGeometry.toLocaleString("ro-RO")}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
cu geometrie,{" "}
|
cu geometrie,{" "}
|
||||||
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
||||||
@@ -2609,15 +2618,25 @@ export function ParcelSyncModule() {
|
|||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span className="font-medium">fără geometrie</span>
|
<span className="font-medium">fără geometrie</span>
|
||||||
</p>
|
</p>
|
||||||
{noGeomScan.withGeometry <
|
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
|
||||||
noGeomScan.remoteGisCount && (
|
Layer GIS:{" "}
|
||||||
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
|
<span className="font-medium">
|
||||||
{noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
|
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
|
||||||
din{" "}
|
</span>
|
||||||
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "}
|
{" features (se descarcă toate)"}
|
||||||
features GIS au corespondent în lista de imobile
|
{noGeomScan.remoteGisCount !== noGeomScan.withGeometry && (
|
||||||
</p>
|
<>
|
||||||
)}
|
{" · "}
|
||||||
|
{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">
|
<p className="text-[11px] text-muted-foreground mt-0.5">
|
||||||
Cele fără geometrie există în baza de date eTerra dar
|
Cele fără geometrie există în baza de date eTerra dar
|
||||||
nu au contur desenat în layerul GIS.
|
nu au contur desenat în layerul GIS.
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ export type NoGeomScanResult = {
|
|||||||
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer */
|
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer */
|
||||||
remoteGisCount: number;
|
remoteGisCount: number;
|
||||||
noGeomCount: number;
|
noGeomCount: number;
|
||||||
|
/** Match quality: how many matched by cadastral ref vs immovable ID */
|
||||||
|
matchedByRef: number;
|
||||||
|
matchedById: number;
|
||||||
/** Quality breakdown of no-geometry items */
|
/** Quality breakdown of no-geometry items */
|
||||||
qualityBreakdown: NoGeomQuality;
|
qualityBreakdown: NoGeomQuality;
|
||||||
/** Sample of immovable identifiers without geometry */
|
/** Sample of immovable identifiers without geometry */
|
||||||
@@ -142,6 +145,8 @@ export type NoGeomScanResult = {
|
|||||||
localDbEnrichedComplete: number;
|
localDbEnrichedComplete: number;
|
||||||
/** Whether local sync is fresh (< 7 days) */
|
/** Whether local sync is fresh (< 7 days) */
|
||||||
localSyncFresh: boolean;
|
localSyncFresh: boolean;
|
||||||
|
/** Timestamp of the scan (for audit trail) */
|
||||||
|
scannedAt: string;
|
||||||
/** Error message if workspace couldn't be resolved */
|
/** Error message if workspace couldn't be resolved */
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
@@ -181,6 +186,8 @@ export async function scanNoGeometryParcels(
|
|||||||
withGeometry: 0,
|
withGeometry: 0,
|
||||||
remoteGisCount: 0,
|
remoteGisCount: 0,
|
||||||
noGeomCount: 0,
|
noGeomCount: 0,
|
||||||
|
matchedByRef: 0,
|
||||||
|
matchedById: 0,
|
||||||
qualityBreakdown: {
|
qualityBreakdown: {
|
||||||
withCadRef: 0,
|
withCadRef: 0,
|
||||||
withPaperCad: 0,
|
withPaperCad: 0,
|
||||||
@@ -198,6 +205,7 @@ export async function scanNoGeometryParcels(
|
|||||||
localDbEnriched: 0,
|
localDbEnriched: 0,
|
||||||
localDbEnrichedComplete: 0,
|
localDbEnrichedComplete: 0,
|
||||||
localSyncFresh: false,
|
localSyncFresh: false,
|
||||||
|
scannedAt: new Date().toISOString(),
|
||||||
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
|
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -248,16 +256,25 @@ export async function scanNoGeometryParcels(
|
|||||||
legalArea?: number;
|
legalArea?: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
|
let matchedByRef = 0;
|
||||||
|
let matchedById = 0;
|
||||||
|
|
||||||
for (const item of allImmovables) {
|
for (const item of allImmovables) {
|
||||||
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
|
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
|
||||||
const immPk = Number(item.immovablePk ?? 0);
|
const immPk = Number(item.immovablePk ?? 0);
|
||||||
const immId = normalizeId(item.immovablePk);
|
const immId = normalizeId(item.immovablePk);
|
||||||
|
|
||||||
// Present in remote GIS layer by cadastral ref? → has geometry
|
// 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
|
// 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({
|
noGeomItems.push({
|
||||||
immovablePk: immPk,
|
immovablePk: immPk,
|
||||||
@@ -334,6 +351,12 @@ export async function scanNoGeometryParcels(
|
|||||||
// withGeometry = immovables that MATCHED a GIS feature (always adds up)
|
// withGeometry = immovables that MATCHED a GIS feature (always adds up)
|
||||||
const matchedCount = allImmovables.length - noGeomItems.length;
|
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
|
// Quality analysis of no-geom items
|
||||||
let qWithCadRef = 0;
|
let qWithCadRef = 0;
|
||||||
let qWithPaperCad = 0;
|
let qWithPaperCad = 0;
|
||||||
@@ -370,6 +393,8 @@ export async function scanNoGeometryParcels(
|
|||||||
withGeometry: matchedCount,
|
withGeometry: matchedCount,
|
||||||
remoteGisCount: remoteFeatures.length,
|
remoteGisCount: remoteFeatures.length,
|
||||||
noGeomCount: noGeomItems.length,
|
noGeomCount: noGeomItems.length,
|
||||||
|
matchedByRef,
|
||||||
|
matchedById,
|
||||||
qualityBreakdown: {
|
qualityBreakdown: {
|
||||||
withCadRef: qWithCadRef,
|
withCadRef: qWithCadRef,
|
||||||
withPaperCad: qWithPaperCad,
|
withPaperCad: qWithPaperCad,
|
||||||
@@ -387,6 +412,7 @@ export async function scanNoGeometryParcels(
|
|||||||
localDbEnriched: localEnriched,
|
localDbEnriched: localEnriched,
|
||||||
localDbEnrichedComplete: localEnrichedComplete,
|
localDbEnrichedComplete: localEnrichedComplete,
|
||||||
localSyncFresh: syncFresh,
|
localSyncFresh: syncFresh,
|
||||||
|
scannedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user