feat(parcel-sync): scan shows local DB context + Magic workflow preview
- NoGeomScanResult now includes: localDbTotal, localDbWithGeom, localDbNoGeom, localDbEnriched, localSyncFresh (parallel DB queries, fast) - Scan card shows 'Baza de date locala: X cu geometrie + Y fara + Z imbogatite' - Workflow preview shows numbered steps with smart estimates: step 1 shows 'skip (date proaspete)' when sync is fresh step 2 shows '~N noi de importat' or 'deja importate' for no-geom step 3 shows '~N de procesat (~M min)' or 'deja imbogatite' for enrichment - All-geometry card also shows local DB summary - User can see exactly what will happen before pressing Magic
This commit is contained in:
@@ -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<string, unknown>;
|
||||
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() {
|
||||
</Card>
|
||||
);
|
||||
|
||||
// Helper: local DB status line
|
||||
const localDbLine = scanDone && noGeomScan.localDbTotal > 0 && (
|
||||
<div className="flex items-center gap-1.5 flex-wrap text-[11px] text-muted-foreground mt-1">
|
||||
<Database className="h-3 w-3 shrink-0" />
|
||||
<span>
|
||||
Baza de date locală:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{noGeomScan.localDbWithGeom.toLocaleString("ro-RO")}
|
||||
</span>{" "}
|
||||
cu geometrie
|
||||
{noGeomScan.localDbNoGeom > 0 && (
|
||||
<>
|
||||
{" + "}
|
||||
<span className="font-medium text-amber-600 dark:text-amber-400">
|
||||
{noGeomScan.localDbNoGeom.toLocaleString("ro-RO")}
|
||||
</span>{" "}
|
||||
fără geometrie
|
||||
</>
|
||||
)}
|
||||
{noGeomScan.localDbEnriched > 0 && (
|
||||
<>
|
||||
{" · "}
|
||||
<span className="font-medium text-teal-600 dark:text-teal-400">
|
||||
{noGeomScan.localDbEnriched.toLocaleString("ro-RO")}
|
||||
</span>{" "}
|
||||
îmbogățite
|
||||
</>
|
||||
)}
|
||||
{noGeomScan.localSyncFresh && (
|
||||
<span className="text-emerald-600 dark:text-emerald-400 ml-1">
|
||||
(proaspăt)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Helper: workflow preview (what Magic will do)
|
||||
const workflowPreview = scanDone && (
|
||||
<div className="mt-2 ml-7 space-y-0.5">
|
||||
<p className="text-[11px] font-medium text-muted-foreground">
|
||||
La apăsarea Magic, pașii vor fi:
|
||||
</p>
|
||||
<ol className="text-[11px] text-muted-foreground list-decimal ml-4 space-y-px">
|
||||
<li>
|
||||
{noGeomScan.localSyncFresh && noGeomScan.localDbWithGeom > 0
|
||||
? "Sync terenuri + clădiri — "
|
||||
: "Sync terenuri + clădiri — "}
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
noGeomScan.localSyncFresh &&
|
||||
noGeomScan.localDbWithGeom > 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`}
|
||||
</span>
|
||||
</li>
|
||||
{includeNoGeom && (
|
||||
<li>
|
||||
Import parcele fără geometrie —{" "}
|
||||
<span className="font-medium text-amber-600 dark:text-amber-400">
|
||||
{(() => {
|
||||
const newNoGeom = Math.max(
|
||||
0,
|
||||
noGeomScan.noGeomCount - noGeomScan.localDbNoGeom,
|
||||
);
|
||||
return newNoGeom > 0
|
||||
? `~${newNoGeom.toLocaleString("ro-RO")} noi de importat`
|
||||
: "deja importate";
|
||||
})()}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
Îmbogățire CF, proprietari, adrese —{" "}
|
||||
<span className="font-medium text-teal-600 dark:text-teal-400">
|
||||
{(() => {
|
||||
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";
|
||||
})()}
|
||||
</span>
|
||||
</li>
|
||||
<li>Generare GPKG + CSV</li>
|
||||
<li>Comprimare ZIP + descărcare</li>
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 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.
|
||||
</p>
|
||||
{localDbLine}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -2449,6 +2570,7 @@ export function ParcelSyncModule() {
|
||||
HAS_GEOMETRY=0). Nu apar în GPKG.
|
||||
</p>
|
||||
)}
|
||||
{workflowPreview}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -2466,6 +2588,15 @@ export function ParcelSyncModule() {
|
||||
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
||||
imobile din eTerra au geometrie — nimic de importat
|
||||
suplimentar.
|
||||
{noGeomScan.localDbTotal > 0 && (
|
||||
<span className="ml-1">
|
||||
({noGeomScan.localDbTotal.toLocaleString("ro-RO")}{" "}
|
||||
în DB local
|
||||
{noGeomScan.localDbEnriched > 0 &&
|
||||
`, ${noGeomScan.localDbEnriched.toLocaleString("ro-RO")} îmbogățite`}
|
||||
{noGeomScan.localSyncFresh && ", proaspăt"})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user