feat: add Geoportal module with MapLibre GL JS + Martin vector tiles

Phase 1 of the geoportal implementation:

Infrastructure:
- Martin vector tile server in docker-compose (port 3010)
- PostGIS setup SQL for GisUat: native geom column, Esri→PostGIS
  trigger, GiST index, gis_uats view for Martin auto-discovery

Geoportal module (src/modules/geoportal/):
- map-viewer.tsx: MapLibre GL JS canvas with OSM base, Martin MVT
  sources (gis_uats, gis_terenuri, gis_cladiri), click-to-inspect,
  zoom-level-aware layer visibility, layer styling
- layer-panel.tsx: collapsible sidebar with layer toggles
- geoportal-module.tsx: standalone page wrapper
- Module registered in config/modules.ts, flags.ts, i18n

ParcelSync integration:
- 6th tab "Harta" with lazy-loaded MapViewer (ssr: false)
- Centered on selected UAT

Dependencies: maplibre-gl v5.21.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-23 14:21:37 +02:00
parent 53595fdf94
commit c297a2c5f7
15 changed files with 1227 additions and 2 deletions
@@ -61,12 +61,29 @@ import {
} from "../services/eterra-layers";
import type { ParcelDetail } from "@/app/api/eterra/search/route";
import type { OwnerSearchResult } from "@/app/api/eterra/search-owner/route";
import { User, FileText, Archive } from "lucide-react";
import { User, FileText, Archive, Map as MapIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { UatDashboard } from "./uat-dashboard";
import { EpayConnect, type EpaySessionStatus } from "./epay-connect";
import { EpayOrderButton } from "./epay-order-button";
import { EpayTab } from "./epay-tab";
/* MapLibre uses WebGL — must disable SSR */
const MapViewer = dynamic(
() =>
import("@/modules/geoportal/components/map-viewer").then((m) => ({
default: m.MapViewer,
})),
{
ssr: false,
loading: () => (
<div className="flex items-center justify-center h-64 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">Se incarca harta...</p>
</div>
),
}
);
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
@@ -2070,6 +2087,10 @@ export function ParcelSyncModule() {
<FileText className="h-4 w-4" />
Extrase CF
</TabsTrigger>
<TabsTrigger value="map" className="gap-1.5">
<MapIcon className="h-4 w-4" />
Harta
</TabsTrigger>
</TabsList>
</div>
@@ -4765,6 +4786,15 @@ export function ParcelSyncModule() {
<TabsContent value="extracts" className="space-y-4">
<EpayTab />
</TabsContent>
{/* ═══════════════════════════════════════════════════════ */}
{/* Tab 6: Harta (MapLibre GL) */}
{/* ═══════════════════════════════════════════════════════ */}
<TabsContent value="map" className="space-y-4">
<div className="relative h-[600px] rounded-lg border overflow-hidden">
<MapViewer className="h-full w-full" />
</div>
</TabsContent>
</Tabs>
);
}