feat(geoportal): Faza E v2 thin client (PMTiles + gis.ac)

New geoportal module flag-gated by session.useGisAc. Legacy code path
preserved as GeoportalV1Legacy (rename only — same logic). When
session.useGisAc=true (Infisical USE_GIS_AC=1 OR email in
GIS_AC_PILOT_USERS), the page renders GeoportalV2 instead.

V2 layout (851 LOC across 5 files):
- map-viewer.tsx (~295 LOC): MapLibre + PMTiles src `pmtiles://pmtiles.gis.ac/overview.pmtiles`. Layers: UAT boundaries (z5, z8), parcels (gis_terenuri line + invisible hit-test fill), buildings (gis_cladiri fill+line). Click → resolves layerId from sourceLayer, emits ClickedFeatureLite (id, siruta, cadastralRef, layerId).
- search-bar.tsx (~160 LOC): debounced 300ms, calls /api/gis/search, dropdown grouped by UATs / Parcele.
- feature-info-panel.tsx (~270 LOC): fetches /api/gis/parcela/[id], renders enrichment block (scope-aware — 403 shown explicitly as "permisiuni insuficiente"). Buttons: "Citește din ANCPI" (POST /api/gis/parcel/tech force:true), "Export GeoPackage" (deep-link to eterra.live/harta?…&autoexport=geopackage), "Comandă CF" placeholder pending Faza F.
- basemap-switcher.tsx (~40 LOC): liberty / dark / satellite / google. Dropped orto + topo50/25 (require ANCPI session — orto/topo via raster.gis.ac is TBD Sprint 2).
- geoportal-v2.tsx (~85 LOC): wraps MapViewer + SearchBar + BasemapSwitcher + FeatureInfoPanel.

API routes (90 LOC across 3 files):
- GET /api/gis/search → gisApi.search
- GET /api/gis/parcela/[id] → gisApi.parcela.get
- POST /api/gis/parcel/tech → gisApi.parcel.tech (refresh ANCPI)

All routes 401 if no NextAuth session, forward GisApiError status+code,
hit api.gis.ac with the Authentik access_token from session. Per
project_audit_correlation_echo memory: no correlationId set on client
side (gis-api overwrites server-side).

Cutover-bottom-right badge "gis.ac · v2" visible until full rollout for
ops visibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-05-18 08:32:36 +03:00
parent fc2bdfb2b4
commit 99a673de3d
9 changed files with 974 additions and 0 deletions
@@ -0,0 +1,41 @@
"use client";
import { cn } from "@/shared/lib/utils";
export type BasemapId = "liberty" | "dark" | "satellite" | "google";
const OPTIONS: Array<{ id: BasemapId; label: string }> = [
{ id: "liberty", label: "Liberty" },
{ id: "dark", label: "Întunecat" },
{ id: "satellite", label: "Satelit" },
{ id: "google", label: "Google" },
];
interface Props {
value: BasemapId;
onChange: (id: BasemapId) => void;
}
export function BasemapSwitcher({ value, onChange }: Props) {
return (
<div className="rounded-md border bg-background/95 shadow-sm backdrop-blur">
<div className="flex items-center gap-1 p-1">
{OPTIONS.map((opt) => (
<button
key={opt.id}
type="button"
onClick={() => onChange(opt.id)}
className={cn(
"rounded px-2 py-1 text-xs font-medium transition-colors",
value === opt.id
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted",
)}
>
{opt.label}
</button>
))}
</div>
</div>
);
}