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,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 &&
+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[''](); });