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:
AI Assistant
2026-03-07 13:06:45 +02:00
parent 30915e8628
commit 5861e06ddb
3 changed files with 138 additions and 60 deletions
@@ -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,12 +2354,70 @@ 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 &&
(() => {
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"> <CardContent className="py-3 px-4">
<div className="flex items-center gap-3 flex-wrap"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<label className="flex items-center gap-2 cursor-pointer select-none"> <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 <input
type="checkbox" type="checkbox"
checked={includeNoGeom} checked={includeNoGeom}
@@ -2357,57 +2425,38 @@ export function ParcelSyncModule() {
disabled={exporting} disabled={exporting}
className="h-4 w-4 rounded border-muted-foreground/30 accent-amber-600" className="h-4 w-4 rounded border-muted-foreground/30 accent-amber-600"
/> />
<span className="text-sm"> <span className="text-sm font-medium">
Include parcele{" "} Include și parcelele fără geometrie la export
<span className="font-medium">fără geometrie</span>
</span> </span>
</label> </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 && ( {includeNoGeom && (
<p className="text-[11px] text-muted-foreground mt-1.5 ml-6"> <p className="text-[11px] text-muted-foreground ml-7">
Parcelele fără geometrie vor apărea doar în CSV (coloana Vor fi importate în DB și incluse în CSV (coloana
HAS_GEOMETRY=0), nu în GPKG. HAS_GEOMETRY=0). Nu apar în GPKG.
</p> </p>
)} )}
</CardContent> </CardContent>
</Card> </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 */} {/* Progress bar */}
{exportProgress && {exportProgress &&
+19
View File
@@ -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();
});
+10
View File
@@ -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[''](); });