fix(parcel-sync): auto-scan no-geometry + redesign UI card
- Auto-scan triggers when UAT selected + connected (no manual click needed) - Three states: scanning spinner, found N parcels (amber alert card), all OK (green check) - Checkbox more prominent: only shown when no-geom parcels exist - Re-scan button available, scan result cached per siruta - AlertTriangle icon for visual warning
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
HardDrive,
|
||||
Clock,
|
||||
ArrowDownToLine,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
@@ -710,6 +711,15 @@ export function ParcelSyncModule() {
|
||||
setNoGeomScanning(false);
|
||||
}, [siruta, noGeomScanning]);
|
||||
|
||||
// Auto-scan for no-geometry parcels when UAT is selected + connected
|
||||
useEffect(() => {
|
||||
if (!siruta || !session.connected || noGeomScanning) return;
|
||||
// Don't re-scan if we already scanned this siruta
|
||||
if (noGeomScanSiruta === siruta && noGeomScan !== null) return;
|
||||
void handleNoGeomScan();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [siruta, session.connected]);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* Layer feature counts */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
@@ -2344,12 +2354,70 @@ export function ParcelSyncModule() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* No-geometry option */}
|
||||
{sirutaValid && session.connected && (
|
||||
<Card className="border-dashed">
|
||||
{/* No-geometry option — shown after auto-scan completes */}
|
||||
{sirutaValid &&
|
||||
session.connected &&
|
||||
(() => {
|
||||
const scanDone = noGeomScan !== null && noGeomScanSiruta === siruta;
|
||||
const hasNoGeomParcels = scanDone && noGeomScan.noGeomCount > 0;
|
||||
const scanning = noGeomScanning;
|
||||
|
||||
// Still scanning
|
||||
if (scanning)
|
||||
return (
|
||||
<Card className="border-amber-200/50 dark:border-amber-800/50">
|
||||
<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">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-amber-500" />
|
||||
Se verifică parcele fără geometrie în eTerra…
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// No-geometry parcels found
|
||||
if (hasNoGeomParcels)
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
includeNoGeom
|
||||
? "border-amber-400 bg-amber-50/50 dark:border-amber-700 dark:bg-amber-950/20"
|
||||
: "border-amber-200 dark:border-amber-800/50",
|
||||
)}
|
||||
>
|
||||
<CardContent className="py-3 px-4 space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm">
|
||||
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
||||
{noGeomScan.noGeomCount.toLocaleString("ro-RO")}
|
||||
</span>{" "}
|
||||
parcele există în eTerra dar{" "}
|
||||
<span className="font-medium">nu au geometrie</span>{" "}
|
||||
în layerul GIS
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">
|
||||
Din{" "}
|
||||
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
||||
imobile total în eTerra,{" "}
|
||||
{noGeomScan.totalInDb.toLocaleString("ro-RO")} sunt
|
||||
deja în baza de date cu geometrie.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs shrink-0"
|
||||
disabled={noGeomScanning || exporting}
|
||||
onClick={() => void handleNoGeomScan()}
|
||||
title="Re-scanare"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none ml-7">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeNoGeom}
|
||||
@@ -2357,57 +2425,38 @@ export function ParcelSyncModule() {
|
||||
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 className="text-sm font-medium">
|
||||
Include și parcelele fără geometrie la export
|
||||
</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 className="text-[11px] text-muted-foreground ml-7">
|
||||
Vor fi importate în DB și incluse în CSV (coloana
|
||||
HAS_GEOMETRY=0). Nu apar în GPKG.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
);
|
||||
|
||||
// Scan done, all parcels have geometry
|
||||
if (scanDone && !hasNoGeomParcels)
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-2.5 px-4">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />
|
||||
Toate cele{" "}
|
||||
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
||||
parcele din eTerra au geometrie — nimic de importat
|
||||
suplimentar.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{/* Progress bar */}
|
||||
{exportProgress &&
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const p = new PrismaClient();
|
||||
const exec = p.$executeRawUnsafe.bind(p);
|
||||
const disconnect = p.$disconnect.bind(p);
|
||||
async function main() {
|
||||
await exec(
|
||||
'ALTER TABLE "GisFeature" ADD COLUMN IF NOT EXISTS "geometrySource" TEXT',
|
||||
);
|
||||
console.log("OK: geometrySource column added");
|
||||
await exec(
|
||||
'CREATE INDEX IF NOT EXISTS "GisFeature_geometrySource_idx" ON "GisFeature" ("geometrySource")',
|
||||
);
|
||||
console.log("OK: index created");
|
||||
await disconnect();
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.log("ERR:", e.message);
|
||||
disconnect();
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const p = new PrismaClient();
|
||||
async function main() {
|
||||
await p['']('ALTER TABLE "GisFeature" ADD COLUMN IF NOT EXISTS "geometrySource" TEXT');
|
||||
console.log('OK: geometrySource column added');
|
||||
await p['']('CREATE INDEX IF NOT EXISTS "GisFeature_geometrySource_idx" ON "GisFeature" ("geometrySource")');
|
||||
console.log('OK: index created');
|
||||
await p['']();
|
||||
}
|
||||
main().catch(e => { console.log('ERR:', e.message); p[''](); });
|
||||
Reference in New Issue
Block a user