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(); +})();