diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx
index 71293da..fa11cf7 100644
--- a/src/modules/parcel-sync/components/parcel-sync-module.tsx
+++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx
@@ -390,6 +390,11 @@ export function ParcelSyncModule() {
totalImmovables: number;
withGeometry: number;
noGeomCount: number;
+ localDbTotal: number;
+ localDbWithGeom: number;
+ localDbNoGeom: number;
+ localDbEnriched: number;
+ localSyncFresh: boolean;
} | null>(null);
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
@@ -686,6 +691,16 @@ export function ParcelSyncModule() {
setNoGeomScanning(true);
setNoGeomScan(null);
setNoGeomScanSiruta(s);
+ const emptyResult = {
+ totalImmovables: 0,
+ withGeometry: 0,
+ noGeomCount: 0,
+ localDbTotal: 0,
+ localDbWithGeom: 0,
+ localDbNoGeom: 0,
+ localDbEnriched: 0,
+ localSyncFresh: false,
+ };
try {
const res = await fetch("/api/eterra/no-geom-scan", {
method: "POST",
@@ -695,25 +710,24 @@ export function ParcelSyncModule() {
workspacePk: workspacePk ?? undefined,
}),
});
- const data = (await res.json()) as {
- totalImmovables?: number;
- withGeometry?: number;
- noGeomCount?: number;
- error?: string;
- };
+ const data = (await res.json()) as Record;
if (data.error) {
console.warn("[no-geom-scan]", data.error);
- setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
+ setNoGeomScan(emptyResult);
} else {
setNoGeomScan({
- totalImmovables: data.totalImmovables ?? 0,
- withGeometry: data.withGeometry ?? 0,
- noGeomCount: data.noGeomCount ?? 0,
+ totalImmovables: Number(data.totalImmovables ?? 0),
+ withGeometry: Number(data.withGeometry ?? 0),
+ noGeomCount: Number(data.noGeomCount ?? 0),
+ localDbTotal: Number(data.localDbTotal ?? 0),
+ localDbWithGeom: Number(data.localDbWithGeom ?? 0),
+ localDbNoGeom: Number(data.localDbNoGeom ?? 0),
+ localDbEnriched: Number(data.localDbEnriched ?? 0),
+ localSyncFresh: Boolean(data.localSyncFresh),
});
}
} catch {
- // Show zero result on network error
- setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
+ setNoGeomScan(emptyResult);
}
setNoGeomScanning(false);
},
@@ -2385,6 +2399,112 @@ export function ParcelSyncModule() {
);
+ // Helper: local DB status line
+ const localDbLine = scanDone && noGeomScan.localDbTotal > 0 && (
+
+
+
+ Baza de date locală:{" "}
+
+ {noGeomScan.localDbWithGeom.toLocaleString("ro-RO")}
+ {" "}
+ cu geometrie
+ {noGeomScan.localDbNoGeom > 0 && (
+ <>
+ {" + "}
+
+ {noGeomScan.localDbNoGeom.toLocaleString("ro-RO")}
+ {" "}
+ fără geometrie
+ >
+ )}
+ {noGeomScan.localDbEnriched > 0 && (
+ <>
+ {" · "}
+
+ {noGeomScan.localDbEnriched.toLocaleString("ro-RO")}
+ {" "}
+ îmbogățite
+ >
+ )}
+ {noGeomScan.localSyncFresh && (
+
+ (proaspăt)
+
+ )}
+
+
+ );
+
+ // Helper: workflow preview (what Magic will do)
+ const workflowPreview = scanDone && (
+
+
+ La apăsarea Magic, pașii vor fi:
+
+
+
+ {noGeomScan.localSyncFresh && noGeomScan.localDbWithGeom > 0
+ ? "Sync terenuri + clădiri — "
+ : "Sync terenuri + clădiri — "}
+ 0
+ ? "text-emerald-600 dark:text-emerald-400"
+ : "text-foreground",
+ )}
+ >
+ {noGeomScan.localSyncFresh &&
+ noGeomScan.localDbWithGeom > 0
+ ? "skip (date proaspete în DB)"
+ : `descarcă ${noGeomScan.withGeometry.toLocaleString("ro-RO")} features`}
+
+
+ {includeNoGeom && (
+
+ Import parcele fără geometrie —{" "}
+
+ {(() => {
+ const newNoGeom = Math.max(
+ 0,
+ noGeomScan.noGeomCount - noGeomScan.localDbNoGeom,
+ );
+ return newNoGeom > 0
+ ? `~${newNoGeom.toLocaleString("ro-RO")} noi de importat`
+ : "deja importate";
+ })()}
+
+
+ )}
+
+ Îmbogățire CF, proprietari, adrese —{" "}
+
+ {(() => {
+ const totalToEnrich =
+ noGeomScan.localDbTotal +
+ (includeNoGeom
+ ? Math.max(
+ 0,
+ noGeomScan.noGeomCount -
+ noGeomScan.localDbNoGeom,
+ )
+ : 0);
+ const remaining =
+ totalToEnrich - noGeomScan.localDbEnriched;
+ return remaining > 0
+ ? `~${remaining.toLocaleString("ro-RO")} de procesat (~${Math.ceil((remaining * 0.25) / 60)} min)`
+ : "deja îmbogățite";
+ })()}
+
+
+ Generare GPKG + CSV
+ Comprimare ZIP + descărcare
+
+
+ );
+
// No-geometry parcels found
if (hasNoGeomParcels)
return (
@@ -2419,6 +2539,7 @@ export function ParcelSyncModule() {
Cele fără geometrie există în baza de date eTerra dar
nu au contur desenat în layerul GIS.
+ {localDbLine}
)}
+ {workflowPreview}
);
@@ -2466,6 +2588,15 @@ export function ParcelSyncModule() {
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
imobile din eTerra au geometrie — nimic de importat
suplimentar.
+ {noGeomScan.localDbTotal > 0 && (
+
+ ({noGeomScan.localDbTotal.toLocaleString("ro-RO")}{" "}
+ în DB local
+ {noGeomScan.localDbEnriched > 0 &&
+ `, ${noGeomScan.localDbEnriched.toLocaleString("ro-RO")} îmbogățite`}
+ {noGeomScan.localSyncFresh && ", proaspăt"})
+
+ )}
>
) : (
<>
diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts
index c90aa12..699a7ff 100644
--- a/src/modules/parcel-sync/services/no-geom-sync.ts
+++ b/src/modules/parcel-sync/services/no-geom-sync.ts
@@ -103,6 +103,16 @@ export type NoGeomScanResult = {
paperCadNo?: string;
paperCfNo?: string;
}>;
+ /** Total features already in local DB (geometry + no-geom) */
+ localDbTotal: number;
+ /** Geometry features already synced in local DB */
+ localDbWithGeom: number;
+ /** No-geometry features already imported in local DB */
+ localDbNoGeom: number;
+ /** How many are already enriched (magic) in local DB */
+ localDbEnriched: number;
+ /** Whether local sync is fresh (< 7 days) */
+ localSyncFresh: boolean;
/** Error message if workspace couldn't be resolved */
error?: string;
};
@@ -141,6 +151,11 @@ export async function scanNoGeometryParcels(
withGeometry: 0,
noGeomCount: 0,
samples: [],
+ localDbTotal: 0,
+ localDbWithGeom: 0,
+ localDbNoGeom: 0,
+ localDbEnriched: 0,
+ localSyncFresh: false,
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
};
}
@@ -206,11 +221,48 @@ export async function scanNoGeometryParcels(
});
}
+ // 4. Query local DB for context (what's already synced/imported)
+ const [localTotal, localNoGeom, localEnriched, lastSyncRun] =
+ await Promise.all([
+ prisma.gisFeature.count({
+ where: { layerId: "TERENURI_ACTIVE", siruta },
+ }),
+ prisma.gisFeature.count({
+ where: {
+ layerId: "TERENURI_ACTIVE",
+ siruta,
+ geometrySource: "NO_GEOMETRY",
+ },
+ }),
+ prisma.gisFeature.count({
+ where: {
+ layerId: "TERENURI_ACTIVE",
+ siruta,
+ enrichedAt: { not: null },
+ },
+ }),
+ prisma.gisSyncRun.findFirst({
+ where: { siruta, layerId: "TERENURI_ACTIVE", status: "done" },
+ orderBy: { completedAt: "desc" },
+ select: { completedAt: true },
+ }),
+ ]);
+
+ const localWithGeom = localTotal - localNoGeom;
+ const syncFresh = lastSyncRun?.completedAt
+ ? Date.now() - lastSyncRun.completedAt.getTime() < 168 * 60 * 60 * 1000
+ : false;
+
return {
totalImmovables: allImmovables.length,
withGeometry: remoteFeatures.length,
noGeomCount: noGeomItems.length,
samples: noGeomItems.slice(0, 20),
+ localDbTotal: localTotal,
+ localDbWithGeom: localWithGeom,
+ localDbNoGeom: localNoGeom,
+ localDbEnriched: localEnriched,
+ localSyncFresh: syncFresh,
};
}