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
*
* 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
* (i.e., they are NOT in the local TERENURI_ACTIVE DB).
* exist in the eTerra database but have no geometry in the remote
* ArcGIS GIS layer (TERENURI_ACTIVE). Cross-references remotely.
*
* Body: { siruta: string }
* Returns: { totalImmovables, totalInDb, noGeomCount, samples }
* Returns: { totalImmovables, withGeometry, noGeomCount, samples }
*
* Requires active eTerra session.
*/
@@ -388,7 +388,7 @@ export function ParcelSyncModule() {
const [noGeomScanning, setNoGeomScanning] = useState(false);
const [noGeomScan, setNoGeomScan] = useState<{
totalImmovables: number;
totalInDb: number;
withGeometry: number;
noGeomCount: number;
} | null>(null);
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
@@ -697,23 +697,23 @@ export function ParcelSyncModule() {
});
const data = (await res.json()) as {
totalImmovables?: number;
totalInDb?: number;
withGeometry?: number;
noGeomCount?: number;
error?: string;
};
if (data.error) {
console.warn("[no-geom-scan]", data.error);
setNoGeomScan({ totalImmovables: 0, totalInDb: 0, noGeomCount: 0 });
setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
} else {
setNoGeomScan({
totalImmovables: data.totalImmovables ?? 0,
totalInDb: data.totalInDb ?? 0,
withGeometry: data.withGeometry ?? 0,
noGeomCount: data.noGeomCount ?? 0,
});
}
} catch {
// Show zero result on network error
setNoGeomScan({ totalImmovables: 0, totalInDb: 0, noGeomCount: 0 });
setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
}
setNoGeomScanning(false);
},
@@ -2407,7 +2407,7 @@ export function ParcelSyncModule() {
</span>{" "}
imobile în eTerra:{" "}
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
{noGeomScan.totalInDb.toLocaleString("ro-RO")}
{noGeomScan.withGeometry.toLocaleString("ro-RO")}
</span>{" "}
cu geometrie,{" "}
<span className="font-semibold text-amber-600 dark:text-amber-400">
@@ -309,6 +309,8 @@ export class EterraClient {
total?: number;
onProgress?: ProgressCallback;
delayMs?: number;
outFields?: string;
returnGeometry?: boolean;
},
) {
const where = await this.buildWhere(layer, siruta);
@@ -93,7 +93,8 @@ const normalizeCadRef = (value: unknown) =>
export type NoGeomScanResult = {
totalImmovables: number;
totalInDb: number;
/** Features present in the remote ArcGIS TERENURI_ACTIVE layer (have geometry) */
withGeometry: number;
noGeomCount: number;
/** Sample of immovable identifiers without geometry */
samples: Array<{
@@ -116,7 +117,11 @@ export type NoGeomSyncResult = {
/**
* 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.
*/
@@ -133,14 +138,14 @@ export async function scanNoGeometryParcels(
if (!wsPk) {
return {
totalImmovables: 0,
totalInDb: 0,
withGeometry: 0,
noGeomCount: 0,
samples: [],
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(
client,
siruta,
@@ -148,20 +153,33 @@ export async function scanNoGeometryParcels(
options?.onProgress,
);
// 2. Get all existing cadastralRefs in DB for TERENURI_ACTIVE
const existingFeatures = await prisma.gisFeature.findMany({
where: { layerId: "TERENURI_ACTIVE", siruta },
select: { cadastralRef: true, objectId: true },
// 2. Fetch remote GIS cadastral refs (lightweight — no geometry)
// This is the source of truth for "has geometry" regardless of local DB state.
// ~4 pages for 8k features with outFields only = very fast.
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 existingObjIds = new Set<number>();
for (const f of existingFeatures) {
if (f.cadastralRef) existingCadRefs.add(normalizeCadRef(f.cadastralRef));
existingObjIds.add(f.objectId);
const remoteCadRefs = new Set<string>();
const remoteImmIds = new Set<string>();
for (const f of remoteFeatures) {
const cadRef = normalizeCadRef(
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<{
immovablePk: number;
identifierDetails: string;
@@ -172,12 +190,13 @@ export async function scanNoGeometryParcels(
for (const item of allImmovables) {
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
const immPk = Number(item.immovablePk ?? 0);
const immId = normalizeId(item.immovablePk);
// Already in DB by cadastral ref?
if (cadRef && existingCadRefs.has(cadRef)) continue;
// Present in remote GIS layer by cadastral ref? → has geometry
if (cadRef && remoteCadRefs.has(cadRef)) continue;
// Already in DB by negative objectId?
if (immPk > 0 && existingObjIds.has(-immPk)) continue;
// Present in remote GIS layer by IMMOVABLE_ID? → has geometry
if (immId && remoteImmIds.has(immId)) continue;
noGeomItems.push({
immovablePk: immPk,
@@ -189,7 +208,7 @@ export async function scanNoGeometryParcels(
return {
totalImmovables: allImmovables.length,
totalInDb: existingFeatures.length,
withGeometry: remoteFeatures.length,
noGeomCount: noGeomItems.length,
samples: noGeomItems.slice(0, 20),
};