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();
} 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);
@@ -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`}
</span>
</li>
{includeNoGeom && (
@@ -2601,7 +2601,7 @@ export function ParcelSyncModule() {
</span>{" "}
imobile în eTerra:{" "}
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
{noGeomScan.withGeometry.toLocaleString("ro-RO")}
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
</span>{" "}
cu geometrie,{" "}
<span className="font-semibold text-amber-600 dark:text-amber-400">
@@ -2609,17 +2609,13 @@ export function ParcelSyncModule() {
</span>{" "}
<span className="font-medium">fără geometrie</span>
</p>
{noGeomScan.remoteGisCount > 0 &&
noGeomScan.remoteGisCount !==
noGeomScan.withGeometry && (
{noGeomScan.withGeometry <
noGeomScan.remoteGisCount && (
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
Layerul GIS are{" "}
{noGeomScan.remoteGisCount.toLocaleString(
"ro-RO",
)}{" "}
features, dar doar{" "}
{noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
se potrivesc cu lista de imobile
din{" "}
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "}
features GIS au corespondent în lista de imobile
</p>
)}
<p className="text-[11px] text-muted-foreground mt-0.5">
@@ -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<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({
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,
};
}
}
+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();
})();