fix: show remoteGisCount (505) as cu geometrie, add no-geom cleanup step

- 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
This commit is contained in:
AI Assistant
2026-03-07 21:00:43 +02:00
parent f9594fff71
commit 1e6888a32a
5 changed files with 124 additions and 21 deletions
BIN
View File
Binary file not shown.
+6 -2
View File
@@ -282,10 +282,14 @@ export async function POST(req: Request) {
pushProgress(); pushProgress();
} else { } else {
noGeomImported = noGeomResult.imported; noGeomImported = noGeomResult.imported;
const cleanedNote =
noGeomResult.cleaned > 0
? `, ${noGeomResult.cleaned} vechi șterse`
: "";
note = note =
noGeomImported > 0 noGeomImported > 0
? `${noGeomImported} parcele noi fără geometrie importate` ? `${noGeomImported} parcele noi fără geometrie importate${cleanedNote}`
: "Nicio parcelă nouă fără geometrie"; : `Nicio parcelă nouă fără geometrie${cleanedNote}`;
pushProgress(); pushProgress();
} }
updatePhaseProgress(1, 1); updatePhaseProgress(1, 1);
@@ -2523,7 +2523,7 @@ export function ParcelSyncModule() {
{noGeomScan.localSyncFresh && {noGeomScan.localSyncFresh &&
noGeomScan.localDbWithGeom > 0 noGeomScan.localDbWithGeom > 0
? "skip (date proaspete în DB)" ? "skip (date proaspete în DB)"
: `descarcă ${noGeomScan.withGeometry.toLocaleString("ro-RO")} features`} : `descarcă ${noGeomScan.remoteGisCount.toLocaleString("ro-RO")} features`}
</span> </span>
</li> </li>
{includeNoGeom && ( {includeNoGeom && (
@@ -2601,7 +2601,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.withGeometry.toLocaleString("ro-RO")} {noGeomScan.remoteGisCount.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">
@@ -2609,19 +2609,15 @@ export function ParcelSyncModule() {
</span>{" "} </span>{" "}
<span className="font-medium">fără geometrie</span> <span className="font-medium">fără geometrie</span>
</p> </p>
{noGeomScan.remoteGisCount > 0 && {noGeomScan.withGeometry <
noGeomScan.remoteGisCount !== noGeomScan.remoteGisCount && (
noGeomScan.withGeometry && ( <p className="text-[10px] text-muted-foreground/70 mt-0.5">
<p className="text-[10px] text-muted-foreground/70 mt-0.5"> {noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
Layerul GIS are{" "} din{" "}
{noGeomScan.remoteGisCount.toLocaleString( {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "}
"ro-RO", features GIS au corespondent în lista de imobile
)}{" "} </p>
features, dar doar{" "} )}
{noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
se potrivesc cu lista de imobile
</p>
)}
<p className="text-[11px] text-muted-foreground mt-0.5"> <p className="text-[11px] text-muted-foreground mt-0.5">
Cele fără geometrie există în baza de date eTerra dar Cele fără geometrie există în baza de date eTerra dar
nu au contur desenat în layerul GIS. nu au contur desenat în layerul GIS.
@@ -149,6 +149,7 @@ export type NoGeomScanResult = {
export type NoGeomSyncResult = { export type NoGeomSyncResult = {
imported: number; imported: number;
skipped: number; skipped: number;
cleaned: number;
errors: number; errors: number;
status: "done" | "error"; status: "done" | "error";
error?: string; error?: string;
@@ -410,6 +411,7 @@ export async function syncNoGeometryParcels(
return { return {
imported: 0, imported: 0,
skipped: 0, skipped: 0,
cleaned: 0,
errors: 0, errors: 0,
status: "error", status: "error",
error: `Nu s-a putut determina workspace-ul pentru SIRUTA ${siruta}`, 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)"); options?.onProgress?.(0, 1, "Descărcare listă imobile (fără geometrie)");
const allImmovables = await fetchAllImmovables(client, siruta, wsPk); 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<number>();
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({ const existingFeatures = await prisma.gisFeature.findMany({
where: { layerId: "TERENURI_ACTIVE", siruta }, where: { layerId: "TERENURI_ACTIVE", siruta },
select: { cadastralRef: true, objectId: true }, select: { cadastralRef: true, objectId: true },
@@ -433,7 +486,7 @@ export async function syncNoGeometryParcels(
existingObjIds.add(f.objectId); 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. // Quality: must be ACTIVE (status=1) AND have identification OR area.
// Items that are inactive, or have no identification AND no area = noise. // Items that are inactive, or have no identification AND no area = noise.
let filteredOut = 0; let filteredOut = 0;
@@ -475,6 +528,7 @@ export async function syncNoGeometryParcels(
return { return {
imported: 0, imported: 0,
skipped: filteredOut, skipped: filteredOut,
cleaned: staleIds.length,
errors: 0, errors: 0,
status: "done", status: "done",
}; };
@@ -594,10 +648,23 @@ export async function syncNoGeometryParcels(
options?.onProgress?.(done, total, "Import parcele fără geometrie"); 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) { } catch (error) {
const msg = error instanceof Error ? error.message : "Unknown 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,
};
} }
} }
+36
View File
@@ -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();
})();