99a673de3d
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>
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState, useCallback } from "react";
|
|
import dynamic from "next/dynamic";
|
|
import { BasemapSwitcher, type BasemapId } from "./basemap-switcher";
|
|
import { SearchBar, type FeatureHit, type UatHit } from "./search-bar";
|
|
import {
|
|
FeatureInfoPanel,
|
|
type ClickedFeatureLite,
|
|
} from "./feature-info-panel";
|
|
import type { MapViewerHandle } from "./map-viewer";
|
|
|
|
const MapViewer = dynamic(
|
|
() => import("./map-viewer").then((m) => ({ default: m.MapViewer })),
|
|
{
|
|
ssr: false,
|
|
loading: () => (
|
|
<div className="flex h-full items-center justify-center bg-muted/30">
|
|
<p className="text-sm text-muted-foreground">Se încarcă harta…</p>
|
|
</div>
|
|
),
|
|
},
|
|
);
|
|
|
|
export function GeoportalV2() {
|
|
const mapRef = useRef<MapViewerHandle>(null);
|
|
const [basemap, setBasemap] = useState<BasemapId>("liberty");
|
|
const [clicked, setClicked] = useState<ClickedFeatureLite | null>(null);
|
|
|
|
const handleFeatureClick = useCallback((f: ClickedFeatureLite | null) => {
|
|
setClicked(f);
|
|
}, []);
|
|
|
|
const handleUatSelect = useCallback((uat: UatHit) => {
|
|
// Search response doesn't carry bbox/centroid yet — leave panel + let user pan.
|
|
// Future: enrich search response with uat bounding box, then flyTo.
|
|
console.info("UAT selected:", uat.siruta, uat.name);
|
|
}, []);
|
|
|
|
const handleFeatureSelect = useCallback((f: FeatureHit) => {
|
|
// Show panel directly (feature ID is known)
|
|
setClicked({
|
|
id: f.id,
|
|
siruta: "",
|
|
cadastralRef: f.cadastralRef,
|
|
layerId: f.layerId,
|
|
areaValue: f.areaValue,
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<div className="absolute inset-0">
|
|
<MapViewer
|
|
ref={mapRef}
|
|
basemap={basemap}
|
|
onFeatureClick={handleFeatureClick}
|
|
className="h-full w-full"
|
|
/>
|
|
|
|
{/* Top-left: search */}
|
|
<div className="absolute left-3 top-3 z-10 flex flex-col gap-2">
|
|
<SearchBar
|
|
onUatSelect={handleUatSelect}
|
|
onFeatureSelect={handleFeatureSelect}
|
|
/>
|
|
</div>
|
|
|
|
{/* Top-right: basemap + panel */}
|
|
<div className="absolute right-14 top-3 z-10 flex flex-col items-end gap-2">
|
|
<BasemapSwitcher value={basemap} onChange={setBasemap} />
|
|
{clicked && (
|
|
<FeatureInfoPanel
|
|
feature={clicked}
|
|
onClose={() => setClicked(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Bottom-right: cutover badge (visible until full rollout) */}
|
|
<div className="pointer-events-none absolute bottom-2 right-2 z-10 rounded bg-primary/90 px-2 py-0.5 text-[10px] font-medium text-primary-foreground shadow">
|
|
gis.ac · v2
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|