feat(parcel-sync): import eTerra immovables without geometry
- Add geometrySource field to GisFeature (NO_GEOMETRY marker) - New no-geom-sync service: scan + import parcels missing from GIS layer - Uses negative immovablePk as objectId to avoid @@unique collision - New /api/eterra/no-geom-scan endpoint for counting - Export-bundle: includeNoGeometry flag, imports before enrich - CSV export: new HAS_GEOMETRY column (0/1) - GPKG: still geometry-only (unchanged) - UI: checkbox + scan button on Export tab - Baza de Date tab: shows no-geometry counts per UAT - db-summary API: includes noGeomCount per layer
This commit is contained in:
@@ -351,10 +351,12 @@ export function ParcelSyncModule() {
|
||||
layerId: string;
|
||||
count: number;
|
||||
enrichedCount: number;
|
||||
noGeomCount: number;
|
||||
lastSynced: string | null;
|
||||
}[];
|
||||
totalFeatures: number;
|
||||
totalEnriched: number;
|
||||
totalNoGeom: number;
|
||||
};
|
||||
type DbSummary = {
|
||||
uats: DbUatSummary[];
|
||||
@@ -380,6 +382,16 @@ export function ParcelSyncModule() {
|
||||
const [loadingFeatures, setLoadingFeatures] = useState(false);
|
||||
const [searchError, setSearchError] = useState("");
|
||||
|
||||
/* ── No-geometry import option ──────────────────────────────── */
|
||||
const [includeNoGeom, setIncludeNoGeom] = useState(false);
|
||||
const [noGeomScanning, setNoGeomScanning] = useState(false);
|
||||
const [noGeomScan, setNoGeomScan] = useState<{
|
||||
totalImmovables: number;
|
||||
totalInDb: number;
|
||||
noGeomCount: number;
|
||||
} | null>(null);
|
||||
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* Load UAT data + check server session on mount */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
@@ -594,6 +606,7 @@ export function ParcelSyncModule() {
|
||||
siruta,
|
||||
jobId,
|
||||
mode,
|
||||
includeNoGeometry: includeNoGeom,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -658,9 +671,45 @@ export function ParcelSyncModule() {
|
||||
// Refresh sync status — data was synced to DB
|
||||
refreshSyncRef.current?.();
|
||||
},
|
||||
[siruta, exporting, startPolling],
|
||||
[siruta, exporting, startPolling, includeNoGeom],
|
||||
);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* No-geometry scan */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
|
||||
const handleNoGeomScan = useCallback(async () => {
|
||||
if (!siruta || noGeomScanning) return;
|
||||
setNoGeomScanning(true);
|
||||
setNoGeomScan(null);
|
||||
try {
|
||||
const res = await fetch("/api/eterra/no-geom-scan", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ siruta }),
|
||||
});
|
||||
const data = (await res.json()) as {
|
||||
totalImmovables?: number;
|
||||
totalInDb?: number;
|
||||
noGeomCount?: number;
|
||||
error?: string;
|
||||
};
|
||||
if (data.error) {
|
||||
setNoGeomScan(null);
|
||||
} else {
|
||||
setNoGeomScan({
|
||||
totalImmovables: data.totalImmovables ?? 0,
|
||||
totalInDb: data.totalInDb ?? 0,
|
||||
noGeomCount: data.noGeomCount ?? 0,
|
||||
});
|
||||
setNoGeomScanSiruta(siruta);
|
||||
}
|
||||
} catch {
|
||||
setNoGeomScan(null);
|
||||
}
|
||||
setNoGeomScanning(false);
|
||||
}, [siruta, noGeomScanning]);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* Layer feature counts */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
@@ -2295,6 +2344,71 @@ export function ParcelSyncModule() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* No-geometry option */}
|
||||
{sirutaValid && session.connected && (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-3 px-4">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeNoGeom}
|
||||
onChange={(e) => setIncludeNoGeom(e.target.checked)}
|
||||
disabled={exporting}
|
||||
className="h-4 w-4 rounded border-muted-foreground/30 accent-amber-600"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
Include parcele{" "}
|
||||
<span className="font-medium">fără geometrie</span>
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={noGeomScanning || exporting}
|
||||
onClick={() => void handleNoGeomScan()}
|
||||
>
|
||||
{noGeomScanning ? (
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Search className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
Scanare
|
||||
</Button>
|
||||
{noGeomScan && noGeomScanSiruta === siruta && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{noGeomScan.noGeomCount > 0 ? (
|
||||
<>
|
||||
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
||||
{noGeomScan.noGeomCount.toLocaleString("ro-RO")}
|
||||
</span>{" "}
|
||||
parcele fără geometrie
|
||||
<span className="ml-1 opacity-60">
|
||||
(din{" "}
|
||||
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
||||
total eTerra,{" "}
|
||||
{noGeomScan.totalInDb.toLocaleString("ro-RO")} în DB)
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-emerald-600 dark:text-emerald-400">
|
||||
Toate parcelele au geometrie
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{includeNoGeom && (
|
||||
<p className="text-[11px] text-muted-foreground mt-1.5 ml-6">
|
||||
Parcelele fără geometrie vor apărea doar în CSV (coloana
|
||||
HAS_GEOMETRY=0), nu în GPKG.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Progress bar */}
|
||||
{exportProgress &&
|
||||
exportProgress.status !== "unknown" &&
|
||||
@@ -2442,12 +2556,14 @@ export function ParcelSyncModule() {
|
||||
{dbSummary.uats.map((uat) => {
|
||||
const catCounts: Record<string, number> = {};
|
||||
let enrichedTotal = 0;
|
||||
let noGeomTotal = 0;
|
||||
let oldestSync: Date | null = null;
|
||||
for (const layer of uat.layers) {
|
||||
const cat =
|
||||
findLayerById(layer.layerId)?.category ?? "administrativ";
|
||||
catCounts[cat] = (catCounts[cat] ?? 0) + layer.count;
|
||||
enrichedTotal += layer.enrichedCount;
|
||||
noGeomTotal += layer.noGeomCount ?? 0;
|
||||
if (layer.lastSynced) {
|
||||
const d = new Date(layer.lastSynced);
|
||||
if (!oldestSync || d < oldestSync) oldestSync = d;
|
||||
@@ -2523,6 +2639,16 @@ export function ParcelSyncModule() {
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{noGeomTotal > 0 && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="text-muted-foreground">
|
||||
Fără geom:
|
||||
</span>
|
||||
<span className="font-medium tabular-nums text-amber-600 dark:text-amber-400">
|
||||
{noGeomTotal.toLocaleString("ro-RO")}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Layer detail pills */}
|
||||
|
||||
Reference in New Issue
Block a user