fix: stable scan display, accurate workflow preview, cladiri count

ROOT CAUSE: The cross-reference between immovable list and GIS layer
produces wildly different matchedCount on each scan (320, 430, 629, 433)
because the eTerra immovable/list API with inscrisCF=-1 returns
inconsistent results across calls. The GIS layer count (505) is stable.

SCAN DISPLAY — now uses only stable numbers:
- Header shows 'Layer GIS: 505 terenuri + X cladiri' (stable ArcGIS count)
- Shows 'Lista imobile: 2.717 (estimat ~2.212 fara geometrie)' using
  simple subtraction totalImmovables - remoteGisCount
- Cross-ref matchedCount kept internally for import logic, but NOT shown
  as the primary number — eliminates visual instability
- hasNoGeomParcels now uses estimated count (stable)

WORKFLOW PREVIEW — now accurate:
- Step 1: 'Sync GIS — descarca 505 terenuri + X cladiri' (separate counts)
  or 'skip (date proaspete in DB)' when fresh
- Step 2 (enrichment): Fixed 'deja imbogatite' bug when DB is empty.
  Now correctly computes what WILL be in DB after sync completes:
  geoAfterSync + noGeomAfterImport - localDbEnrichedComplete
- Steps 3-4 unchanged

CLADIRI COUNT:
- Scan now also fetches CLADIRI_ACTIVE layer count (lightweight, OBJECTID only)
- New field remoteCladiriCount in NoGeomScanResult
- Displayed in header and workflow step 1
- Non-fatal: if CLADIRI fetch fails, just shows 0
This commit is contained in:
AI Assistant
2026-03-07 21:40:38 +02:00
parent 531c3b0858
commit b287b4c34b
3 changed files with 88 additions and 50 deletions
+6 -2
View File
@@ -696,8 +696,12 @@ export async function POST(req: Request) {
...(hasNoGeom
? [
` 3. Import fără geometrie: ${fmt(noGeomImported)} noi importate` +
(noGeomCleaned > 0 ? `, ${fmt(noGeomCleaned)} vechi șterse` : "") +
(noGeomSkipped > 0 ? `, ${fmt(noGeomSkipped)} filtrate/skip` : ""),
(noGeomCleaned > 0
? `, ${fmt(noGeomCleaned)} vechi șterse`
: "") +
(noGeomSkipped > 0
? `, ${fmt(noGeomSkipped)} filtrate/skip`
: ""),
]
: [` 3. Import fără geometrie: dezactivat`]),
` 4. Îmbogățire (CF, prop.): da`,
@@ -390,6 +390,7 @@ export function ParcelSyncModule() {
totalImmovables: number;
withGeometry: number;
remoteGisCount: number;
remoteCladiriCount: number;
noGeomCount: number;
matchedByRef: number;
matchedById: number;
@@ -720,6 +721,7 @@ export function ParcelSyncModule() {
totalImmovables: 0,
withGeometry: 0,
remoteGisCount: 0,
remoteCladiriCount: 0,
noGeomCount: 0,
matchedByRef: 0,
matchedById: 0,
@@ -751,6 +753,7 @@ export function ParcelSyncModule() {
totalImmovables: Number(data.totalImmovables ?? 0),
withGeometry: Number(data.withGeometry ?? 0),
remoteGisCount: Number(data.remoteGisCount ?? 0),
remoteCladiriCount: Number(data.remoteCladiriCount ?? 0),
noGeomCount: Number(data.noGeomCount ?? 0),
matchedByRef: Number(data.matchedByRef ?? 0),
matchedById: Number(data.matchedById ?? 0),
@@ -2430,7 +2433,13 @@ export function ParcelSyncModule() {
session.connected &&
(() => {
const scanDone = noGeomScan !== null && noGeomScanSiruta === siruta;
const hasNoGeomParcels = scanDone && noGeomScan.noGeomCount > 0;
const estimatedNoGeom = scanDone
? Math.max(
0,
noGeomScan.totalImmovables - noGeomScan.remoteGisCount,
)
: 0;
const hasNoGeomParcels = scanDone && estimatedNoGeom > 0;
const scanning = noGeomScanning;
// Still scanning
@@ -2517,9 +2526,7 @@ export function ParcelSyncModule() {
</p>
<ol className="text-[11px] text-muted-foreground list-decimal ml-4 space-y-px">
<li>
{noGeomScan.localSyncFresh && noGeomScan.localDbWithGeom > 0
? "Sync terenuri + clădiri — "
: "Sync terenuri + clădiri — "}
{"Sync GIS — "}
<span
className={cn(
"font-medium",
@@ -2532,7 +2539,10 @@ export function ParcelSyncModule() {
{noGeomScan.localSyncFresh &&
noGeomScan.localDbWithGeom > 0
? "skip (date proaspete în DB)"
: `descarcă ${noGeomScan.remoteGisCount.toLocaleString("ro-RO")} features`}
: `descarcă ${noGeomScan.remoteGisCount.toLocaleString("ro-RO")} terenuri` +
(noGeomScan.remoteCladiriCount > 0
? ` + ${noGeomScan.remoteCladiriCount.toLocaleString("ro-RO")} clădiri`
: "")}
</span>
</li>
{includeNoGeom && (
@@ -2540,7 +2550,6 @@ export function ParcelSyncModule() {
Import parcele fără geometrie {" "}
<span className="font-medium text-amber-600 dark:text-amber-400">
{(() => {
// Only useful items will be imported (quality filter)
const usefulNoGeom =
noGeomScan.qualityBreakdown.useful;
const newNoGeom = Math.max(
@@ -2562,20 +2571,23 @@ export function ParcelSyncModule() {
Îmbogățire CF, proprietari, adrese {" "}
<span className="font-medium text-teal-600 dark:text-teal-400">
{(() => {
const usefulNoGeom = noGeomScan.qualityBreakdown.useful;
const totalToEnrich =
noGeomScan.localDbTotal +
(includeNoGeom
// What will be in DB after sync + optional no-geom import:
// If DB is empty: sync will add remoteGisCount geo features
// If DB is fresh: keep localDbTotal
const geoAfterSync =
noGeomScan.localSyncFresh &&
noGeomScan.localDbWithGeom > 0
? noGeomScan.localDbWithGeom
: noGeomScan.remoteGisCount;
const noGeomAfterImport = includeNoGeom
? Math.max(
0,
usefulNoGeom - noGeomScan.localDbNoGeom,
noGeomScan.localDbNoGeom,
noGeomScan.qualityBreakdown.useful,
)
: 0);
// Use enrichedComplete (not enriched) — stale
// enrichment (missing PROPRIETARI_VECHI etc.)
// will be re-processed
: noGeomScan.localDbNoGeom;
const totalAfter = geoAfterSync + noGeomAfterImport;
const remaining =
totalToEnrich - noGeomScan.localDbEnrichedComplete;
totalAfter - noGeomScan.localDbEnrichedComplete;
return remaining > 0
? `~${remaining.toLocaleString("ro-RO")} de procesat (~${Math.ceil((remaining * 0.25) / 60)} min)`
: "deja îmbogățite";
@@ -2604,38 +2616,37 @@ export function ParcelSyncModule() {
<AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm">
Din{" "}
<span className="font-semibold">
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}
</span>{" "}
imobile în eTerra:{" "}
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
{noGeomScan.withGeometry.toLocaleString("ro-RO")}
</span>{" "}
cu geometrie,{" "}
<span className="font-semibold text-amber-600 dark:text-amber-400">
{noGeomScan.noGeomCount.toLocaleString("ro-RO")}
</span>{" "}
<span className="font-medium">fără geometrie</span>
</p>
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
Layer GIS:{" "}
<span className="font-medium">
<span className="text-emerald-600 dark:text-emerald-400 font-semibold">
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
</span>
{" features (se descarcă toate)"}
{noGeomScan.remoteGisCount !== noGeomScan.withGeometry && (
</span>{" "}
terenuri
{noGeomScan.remoteCladiriCount > 0 && (
<>
{" · "}
{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>
{" + "}
<span className="font-semibold">
{noGeomScan.remoteCladiriCount.toLocaleString(
"ro-RO",
)}
</span>{" "}
clădiri
</>
)}
{" · "}
Lista imobile:{" "}
<span className="font-semibold">
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}
</span>
{" (estimat "}
<span className="font-semibold text-amber-600 dark:text-amber-400">
~
{Math.max(
0,
noGeomScan.totalImmovables -
noGeomScan.remoteGisCount,
).toLocaleString("ro-RO")}
</span>
{" fără geometrie)"}
</p>
<p className="text-[11px] text-muted-foreground mt-0.5">
Cele fără geometrie există în baza de date eTerra dar
@@ -113,10 +113,12 @@ export type NoGeomQuality = {
export type NoGeomScanResult = {
totalImmovables: number;
/** Immovables that matched a remote GIS feature (have geometry) */
/** Immovables that matched a remote GIS feature (cross-ref, may vary) */
withGeometry: number;
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer */
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer (stable) */
remoteGisCount: number;
/** Total features in the remote ArcGIS CLADIRI_ACTIVE layer (stable) */
remoteCladiriCount: number;
noGeomCount: number;
/** Match quality: how many matched by cadastral ref vs immovable ID */
matchedByRef: number;
@@ -185,6 +187,7 @@ export async function scanNoGeometryParcels(
totalImmovables: 0,
withGeometry: 0,
remoteGisCount: 0,
remoteCladiriCount: 0,
noGeomCount: 0,
matchedByRef: 0,
matchedById: 0,
@@ -233,6 +236,25 @@ export async function scanNoGeometryParcels(
pageSize: 2000,
});
// 2b. Also fetch CLADIRI_ACTIVE count (lightweight, just OBJECTID)
const cladiriLayer = {
id: "CLADIRI_ACTIVE",
name: "CLADIRI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
};
let remoteCladiriCount = 0;
try {
const cladiriFeatures = await client.fetchAllLayer(cladiriLayer, siruta, {
returnGeometry: false,
outFields: "OBJECTID",
pageSize: 2000,
});
remoteCladiriCount = cladiriFeatures.length;
} catch {
// Non-fatal — just won't show clădiri count
}
const remoteCadRefs = new Set<string>();
const remoteImmIds = new Set<string>();
for (const f of remoteFeatures) {
@@ -392,6 +414,7 @@ export async function scanNoGeometryParcels(
totalImmovables: allImmovables.length,
withGeometry: matchedCount,
remoteGisCount: remoteFeatures.length,
remoteCladiriCount,
noGeomCount: noGeomItems.length,
matchedByRef,
matchedById,