From 1e6888a32af31c02213c4dbe47bd3ae59fbc5eae Mon Sep 17 00:00:00 2001
From: AI Assistant
Date: Sat, 7 Mar 2026 21:00:43 +0200
Subject: [PATCH] fix: show remoteGisCount (505) as cu geometrie, add no-geom
cleanup step
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- UI: scan card now shows remoteGisCount instead of matchedCount (withGeometry)
as the primary 'cu geometrie' number — this is the true GIS layer feature count
- UI: workflow preview step 1 shows remoteGisCount for download count
- UI: mismatch note reworded as secondary detail about cross-reference matching
- Import: automatic cleanup step at start of syncNoGeometryParcels
- Builds valid immovablePk set from fresh list (active + identification/area)
- Deletes stale NO_GEOMETRY records not in the valid set
- Reports cleaned count in result + progress note
- NoGeomSyncResult type: added 'cleaned' field
- Gitignore: temp-db-check.cjs
---
.gitignore | Bin 508 -> 591 bytes
src/app/api/eterra/export-bundle/route.ts | 8 +-
.../components/parcel-sync-module.tsx | 26 +++---
.../parcel-sync/services/no-geom-sync.ts | 75 +++++++++++++++++-
temp-db-check.cjs | 36 +++++++++
5 files changed, 124 insertions(+), 21 deletions(-)
create mode 100644 temp-db-check.cjs
diff --git a/.gitignore b/.gitignore
index 3b7dd805add4df6aeb87026a6361f6f3f4281886..57858bc0a89b06e58a26dd5665c5384d375ffa2a 100644
GIT binary patch
literal 591
zcmZWlNp8b14AfbG|DYnb0G94O_Xl}EAXAL3R2w8ZXntSHj?osqki+3NPV|Ctp_>ClX6%vIZWWO
z4br6S@EWsQc>ssHq%z2E^_l(C)?^sG?WkCSrPm#CSZAMp;)(%n`&ID3+I;R6eKlbt
zR?BN3bH>iN^9jF5-?#N08ZKb@?0QqFEDngLFg>k4xSfx_0W18tTrb2Y5Av)FMrI(`
z)ggC7PShvISBJxC<>jfwaf3SKnQ!Yi{wJ{3TwfLa9e;beFiemAvAmPRKKuxAdiedJlNLG^{Yernr&KHW0QWa%T%)sFc=O4BNs8594F(3w(wi&m0A5k(QB
VaAIN$Vqpfvr_gNgqKQFB@eNU@vRD8B
literal 508
zcmZWlOKt-p4Bh(_RN0Lbyzja<$N?e-9EOUYz(mdMYi5#GRX6ZHHumGBE<|c$uKgWx
zqgb^Myp2`clF&2`Hd4pnb7>*_^DRuOZl$SI8Y#1u!RlqJ-PQmn<)~72lx?66n2V!p
zDsD(Opsu;XJoEzkVOquo-VRU=+f8a-G!7r~4{a35^k4fVXskQ(B
diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts
index 0c4d5fb..0fa9180 100644
--- a/src/app/api/eterra/export-bundle/route.ts
+++ b/src/app/api/eterra/export-bundle/route.ts
@@ -282,10 +282,14 @@ export async function POST(req: Request) {
pushProgress();
} else {
noGeomImported = noGeomResult.imported;
+ const cleanedNote =
+ noGeomResult.cleaned > 0
+ ? `, ${noGeomResult.cleaned} vechi șterse`
+ : "";
note =
noGeomImported > 0
- ? `${noGeomImported} parcele noi fără geometrie importate`
- : "Nicio parcelă nouă fără geometrie";
+ ? `${noGeomImported} parcele noi fără geometrie importate${cleanedNote}`
+ : `Nicio parcelă nouă fără geometrie${cleanedNote}`;
pushProgress();
}
updatePhaseProgress(1, 1);
diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx
index f370060..9bf8991 100644
--- a/src/modules/parcel-sync/components/parcel-sync-module.tsx
+++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx
@@ -2523,7 +2523,7 @@ export function ParcelSyncModule() {
{noGeomScan.localSyncFresh &&
noGeomScan.localDbWithGeom > 0
? "skip (date proaspete în DB)"
- : `descarcă ${noGeomScan.withGeometry.toLocaleString("ro-RO")} features`}
+ : `descarcă ${noGeomScan.remoteGisCount.toLocaleString("ro-RO")} features`}
{includeNoGeom && (
@@ -2601,7 +2601,7 @@ export function ParcelSyncModule() {
{" "}
imobile în eTerra:{" "}
- {noGeomScan.withGeometry.toLocaleString("ro-RO")}
+ {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
{" "}
cu geometrie,{" "}
@@ -2609,19 +2609,15 @@ export function ParcelSyncModule() {
{" "}
fără geometrie
- {noGeomScan.remoteGisCount > 0 &&
- noGeomScan.remoteGisCount !==
- noGeomScan.withGeometry && (
-
- Layerul GIS are{" "}
- {noGeomScan.remoteGisCount.toLocaleString(
- "ro-RO",
- )}{" "}
- features, dar doar{" "}
- {noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
- se potrivesc cu lista de imobile
-
- )}
+ {noGeomScan.withGeometry <
+ noGeomScan.remoteGisCount && (
+
+ {noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
+ din{" "}
+ {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "}
+ features GIS au corespondent în lista de imobile
+
+ )}
Cele fără geometrie există în baza de date eTerra dar
nu au contur desenat în layerul GIS.
diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts
index 74bef8b..224bc1d 100644
--- a/src/modules/parcel-sync/services/no-geom-sync.ts
+++ b/src/modules/parcel-sync/services/no-geom-sync.ts
@@ -149,6 +149,7 @@ export type NoGeomScanResult = {
export type NoGeomSyncResult = {
imported: number;
skipped: number;
+ cleaned: number;
errors: number;
status: "done" | "error";
error?: string;
@@ -410,6 +411,7 @@ export async function syncNoGeometryParcels(
return {
imported: 0,
skipped: 0,
+ cleaned: 0,
errors: 0,
status: "error",
error: `Nu s-a putut determina workspace-ul pentru SIRUTA ${siruta}`,
@@ -420,7 +422,58 @@ export async function syncNoGeometryParcels(
options?.onProgress?.(0, 1, "Descărcare listă imobile (fără geometrie)");
const allImmovables = await fetchAllImmovables(client, siruta, wsPk);
- // 2. Get existing features from DB
+ // 2. Cleanup: remove stale/orphan no-geom records from DB
+ // Build a set of valid immovablePks from the fresh immovable list.
+ // Any NO_GEOMETRY record whose immovablePk is NOT in the fresh list
+ // (or whose immovable is inactive/empty) gets deleted.
+ const validImmPks = new Set();
+ for (const item of allImmovables) {
+ const pk = Number(item.immovablePk ?? 0);
+ if (pk > 0) {
+ const status = typeof item.status === "number" ? item.status : 1;
+ const cadRef = (item.identifierDetails ?? "").toString().trim();
+ const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim();
+ const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim();
+ const hasArea =
+ (typeof item.measuredArea === "number" && item.measuredArea > 0) ||
+ (typeof item.legalArea === "number" && item.legalArea > 0);
+ const hasIdentification = !!cadRef || hasPaperLb || hasPaperCad;
+
+ // Only keep items that pass the quality gate (active + identification/area)
+ if (status === 1 && (hasIdentification || hasArea)) {
+ validImmPks.add(pk);
+ }
+ }
+ }
+
+ // Find stale no-geom records: objectId < 0 means no-geom (objectId = -immPk)
+ const existingNoGeom = await prisma.gisFeature.findMany({
+ where: {
+ layerId: "TERENURI_ACTIVE",
+ siruta,
+ geometrySource: "NO_GEOMETRY",
+ },
+ select: { id: true, objectId: true },
+ });
+
+ const staleIds: string[] = [];
+ for (const f of existingNoGeom) {
+ const immPk = -f.objectId; // objectId = -immovablePk for no-geom
+ if (!validImmPks.has(immPk)) {
+ staleIds.push(f.id);
+ }
+ }
+
+ if (staleIds.length > 0) {
+ await prisma.gisFeature.deleteMany({
+ where: { id: { in: staleIds } },
+ });
+ console.log(
+ `[no-geom-sync] Cleanup: removed ${staleIds.length} stale/invalid no-geom records`,
+ );
+ }
+
+ // 3. Get existing features from DB (after cleanup)
const existingFeatures = await prisma.gisFeature.findMany({
where: { layerId: "TERENURI_ACTIVE", siruta },
select: { cadastralRef: true, objectId: true },
@@ -433,7 +486,7 @@ export async function syncNoGeometryParcels(
existingObjIds.add(f.objectId);
}
- // 3. Filter: not yet in DB + quality gate
+ // 4. Filter: not yet in DB + quality gate
// Quality: must be ACTIVE (status=1) AND have identification OR area.
// Items that are inactive, or have no identification AND no area = noise.
let filteredOut = 0;
@@ -475,6 +528,7 @@ export async function syncNoGeometryParcels(
return {
imported: 0,
skipped: filteredOut,
+ cleaned: staleIds.length,
errors: 0,
status: "done",
};
@@ -594,10 +648,23 @@ export async function syncNoGeometryParcels(
options?.onProgress?.(done, total, "Import parcele fără geometrie");
}
- return { imported, skipped: skipped + filteredOut, errors, status: "done" };
+ return {
+ imported,
+ skipped: skipped + filteredOut,
+ cleaned: staleIds.length,
+ errors,
+ status: "done",
+ };
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
- return { imported: 0, skipped: 0, errors: 0, status: "error", error: msg };
+ return {
+ imported: 0,
+ skipped: 0,
+ cleaned: 0,
+ errors: 0,
+ status: "error",
+ error: msg,
+ };
}
}
diff --git a/temp-db-check.cjs b/temp-db-check.cjs
new file mode 100644
index 0000000..a2447bf
--- /dev/null
+++ b/temp-db-check.cjs
@@ -0,0 +1,36 @@
+const { PrismaClient } = require("@prisma/client");
+const p = new PrismaClient({
+ datasourceUrl:
+ "postgresql://architools_user:stictMyFon34!_gonY@10.10.10.166:5432/architools_db?schema=public",
+});
+
+(async () => {
+ const features = await p.$queryRawUnsafe(`
+ SELECT "layerId", siruta, "geometrySource", COUNT(*)::int as nr
+ FROM "GisFeature"
+ GROUP BY "layerId", siruta, "geometrySource"
+ ORDER BY "layerId", siruta, "geometrySource"
+ `);
+ console.log("=== GisFeature ===");
+ console.table(features);
+
+ const syncs = await p.$queryRawUnsafe(`
+ SELECT "layerId", siruta, status, COUNT(*)::int as nr
+ FROM "GisSyncRun"
+ GROUP BY "layerId", siruta, status
+ ORDER BY "layerId", siruta
+ `);
+ console.log("=== GisSyncRun ===");
+ console.table(syncs);
+
+ const uats = await p.$queryRawUnsafe(`
+ SELECT siruta, name, county, "workspacePk"
+ FROM "GisUat"
+ WHERE "workspacePk" IS NOT NULL AND "workspacePk" > 0
+ LIMIT 20
+ `);
+ console.log("=== GisUat (with workspacePk) ===");
+ console.table(uats);
+
+ await p.$disconnect();
+})();