From 52c31e3c4d6639349656d035bf537aada9adb95c Mon Sep 17 00:00:00 2001 From: Claude VM Date: Wed, 20 May 2026 17:49:46 +0300 Subject: [PATCH] =?UTF-8?q?feat(geoportal-v2):=20UAT=20name=20+=20SOLICITA?= =?UTF-8?q?NT=20into=20=C3=8Enscriere=20+=20Google=20Maps=20inline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration on the info panel per Marius's feedback. 1. UAT NAME IN HEADER New uat-lookup.ts hook loads public/uat.json (3,186 rows, ~95 KB, one-shot fetch + Map cache + subscribers) and exposes useUatName(siruta). Header reads: Terenuri · 2.400 m² · FELEACU · 57582 instead of just "SIRUTA 57582". The localitate name lives in front of the bare siruta number (muted, smaller weight) — siruta is still there for ops + tooltip, just not the primary signal. 2. SOLICITANT MOVED INTO ÎNSCRIERE Was rendered as a prominent User-icon line right above PROPRIETARI, which led to "BOJAN ELENA = current owner?" confusion. The two fields semantically differ: SOLICITANT is the person who filed the most recent ANCPI application (e.g. the new buyer initiating a transfer), PROPRIETARI is who's currently registered as owner. Now SOLICITANT is collapsed into the existing Înscriere
next to TIP_INSCRIERE / DATA_CERERE / ACT_PROPRIETATE — the registration-metadata bucket where it belongs. 3. GOOGLE MAPS INLINE WITH ADDRESS When ADRESA exists, the Google Maps text-link sits right of the address (using feature.lat/lng for the query). One-tap go-to-map without a separate Localizare section. 4. LOCALIZARE → COLLAPSIBLE Bottom Localizare card becomes a closed-by-default
. Inside: WGS84 lat/lng, SIRUTA, and a separate Google Maps link. ID (objectId) shows in the summary line. Mirrors eterra.live's approach. The redundant Feleacu/coords echo at the bottom is gone — coords are still one click away when needed. NOT in this commit (parked for follow-up): - PIZ / Plan situație / Coord. / DXF actions — would mean porting eterra.live's three /api/geoportal/{piz,pad,coords-xlsx} document generators. Substantial work (mapbox-static-image render + server-side PDF layout); needs its own session. - CF intern (gratuit) vs Extras CF (1 credit) split — current "Comandă CF" modal already handles both pool/connection states, but the two-button visual split mirroring eterra.live's catalog- hit fast path is a smaller follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../geoportal/v2/feature-info-panel.tsx | 83 ++++++++++++++----- src/modules/geoportal/v2/uat-lookup.ts | 57 +++++++++++++ 2 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 src/modules/geoportal/v2/uat-lookup.ts diff --git a/src/modules/geoportal/v2/feature-info-panel.tsx b/src/modules/geoportal/v2/feature-info-panel.tsx index db48d78..fed69b9 100644 --- a/src/modules/geoportal/v2/feature-info-panel.tsx +++ b/src/modules/geoportal/v2/feature-info-panel.tsx @@ -5,11 +5,12 @@ import { signIn } from "next-auth/react"; import { X, RefreshCw, Loader2, FileText, AlertCircle, Home, Building, Building2, MapPin, ChevronRight, Users, - Sparkles, User, ShieldCheck, AlertTriangle, HelpCircle, + Sparkles, ShieldCheck, AlertTriangle, HelpCircle, Factory, Warehouse, } from "lucide-react"; import { cn } from "@/shared/lib/utils"; import { CfOrderModal } from "./cf-order-modal"; +import { useUatName } from "./uat-lookup"; const AUTH_RETRY_KEY = "gis_panel_auth_retry"; @@ -791,6 +792,7 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa const cadrefHeader = feature.cadastralRef || feature.objectId || "—"; const layerLabel = feature.layerId.replace("_ACTIVE", "").toLowerCase(); + const uatName = useUatName(feature.siruta); const enrichedAgo = formatRelativeTime(detail?.enrichedAt); @@ -811,8 +813,14 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa {feature.areaValue != null && ( · {formatNum(feature.areaValue)} m² )} + {uatName && ( + · {uatName} + )} {feature.siruta && ( - · SIRUTA {feature.siruta} + · {feature.siruta} )}

