fix(parcel-sync): scan uses remote GIS layer instead of empty local DB

- scanNoGeometryParcels now fetches TERENURI_ACTIVE features from remote
  ArcGIS (lightweight, no geometry) to cross-reference with eTerra immovable list
- Cross-references by both NATIONAL_CADASTRAL_REFERENCE and IMMOVABLE_ID
- Works correctly regardless of whether user has synced to local DB
- Renamed totalInDb -> withGeometry in NoGeomScanResult, UI, and API
- Extended fetchAllLayer() to forward outFields/returnGeometry options
This commit is contained in:
AI Assistant
2026-03-07 17:32:49 +02:00
parent 40b9522e12
commit b01ea9fc37
4 changed files with 49 additions and 28 deletions
+3 -3
View File
@@ -2,11 +2,11 @@
* POST /api/eterra/no-geom-scan * POST /api/eterra/no-geom-scan
* *
* Scans eTerra immovable list for a UAT and counts how many parcels * Scans eTerra immovable list for a UAT and counts how many parcels
* exist in the eTerra database but have no geometry in the GIS layer * exist in the eTerra database but have no geometry in the remote
* (i.e., they are NOT in the local TERENURI_ACTIVE DB). * ArcGIS GIS layer (TERENURI_ACTIVE). Cross-references remotely.
* *
* Body: { siruta: string } * Body: { siruta: string }
* Returns: { totalImmovables, totalInDb, noGeomCount, samples } * Returns: { totalImmovables, withGeometry, noGeomCount, samples }
* *
* Requires active eTerra session. * Requires active eTerra session.
*/ */
@@ -388,7 +388,7 @@ export function ParcelSyncModule() {
const [noGeomScanning, setNoGeomScanning] = useState(false); const [noGeomScanning, setNoGeomScanning] = useState(false);
const [noGeomScan, setNoGeomScan] = useState<{ const [noGeomScan, setNoGeomScan] = useState<{
totalImmovables: number; totalImmovables: number;
totalInDb: number; withGeometry: number;
noGeomCount: number; noGeomCount: number;
} | null>(null); } | null>(null);
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
@@ -697,23 +697,23 @@ export function ParcelSyncModule() {
}); });
const data = (await res.json()) as { const data = (await res.json()) as {
totalImmovables?: number; totalImmovables?: number;
totalInDb?: number; withGeometry?: number;
noGeomCount?: number; noGeomCount?: number;
error?: string; error?: string;
}; };
if (data.error) { if (data.error) {
console.warn("[no-geom-scan]", data.error); console.warn("[no-geom-scan]", data.error);
setNoGeomScan({ totalImmovables: 0, totalInDb: 0, noGeomCount: 0 }); setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
} else { } else {
setNoGeomScan({ setNoGeomScan({
totalImmovables: data.totalImmovables ?? 0, totalImmovables: data.totalImmovables ?? 0,
totalInDb: data.totalInDb ?? 0, withGeometry: data.withGeometry ?? 0,
noGeomCount: data.noGeomCount ?? 0, noGeomCount: data.noGeomCount ?? 0,
}); });
} }
} catch { } catch {
// Show zero result on network error // Show zero result on network error
setNoGeomScan({ totalImmovables: 0, totalInDb: 0, noGeomCount: 0 }); setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
} }
setNoGeomScanning(false); setNoGeomScanning(false);
}, },
@@ -2407,7 +2407,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.totalInDb.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">
@@ -309,6 +309,8 @@ export class EterraClient {
total?: number; total?: number;
onProgress?: ProgressCallback; onProgress?: ProgressCallback;
delayMs?: number; delayMs?: number;
outFields?: string;
returnGeometry?: boolean;
}, },
) { ) {
const where = await this.buildWhere(layer, siruta); const where = await this.buildWhere(layer, siruta);
@@ -93,7 +93,8 @@ const normalizeCadRef = (value: unknown) =>
export type NoGeomScanResult = { export type NoGeomScanResult = {
totalImmovables: number; totalImmovables: number;
totalInDb: number; /** Features present in the remote ArcGIS TERENURI_ACTIVE layer (have geometry) */
withGeometry: number;
noGeomCount: number; noGeomCount: number;
/** Sample of immovable identifiers without geometry */ /** Sample of immovable identifiers without geometry */
samples: Array<{ samples: Array<{
@@ -116,7 +117,11 @@ export type NoGeomSyncResult = {
/** /**
* Scan: count how many eTerra immovables for this UAT have no geometry * Scan: count how many eTerra immovables for this UAT have no geometry
* in the local DB. * in the remote GIS layer (TERENURI_ACTIVE).
*
* Cross-references the eTerra immovable list against the REMOTE ArcGIS
* layer (lightweight fetch, no geometry download). This works correctly
* regardless of whether the user has synced to local DB yet.
* *
* This does NOT write anything — it's a read-only operation. * This does NOT write anything — it's a read-only operation.
*/ */
@@ -133,14 +138,14 @@ export async function scanNoGeometryParcels(
if (!wsPk) { if (!wsPk) {
return { return {
totalImmovables: 0, totalImmovables: 0,
totalInDb: 0, withGeometry: 0,
noGeomCount: 0, noGeomCount: 0,
samples: [], samples: [],
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}`,
}; };
} }
// 1. Fetch all immovables from eTerra // 1. Fetch all immovables from eTerra immovable list API
const allImmovables = await fetchAllImmovables( const allImmovables = await fetchAllImmovables(
client, client,
siruta, siruta,
@@ -148,20 +153,33 @@ export async function scanNoGeometryParcels(
options?.onProgress, options?.onProgress,
); );
// 2. Get all existing cadastralRefs in DB for TERENURI_ACTIVE // 2. Fetch remote GIS cadastral refs (lightweight — no geometry)
const existingFeatures = await prisma.gisFeature.findMany({ // This is the source of truth for "has geometry" regardless of local DB state.
where: { layerId: "TERENURI_ACTIVE", siruta }, // ~4 pages for 8k features with outFields only = very fast.
select: { cadastralRef: true, objectId: true }, const terenuriLayer = {
id: "TERENURI_ACTIVE",
name: "TERENURI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
};
const remoteFeatures = await client.fetchAllLayer(terenuriLayer, siruta, {
returnGeometry: false,
outFields: "OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID",
pageSize: 2000,
}); });
const existingCadRefs = new Set<string>(); const remoteCadRefs = new Set<string>();
const existingObjIds = new Set<number>(); const remoteImmIds = new Set<string>();
for (const f of existingFeatures) { for (const f of remoteFeatures) {
if (f.cadastralRef) existingCadRefs.add(normalizeCadRef(f.cadastralRef)); const cadRef = normalizeCadRef(
existingObjIds.add(f.objectId); f.attributes?.NATIONAL_CADASTRAL_REFERENCE ?? "",
);
if (cadRef) remoteCadRefs.add(cadRef);
const immId = normalizeId(f.attributes?.IMMOVABLE_ID);
if (immId) remoteImmIds.add(immId);
} }
// 3. Find immovables not in DB // 3. Cross-reference: immovables NOT in remote GIS = no geometry
const noGeomItems: Array<{ const noGeomItems: Array<{
immovablePk: number; immovablePk: number;
identifierDetails: string; identifierDetails: string;
@@ -172,12 +190,13 @@ export async function scanNoGeometryParcels(
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);
// Already in DB by cadastral ref? // Present in remote GIS layer by cadastral ref? → has geometry
if (cadRef && existingCadRefs.has(cadRef)) continue; if (cadRef && remoteCadRefs.has(cadRef)) continue;
// Already in DB by negative objectId? // Present in remote GIS layer by IMMOVABLE_ID? → has geometry
if (immPk > 0 && existingObjIds.has(-immPk)) continue; if (immId && remoteImmIds.has(immId)) continue;
noGeomItems.push({ noGeomItems.push({
immovablePk: immPk, immovablePk: immPk,
@@ -189,7 +208,7 @@ export async function scanNoGeometryParcels(
return { return {
totalImmovables: allImmovables.length, totalImmovables: allImmovables.length,
totalInDb: existingFeatures.length, withGeometry: remoteFeatures.length,
noGeomCount: noGeomItems.length, noGeomCount: noGeomItems.length,
samples: noGeomItems.slice(0, 20), samples: noGeomItems.slice(0, 20),
}; };