diff --git a/.gitignore b/.gitignore
index 3b7dd80..57858bc 100644
Binary files a/.gitignore and b/.gitignore differ
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();
+})();