feat(parcel-sync): native PostGIS geometry support for QGIS

- Remove postgresqlExtensions/postgis from Prisma schema (PostGIS not yet installed)
- Add prisma/postgis-setup.sql: trigger auto-converts GeoJSON→native geometry,
  GiST spatial index, QGIS-friendly views (gis_terenuri, gis_cladiri, etc.)
- Add POST /api/eterra/setup-postgis endpoint (idempotent, runs all SQL setup)
- Add safety-net raw SQL in sync-service: backfills geom after upsert phase
- Add QGIS/PostGIS setup card in layer catalog UI with connection info
- Schema comment documents the trigger-managed 'geom' column approach
This commit is contained in:
AI Assistant
2026-03-07 10:25:30 +02:00
parent b0c4bf91d7
commit 0d0b1f8c9f
5 changed files with 433 additions and 15 deletions
@@ -338,6 +338,15 @@ export function ParcelSyncModule() {
const [syncProgress, setSyncProgress] = useState("");
const [exportingLocal, setExportingLocal] = useState(false);
/* ── PostGIS setup ───────────────────────────────────────────── */
const [postgisRunning, setPostgisRunning] = useState(false);
const [postgisResult, setPostgisResult] = useState<{
success: boolean;
message?: string;
details?: Record<string, unknown>;
error?: string;
} | null>(null);
/* ── Parcel search tab ──────────────────────────────────────── */
const [searchResults, setSearchResults] = useState<ParcelDetail[]>([]);
const [searchList, setSearchList] = useState<ParcelDetail[]>([]);
@@ -783,6 +792,25 @@ export function ParcelSyncModule() {
[siruta, exportingLocal],
);
/* ════════════════════════════════════════════════════════════ */
/* PostGIS setup (one-time) */
/* ════════════════════════════════════════════════════════════ */
const handleSetupPostgis = useCallback(async () => {
if (postgisRunning) return;
setPostgisRunning(true);
setPostgisResult(null);
try {
const res = await fetch("/api/eterra/setup-postgis", { method: "POST" });
const json = await res.json();
setPostgisResult(json as typeof postgisResult);
} catch (error) {
const msg = error instanceof Error ? error.message : "Eroare setup";
setPostgisResult({ success: false, error: msg });
}
setPostgisRunning(false);
}, [postgisRunning, postgisResult]);
/* ════════════════════════════════════════════════════════════ */
/* Export individual layer */
/* ════════════════════════════════════════════════════════════ */
@@ -1852,6 +1880,103 @@ export function ParcelSyncModule() {
</CardContent>
</Card>
)}
{/* PostGIS / QGIS setup */}
<Card>
<div className="px-4 py-3 border-b">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-violet-500" />
<span className="text-sm font-semibold">
QGIS / PostGIS
</span>
</div>
<Button
size="sm"
variant="outline"
disabled={postgisRunning}
onClick={() => void handleSetupPostgis()}
className="border-violet-300 text-violet-700 dark:border-violet-700 dark:text-violet-300"
>
{postgisRunning ? (
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
) : (
<Database className="h-3.5 w-3.5 mr-1.5" />
)}
{postgisRunning ? "Se configurează…" : "Setup PostGIS"}
</Button>
</div>
</div>
<CardContent className="py-3 space-y-2">
{postgisResult ? (
postgisResult.success ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-400">
{postgisResult.message}
</span>
</div>
{postgisResult.details && (
<div className="rounded bg-muted/50 p-3 text-xs space-y-1 font-mono">
<p>
Backfill:{" "}
{String(
(
postgisResult.details as {
backfilledFeatures?: number;
}
).backfilledFeatures ?? 0,
)}{" "}
features convertite
</p>
<p>
Total cu geometrie nativă:{" "}
{String(
(
postgisResult.details as {
totalFeaturesWithGeom?: number;
}
).totalFeaturesWithGeom ?? 0,
)}
</p>
<p className="text-muted-foreground mt-1">
QGIS PostgreSQL 10.10.10.166:5432 /
architools_db
</p>
<p className="text-muted-foreground">
View-uri: gis_terenuri, gis_cladiri,
gis_documentatii, gis_administrativ
</p>
<p className="text-muted-foreground">SRID: 3844</p>
</div>
)}
</div>
) : (
<div className="flex items-start gap-2">
<XCircle className="h-4 w-4 text-red-500 shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-red-700 dark:text-red-400">
PostGIS nu este instalat
</p>
<p className="text-xs text-muted-foreground mt-1">
Instalează PostGIS pe serverul PostgreSQL:
</p>
<code className="text-xs block mt-1 bg-muted rounded px-2 py-1">
apt install postgresql-16-postgis-3
</code>
</div>
</div>
)
) : (
<p className="text-xs text-muted-foreground">
Creează coloana nativă PostGIS, trigger auto-conversie,
index spațial GiST și view-uri QGIS-compatibile. Necesită
PostGIS instalat pe server.
</p>
)}
</CardContent>
</Card>
</div>
)}