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,
|
HardDrive,
|
||||||
Clock,
|
Clock,
|
||||||
ArrowDownToLine,
|
ArrowDownToLine,
|
||||||
|
AlertTriangle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Input } from "@/shared/components/ui/input";
|
import { Input } from "@/shared/components/ui/input";
|
||||||
@@ -710,6 +711,15 @@ export function ParcelSyncModule() {
|
|||||||
setNoGeomScanning(false);
|
setNoGeomScanning(false);
|
||||||
}, [siruta, noGeomScanning]);
|
}, [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 */
|
/* Layer feature counts */
|
||||||
/* ════════════════════════════════════════════════════════════ */
|
/* ════════════════════════════════════════════════════════════ */
|
||||||
@@ -2344,70 +2354,109 @@ export function ParcelSyncModule() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* No-geometry option */}
|
{/* No-geometry option — shown after auto-scan completes */}
|
||||||
{sirutaValid && session.connected && (
|
{sirutaValid &&
|
||||||
<Card className="border-dashed">
|
session.connected &&
|
||||||
<CardContent className="py-3 px-4">
|
(() => {
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
const scanDone = noGeomScan !== null && noGeomScanSiruta === siruta;
|
||||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
const hasNoGeomParcels = scanDone && noGeomScan.noGeomCount > 0;
|
||||||
<input
|
const scanning = noGeomScanning;
|
||||||
type="checkbox"
|
|
||||||
checked={includeNoGeom}
|
// Still scanning
|
||||||
onChange={(e) => setIncludeNoGeom(e.target.checked)}
|
if (scanning)
|
||||||
disabled={exporting}
|
return (
|
||||||
className="h-4 w-4 rounded border-muted-foreground/30 accent-amber-600"
|
<Card className="border-amber-200/50 dark:border-amber-800/50">
|
||||||
/>
|
<CardContent className="py-3 px-4">
|
||||||
<span className="text-sm">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
Include parcele{" "}
|
<Loader2 className="h-4 w-4 animate-spin text-amber-500" />
|
||||||
<span className="font-medium">fără geometrie</span>
|
Se verifică parcele fără geometrie în eTerra…
|
||||||
</span>
|
</div>
|
||||||
</label>
|
</CardContent>
|
||||||
<Button
|
</Card>
|
||||||
variant="outline"
|
);
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs"
|
// No-geometry parcels found
|
||||||
disabled={noGeomScanning || exporting}
|
if (hasNoGeomParcels)
|
||||||
onClick={() => void handleNoGeomScan()}
|
return (
|
||||||
>
|
<Card
|
||||||
{noGeomScanning ? (
|
className={cn(
|
||||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
"transition-colors",
|
||||||
) : (
|
includeNoGeom
|
||||||
<Search className="h-3 w-3 mr-1" />
|
? "border-amber-400 bg-amber-50/50 dark:border-amber-700 dark:bg-amber-950/20"
|
||||||
|
: "border-amber-200 dark:border-amber-800/50",
|
||||||
)}
|
)}
|
||||||
Scanare
|
>
|
||||||
</Button>
|
<CardContent className="py-3 px-4 space-y-2">
|
||||||
{noGeomScan && noGeomScanSiruta === siruta && (
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xs text-muted-foreground">
|
<AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />
|
||||||
{noGeomScan.noGeomCount > 0 ? (
|
<div className="flex-1 min-w-0">
|
||||||
<>
|
<p className="text-sm">
|
||||||
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
<span className="font-semibold text-amber-600 dark:text-amber-400">
|
||||||
{noGeomScan.noGeomCount.toLocaleString("ro-RO")}
|
{noGeomScan.noGeomCount.toLocaleString("ro-RO")}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
parcele fără geometrie
|
parcele există în eTerra dar{" "}
|
||||||
<span className="ml-1 opacity-60">
|
<span className="font-medium">nu au geometrie</span>{" "}
|
||||||
(din{" "}
|
în layerul GIS
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground mt-0.5">
|
||||||
|
Din{" "}
|
||||||
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
{noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "}
|
||||||
total eTerra,{" "}
|
imobile total în eTerra,{" "}
|
||||||
{noGeomScan.totalInDb.toLocaleString("ro-RO")} în DB)
|
{noGeomScan.totalInDb.toLocaleString("ro-RO")} sunt
|
||||||
</span>
|
deja în baza de date cu geometrie.
|
||||||
</>
|
</p>
|
||||||
) : (
|
</div>
|
||||||
<span className="text-emerald-600 dark:text-emerald-400">
|
<Button
|
||||||
Toate parcelele au geometrie
|
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}
|
||||||
|
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 font-medium">
|
||||||
|
Include și parcelele fără geometrie la export
|
||||||
</span>
|
</span>
|
||||||
|
</label>
|
||||||
|
{includeNoGeom && (
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
</span>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
</div>
|
);
|
||||||
{includeNoGeom && (
|
|
||||||
<p className="text-[11px] text-muted-foreground mt-1.5 ml-6">
|
// Scan done, all parcels have geometry
|
||||||
Parcelele fără geometrie vor apărea doar în CSV (coloana
|
if (scanDone && !hasNoGeomParcels)
|
||||||
HAS_GEOMETRY=0), nu în GPKG.
|
return (
|
||||||
</p>
|
<Card className="border-dashed">
|
||||||
)}
|
<CardContent className="py-2.5 px-4">
|
||||||
</CardContent>
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
</Card>
|
<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 */}
|
{/* Progress bar */}
|
||||||
{exportProgress &&
|
{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