@@ -979,14 +987,18 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa {adresa && (
-

{adresa}

-
- )} - - {solicitant && ( -
- -

{solicitant}

+

{adresa}

+ {feature.lat != null && feature.lng != null && ( + + Google Maps + + )}
)} @@ -1001,7 +1013,12 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa /> )} - {(tipInscriere || dataCererii || actProp) && ( + {/* Înscriere — collapsed. Holds the most-recent application + metadata (who applied, when, what document) which is + NOT the same as current ownership. SOLICITANT lives + here, not next to PROPRIETARI, to avoid the "Bojan + Elena = proprietar?" confusion. */} + {(solicitant || tipInscriere || dataCererii || actProp) && (
@@ -1010,6 +1027,7 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa

+ {solicitant && } {tipInscriere && } {dataCererii && } {actProp && } @@ -1193,24 +1211,47 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
)} - {/* Localizare */} + {/* Localizare — collapsible. Coords + ObjectId for those who need + them (cadastrali, etc.). Skipped when ADRESA is shown above + (Google Maps link moves there); otherwise this is the only + way to get a map link, so keep it. */} {feature.lat != null && feature.lng != null && ( -
-
- - - {feature.lat.toFixed(5)}, {feature.lng.toFixed(5)} - +
+ + +

+ Localizare +

+ {feature.objectId && ( + + ID {feature.objectId} + + )} +
+
+
+ WGS84 + + {feature.lat.toFixed(6)}, {feature.lng.toFixed(6)} + +
+
+ SIRUTA + + {feature.siruta || "—"} + +
- Google Maps + + Deschide în Google Maps
-
+
)} {!loading && !detail && !error && ( diff --git a/src/modules/geoportal/v2/uat-lookup.ts b/src/modules/geoportal/v2/uat-lookup.ts new file mode 100644 index 0000000..d5b52a7 --- /dev/null +++ b/src/modules/geoportal/v2/uat-lookup.ts @@ -0,0 +1,57 @@ +"use client"; + +// Lazy, browser-side lookup of UAT (siruta → name). +// +// `public/uat.json` is a 3,186-row static asset (~95 KB) generated once +// from `gis_core.GisUat`. The V2 panel uses it to display the UAT +// name in the header ("FELEACU · SIRUTA 57582" instead of just "SIRUTA +// 57582"). Single fetch per tab, cached in a module-level Map. +// +// Hook callers re-render once the map resolves. No suspense, no +// loading flash — the header just upgrades from "SIRUTA 57582" to +// "FELEACU · SIRUTA 57582" the instant the fetch completes. + +import { useEffect, useState } from "react"; + +type UatEntry = { siruta: string; name: string }; + +let cache: Map | null = null; +let inflight: Promise> | null = null; +const subscribers = new Set<() => void>(); + +async function loadUatMap(): Promise> { + if (cache) return cache; + if (inflight) return inflight; + inflight = (async () => { + try { + const res = await fetch("/uat.json", { cache: "force-cache" }); + if (!res.ok) throw new Error(`uat.json HTTP ${res.status}`); + const arr = (await res.json()) as UatEntry[]; + const map = new Map(); + for (const u of arr) { + if (u?.siruta && u?.name) map.set(String(u.siruta), u.name); + } + cache = map; + subscribers.forEach((cb) => cb()); + return map; + } finally { + inflight = null; + } + })(); + return inflight; +} + +export function useUatName(siruta: string | undefined): string | null { + const [, force] = useState(0); + useEffect(() => { + if (cache) return; + const cb = () => force((n) => n + 1); + subscribers.add(cb); + void loadUatMap(); + return () => { + subscribers.delete(cb); + }; + }, []); + if (!siruta) return null; + return cache?.get(String(siruta)) ?? null; +}