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;
|
totalImmovables: number;
|
||||||
withGeometry: number;
|
withGeometry: number;
|
||||||
noGeomCount: number;
|
noGeomCount: number;
|
||||||
|
localDbTotal: number;
|
||||||
|
localDbWithGeom: number;
|
||||||
|
localDbNoGeom: number;
|
||||||
|
localDbEnriched: number;
|
||||||
|
localSyncFresh: boolean;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
|
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
|
||||||
|
|
||||||
@@ -686,6 +691,16 @@ export function ParcelSyncModule() {
|
|||||||
setNoGeomScanning(true);
|
setNoGeomScanning(true);
|
||||||
setNoGeomScan(null);
|
setNoGeomScan(null);
|
||||||
setNoGeomScanSiruta(s);
|
setNoGeomScanSiruta(s);
|
||||||
|
const emptyResult = {
|
||||||
|
totalImmovables: 0,
|
||||||
|
withGeometry: 0,
|
||||||
|
noGeomCount: 0,
|
||||||
|
localDbTotal: 0,
|
||||||
|
localDbWithGeom: 0,
|
||||||
|
localDbNoGeom: 0,
|
||||||
|
localDbEnriched: 0,
|
||||||
|
localSyncFresh: false,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/eterra/no-geom-scan", {
|
const res = await fetch("/api/eterra/no-geom-scan", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -695,25 +710,24 @@ export function ParcelSyncModule() {
|
|||||||
workspacePk: workspacePk ?? undefined,
|
workspacePk: workspacePk ?? undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json()) as {
|
const data = (await res.json()) as Record<string, unknown>;
|
||||||
totalImmovables?: number;
|
|
||||||
withGeometry?: number;
|
|
||||||
noGeomCount?: number;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.warn("[no-geom-scan]", data.error);
|
console.warn("[no-geom-scan]", data.error);
|
||||||
setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
|
setNoGeomScan(emptyResult);
|
||||||
} else {
|
} else {
|
||||||
setNoGeomScan({
|
setNoGeomScan({
|
||||||
totalImmovables: data.totalImmovables ?? 0,
|
totalImmovables: Number(data.totalImmovables ?? 0),
|
||||||
withGeometry: data.withGeometry ?? 0,
|
withGeometry: Number(data.withGeometry ?? 0),
|
||||||
noGeomCount: data.noGeomCount ?? 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 {
|
} catch {
|
||||||
// Show zero result on network error
|
setNoGeomScan(emptyResult);
|
||||||
setNoGeomScan({ totalImmovables: 0, withGeometry: 0, noGeomCount: 0 });
|
|
||||||
}
|
}
|
||||||
setNoGeomScanning(false);
|
setNoGeomScanning(false);
|
||||||
},
|
},
|
||||||
@@ -2385,6 +2399,112 @@ export function ParcelSyncModule() {
|
|||||||
</Card>
|
</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
|
// No-geometry parcels found
|
||||||
if (hasNoGeomParcels)
|
if (hasNoGeomParcels)
|
||||||
return (
|
return (
|
||||||
@@ -2419,6 +2539,7 @@ export function ParcelSyncModule() {
|
|||||||
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.
|
||||||
</p>
|
</p>
|
||||||
|
{localDbLine}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -2449,6 +2570,7 @@ export function ParcelSyncModule() {
|
|||||||
HAS_GEOMETRY=0). Nu apar în GPKG.
|
HAS_GEOMETRY=0). Nu apar în GPKG.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{workflowPreview}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -2466,6 +2588,15 @@ export function ParcelSyncModule() {
|
|||||||
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
||||||
imobile din eTerra au geometrie — nimic de importat
|
imobile din eTerra au geometrie — nimic de importat
|
||||||
suplimentar.
|
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;
|
paperCadNo?: string;
|
||||||
paperCfNo?: 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 message if workspace couldn't be resolved */
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
@@ -141,6 +151,11 @@ export async function scanNoGeometryParcels(
|
|||||||
withGeometry: 0,
|
withGeometry: 0,
|
||||||
noGeomCount: 0,
|
noGeomCount: 0,
|
||||||
samples: [],
|
samples: [],
|
||||||
|
localDbTotal: 0,
|
||||||
|
localDbWithGeom: 0,
|
||||||
|
localDbNoGeom: 0,
|
||||||
|
localDbEnriched: 0,
|
||||||
|
localSyncFresh: false,
|
||||||
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
|
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 {
|
return {
|
||||||
totalImmovables: allImmovables.length,
|
totalImmovables: allImmovables.length,
|
||||||
withGeometry: remoteFeatures.length,
|
withGeometry: remoteFeatures.length,
|
||||||
noGeomCount: noGeomItems.length,
|
noGeomCount: noGeomItems.length,
|
||||||
samples: noGeomItems.slice(0, 20),
|
samples: noGeomItems.slice(0, 20),
|
||||||
|
localDbTotal: localTotal,
|
||||||
|
localDbWithGeom: localWithGeom,
|
||||||
|
localDbNoGeom: localNoGeom,
|
||||||
|
localDbEnriched: localEnriched,
|
||||||
|
localSyncFresh: syncFresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user