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:
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user