"use client"; import React, { useState, useCallback, useMemo, useEffect, useRef } from "react"; import dynamic from "next/dynamic"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; import { Badge } from "@/shared/components/ui/badge"; import { Card, CardContent } from "@/shared/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/shared/components/ui/select"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/shared/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/shared/components/ui/tooltip"; import { Loader2, ChevronDown, ChevronUp, Download, Search, FileText, Clock, AlertTriangle, Settings2, Shield, ArrowUpDown, ArrowUp, ArrowDown, Archive, Map as MapIcon, Moon, Satellite, RefreshCw, } from "lucide-react"; import { cn } from "@/shared/lib/utils"; import { SelectionToolbar, type SelectionMode } from "@/modules/geoportal/components/selection-toolbar"; // Simple inline feature panel — no enrichment, no CF extract import type { MapViewerHandle } from "@/modules/geoportal/components/map-viewer"; import type { BasemapId, ClickedFeature, LayerVisibility, SelectedFeature, } from "@/modules/geoportal/types"; /* MapLibre uses WebGL — must disable SSR */ const MapViewer = dynamic( () => import("@/modules/geoportal/components/map-viewer").then((m) => ({ default: m.MapViewer })), { ssr: false, loading: () => (

Se incarca harta...

), }, ); /* ================================================================== */ /* RGI Types & Constants */ /* ================================================================== */ type App = { actorName: string; adminUnit: number; appDate: number; appNo: number; applicationObject: string; applicationPk: number; colorNumber: number; communicationType: string; deponent: string; dueDate: number; hasSolution: number; identifiers: string; initialAppNo: string; orgUnit: string; requester: string; resolutionName: string; stateCode: string; statusName: string; totalFee: number; uat: string; workspace: string; workspaceId: number; [key: string]: unknown; }; type IssuedDoc = { applicationId: number; docType: string; documentPk: number; documentTypeCode: string; documentTypeId: number; fileExtension: string; digitallySigned: number; startDate: number; lastUpdatedDtm: number; initialAppNo: string; workspaceId: number; identifierDetails: string | null; [key: string]: unknown; }; type SortDir = "asc" | "desc"; type SortState = { key: string; dir: SortDir } | null; const COUNTIES = [ { id: 10, name: "Alba" }, { id: 29, name: "Arad" }, { id: 38, name: "Arges" }, { id: 47, name: "Bacau" }, { id: 56, name: "Bihor" }, { id: 65, name: "Bistrita-Nasaud" }, { id: 74, name: "Botosani" }, { id: 83, name: "Brasov" }, { id: 92, name: "Braila" }, { id: 108, name: "Buzau" }, { id: 117, name: "Caras-Severin" }, { id: 127, name: "Cluj" }, { id: 136, name: "Constanta" }, { id: 145, name: "Covasna" }, { id: 154, name: "Dambovita" }, { id: 163, name: "Dolj" }, { id: 172, name: "Galati" }, { id: 181, name: "Giurgiu" }, { id: 190, name: "Gorj" }, { id: 199, name: "Harghita" }, { id: 208, name: "Hunedoara" }, { id: 217, name: "Ialomita" }, { id: 226, name: "Iasi" }, { id: 235, name: "Ilfov" }, { id: 244, name: "Maramures" }, { id: 253, name: "Mehedinti" }, { id: 262, name: "Mures" }, { id: 271, name: "Neamt" }, { id: 280, name: "Olt" }, { id: 289, name: "Prahova" }, { id: 298, name: "Satu Mare" }, { id: 307, name: "Salaj" }, { id: 316, name: "Sibiu" }, { id: 325, name: "Suceava" }, { id: 334, name: "Teleorman" }, { id: 343, name: "Timis" }, { id: 352, name: "Tulcea" }, { id: 361, name: "Vaslui" }, { id: 370, name: "Valcea" }, { id: 379, name: "Vrancea" }, { id: 401, name: "Bucuresti" }, ] as const; /* ================================================================== */ /* RGI Column definitions */ /* ================================================================== */ type ColumnDef = { key: string; label: string; defaultVisible: boolean; render: (app: App) => string; className?: string; }; function fmtTs(ts: number | null | undefined): string { if (!ts) return "-"; const d = new Date(ts); if (isNaN(d.getTime())) return "-"; return d.toLocaleDateString("ro-RO", { day: "2-digit", month: "2-digit", year: "numeric", }); } const ALL_COLUMNS: ColumnDef[] = [ { key: "appNo", label: "Nr. cerere", defaultVisible: true, render: (a) => String(a.appNo ?? "-"), className: "font-mono font-semibold", }, { key: "initialAppNo", label: "Nr. initial", defaultVisible: false, render: (a) => a.initialAppNo || "-", className: "font-mono text-xs", }, { key: "applicationObject", label: "Obiect", defaultVisible: false, render: (a) => a.applicationObject || "-", }, { key: "identifiers", label: "Identificatori (IE/CF)", defaultVisible: false, render: (a) => a.identifiers || "-", className: "text-xs max-w-[300px] truncate", }, { key: "deponent", label: "Deponent", defaultVisible: false, render: (a) => a.deponent || "-", }, { key: "requester", label: "Solicitant", defaultVisible: true, render: (a) => a.requester || "-", }, { key: "appDate", label: "Data depunere", defaultVisible: false, render: (a) => fmtTs(a.appDate), className: "tabular-nums", }, { key: "dueDate", label: "Termen", defaultVisible: true, render: (a) => fmtTs(a.dueDate), className: "tabular-nums", }, { key: "statusName", label: "Status", defaultVisible: true, render: (a) => a.statusName || a.stateCode || "-", }, { key: "resolutionName", label: "Rezolutie", defaultVisible: true, render: (a) => a.resolutionName || "-", }, { key: "hasSolution", label: "Solutionat", defaultVisible: false, render: (a) => (a.hasSolution === 1 ? "DA" : "NU"), }, { key: "totalFee", label: "Taxa (lei)", defaultVisible: false, render: (a) => (a.totalFee != null ? String(a.totalFee) : "-"), className: "tabular-nums", }, { key: "uat", label: "UAT", defaultVisible: true, render: (a) => a.uat || "-", }, { key: "orgUnit", label: "OCPI", defaultVisible: false, render: (a) => a.orgUnit || "-", }, { key: "communicationType", label: "Comunicare", defaultVisible: false, render: (a) => a.communicationType || "-", className: "text-xs", }, { key: "actorName", label: "Actor curent", defaultVisible: false, render: (a) => a.actorName || "-", }, { key: "applicationPk", label: "Application PK", defaultVisible: false, render: (a) => String(a.applicationPk ?? "-"), className: "font-mono text-xs", }, ]; /* ================================================================== */ /* RGI Helpers */ /* ================================================================== */ function removeDiacritics(str: string): string { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function matchesSearch(text: string, query: string): boolean { return removeDiacritics(text.toLowerCase()).includes( removeDiacritics(query.toLowerCase()), ); } function sanitize(raw: string): string { return raw .replace(/[ăâ]/g, "a") .replace(/[ĂÂ]/g, "A") .replace(/[îÎ]/g, "i") .replace(/[țȚ]/g, "t") .replace(/[șȘ]/g, "s") .replace(/[^a-zA-Z0-9._-]/g, "_") .replace(/_+/g, "_") .replace(/^_|_$/g, ""); } /* ================================================================== */ /* Issued Documents Panel */ /* ================================================================== */ function IssuedDocsPanel({ applicationPk, workspaceId, appNo, }: { applicationPk: number; workspaceId: number; appNo: number; }) { const [docs, setDocs] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [blockedDocPk, setBlockedDocPk] = useState(null); const [downloadingAll, setDownloadingAll] = useState(false); const [downloadProgress, setDownloadProgress] = useState(""); const blockedTimerRef = useRef | null>(null); useEffect(() => { let cancelled = false; void (async () => { try { const res = await fetch( `/api/eterra/rgi/issued-docs?applicationId=${applicationPk}&workspaceId=${workspaceId}`, ); const data = await res.json(); const items: IssuedDoc[] = Array.isArray(data) ? data : data?.content ?? data?.data ?? data?.list ?? []; if (!cancelled) setDocs(items); } catch { if (!cancelled) setError("Eroare la incarcarea documentelor"); } if (!cancelled) setLoading(false); })(); return () => { cancelled = true; }; }, [applicationPk, workspaceId]); useEffect(() => { return () => { if (blockedTimerRef.current) clearTimeout(blockedTimerRef.current); }; }, []); const handleDownloadAll = useCallback(async () => { if (!docs || docs.length === 0 || downloadingAll) return; setDownloadingAll(true); let downloaded = 0; let blocked = 0; const typeCounts: Record = {}; for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1; const typeIdx: Record = {}; for (const doc of docs) { const docName = sanitize(doc.docType || doc.documentTypeCode || "Document"); const ext = (doc.fileExtension || "pdf").toLowerCase(); const typeKey = doc.docType || "Document"; typeIdx[typeKey] = (typeIdx[typeKey] || 0) + 1; const suffix = (typeCounts[typeKey] ?? 0) > 1 ? `_${typeIdx[typeKey]}` : ""; const filename = `${docName}_${appNo}${suffix}.${ext}`; setDownloadProgress(`${downloaded + blocked + 1}/${docs.length}: ${doc.docType || "Document"}...`); const url = `/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || workspaceId}` + `&applicationId=${doc.applicationId || applicationPk}` + `&documentPk=${doc.documentPk}` + `&documentTypeId=${doc.documentTypeId}` + `&docType=${encodeURIComponent(doc.docType || "")}` + `&appNo=${appNo}`; try { const res = await fetch(url); const ct = res.headers.get("content-type") || ""; if (ct.includes("application/json")) { blocked++; continue; } const blob = await res.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); URL.revokeObjectURL(a.href); document.body.removeChild(a); downloaded++; await new Promise((r) => setTimeout(r, 300)); } catch { blocked++; } } setDownloadProgress( blocked > 0 ? `${downloaded} descarcat${downloaded !== 1 ? "e" : ""}, ${blocked} indisponibil${blocked !== 1 ? "e" : ""}` : `${downloaded} document${downloaded !== 1 ? "e" : ""} descarcat${downloaded !== 1 ? "e" : ""}`, ); setDownloadingAll(false); setTimeout(() => setDownloadProgress(""), 5000); }, [docs, downloadingAll, workspaceId, applicationPk, appNo]); const handleDownload = useCallback( async (doc: IssuedDoc, e: React.MouseEvent) => { e.stopPropagation(); const url = `/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || workspaceId}` + `&applicationId=${doc.applicationId || applicationPk}` + `&documentPk=${doc.documentPk}` + `&documentTypeId=${doc.documentTypeId}` + `&docType=${encodeURIComponent(doc.docType || doc.documentTypeCode || "Document")}` + `&appNo=${appNo}`; try { const res = await fetch(url); const contentType = res.headers.get("content-type") || ""; if (contentType.includes("application/json")) { const json = await res.json(); if (json.blocked || json.error) { setBlockedDocPk(doc.documentPk); if (blockedTimerRef.current) clearTimeout(blockedTimerRef.current); blockedTimerRef.current = setTimeout(() => setBlockedDocPk(null), 5000); return; } } const blob = await res.blob(); const disposition = res.headers.get("content-disposition") || ""; let filename = `document_${doc.documentPk}.pdf`; const match = disposition.match(/filename="?([^";\n]+)"?/); if (match) { const decoded = match[1]; if (decoded) filename = decodeURIComponent(decoded); } const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); URL.revokeObjectURL(a.href); document.body.removeChild(a); } catch { setBlockedDocPk(doc.documentPk); if (blockedTimerRef.current) clearTimeout(blockedTimerRef.current); blockedTimerRef.current = setTimeout(() => setBlockedDocPk(null), 5000); } }, [workspaceId, applicationPk, appNo], ); if (loading) { return (
Se incarca documentele...
); } if (error) { return

{error}

; } if (!docs || docs.length === 0) { return (

Niciun document eliberat.

); } return (

{docs.length} document{docs.length > 1 ? "e" : ""} eliberat {docs.length > 1 ? "e" : ""}

{downloadProgress && ( {downloadProgress} )}
{docs.map((doc, i) => (

{doc.docType || doc.documentTypeCode || "Document"}

{fmtTs(doc.startDate || doc.lastUpdatedDtm)} .{(doc.fileExtension || "PDF").toLowerCase()} {doc.digitallySigned === 1 && ( semnat )} {doc.identifierDetails && ( {doc.identifierDetails} )}
{blockedDocPk === doc.documentPk && (
Documentul nu este inca disponibil pentru descarcare din eTerra
)}
))}
); } /* ================================================================== */ /* RGI Content Tab */ /* ================================================================== */ function RgiContent() { const [countyId, setCountyId] = useState(127); const orgUnitId = countyId * 1000 + 2; const [year, setYear] = useState("2026"); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [applications, setApplications] = useState([]); const [totalCount, setTotalCount] = useState(0); const [expandedPk, setExpandedPk] = useState(null); const [showColumnPicker, setShowColumnPicker] = useState(false); const [downloadingAppPk, setDownloadingAppPk] = useState(null); const [filterMode, setFilterMode] = useState<"all" | "solved" | "confirmed">("solved"); const [searchQuery, setSearchQuery] = useState(""); const [sortState, setSortState] = useState(null); const [visibleCols, setVisibleCols] = useState>( () => new Set(ALL_COLUMNS.filter((c) => c.defaultVisible).map((c) => c.key)), ); const toggleColumn = (key: string) => { setVisibleCols((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }; const columns = useMemo( () => ALL_COLUMNS.filter((c) => visibleCols.has(c.key)), [visibleCols], ); const downloadAllForApp = useCallback(async (app: App) => { if (downloadingAppPk) return; setDownloadingAppPk(app.applicationPk); try { const res = await fetch( `/api/eterra/rgi/issued-docs?applicationId=${app.applicationPk}&workspaceId=${app.workspaceId}`, ); const data = await res.json(); const docs: IssuedDoc[] = Array.isArray(data) ? data : data?.content ?? data?.data ?? data?.list ?? []; if (docs.length === 0) { setDownloadingAppPk(null); return; } const typeCounts: Record = {}; for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1; const typeIdx: Record = {}; for (const doc of docs) { const docName = sanitize(doc.docType || doc.documentTypeCode || "Document"); const ext = (doc.fileExtension || "pdf").toLowerCase(); const typeKey = doc.docType || "Document"; typeIdx[typeKey] = (typeIdx[typeKey] || 0) + 1; const suffix = (typeCounts[typeKey] ?? 0) > 1 ? `_${typeIdx[typeKey]}` : ""; const filename = `${docName}_${app.appNo}${suffix}.${ext}`; const url = `/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || app.workspaceId}` + `&applicationId=${doc.applicationId || app.applicationPk}` + `&documentPk=${doc.documentPk}` + `&documentTypeId=${doc.documentTypeId}` + `&docType=${encodeURIComponent(doc.docType || "")}` + `&appNo=${app.appNo}`; try { const r = await fetch(url); const ct = r.headers.get("content-type") || ""; if (ct.includes("application/json")) continue; const blob = await r.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); URL.revokeObjectURL(a.href); document.body.removeChild(a); await new Promise((resolve) => setTimeout(resolve, 300)); } catch { // skip } } } catch { // silent } setDownloadingAppPk(null); }, [downloadingAppPk]); const handleSort = useCallback( (key: string) => { setSortState((prev) => { if (prev && prev.key === key) { return prev.dir === "asc" ? { key, dir: "desc" } : null; } return { key, dir: "asc" }; }); }, [], ); const loadApplications = useCallback(async () => { setLoading(true); setError(""); setApplications([]); setExpandedPk(null); try { const res = await fetch("/api/eterra/rgi/applications", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ workspaceId: countyId, orgUnitId, year, page: 0, nrElements: 200, }), }); const data = await res.json(); if (data.error) { setError(data.error); return; } const items: App[] = Array.isArray(data) ? data : data?.content ?? data?.data ?? data?.list ?? []; setApplications(items); setTotalCount( typeof data?.totalElements === "number" ? data.totalElements : items.length, ); } catch { setError("Eroare de retea. Verifica conexiunea la eTerra."); } setLoading(false); }, [countyId, orgUnitId, year]); const processed = useMemo(() => { let result = applications; if (filterMode === "solved") { result = result.filter((a) => a.hasSolution === 1); } else if (filterMode === "confirmed") { result = result.filter((a) => a.stateCode === "CONFIRMED"); } if (searchQuery.trim()) { const q = searchQuery.trim(); result = result.filter((app) => columns.some((col) => matchesSearch(col.render(app), q)), ); } if (sortState) { const col = ALL_COLUMNS.find((c) => c.key === sortState.key); if (col) { const dir = sortState.dir === "asc" ? 1 : -1; const dateKeys = new Set(["dueDate", "appDate"]); result = [...result].sort((a, b) => { if (dateKeys.has(sortState.key)) { const va = (a[sortState.key] as number) || 0; const vb = (b[sortState.key] as number) || 0; return (va - vb) * dir; } const va = col.render(a); const vb = col.render(b); const na = parseFloat(va); const nb = parseFloat(vb); if (!isNaN(na) && !isNaN(nb)) return (na - nb) * dir; return va.localeCompare(vb, "ro") * dir; }); } } return result; }, [applications, filterMode, searchQuery, columns, sortState]); const SortIcon = ({ colKey }: { colKey: string }) => { if (!sortState || sortState.key !== colKey) { return ; } if (sortState.dir === "asc") { return ; } return ; }; return (

Documente Eliberate eTerra

Lucrari depuse cu documente eliberate

setYear(e.target.value)} className="w-20" />
{showColumnPicker && (
{ALL_COLUMNS.map((col) => ( ))}
)}
{( [ { id: "solved" as const, label: "Solutionate", desc: "lucrari cu solutie" }, { id: "confirmed" as const, label: "Confirmate", desc: "solutie confirmata" }, { id: "all" as const, label: "Toate", desc: "" }, ] as const ).map((opt) => ( ))}
{applications.length > 0 && (
setSearchQuery(e.target.value)} placeholder="Cauta in rezultate..." className="pl-8 h-8 text-xs" />
{processed.length} din {applications.length} lucrari {totalCount > applications.length && ` (${totalCount} total)`}
)}
{error && ( {error} )} {loading && (

Se incarca lucrarile din eTerra RGI...

)} {/* Mobile card view */} {!loading && processed.length > 0 && (
{processed.map((app) => { const pk = app.applicationPk; const isExpanded = expandedPk === pk; const solved = app.hasSolution === 1; return (
setExpandedPk(isExpanded ? null : pk)}>
{app.appNo} {app.statusName || app.stateCode || "-"}

{app.requester || app.deponent || "-"}

Termen: {fmtTs(app.dueDate)} {app.resolutionName || "-"} {app.uat && {app.uat}}
{isExpanded ? : }
{isExpanded && (
)}
); })}
)} {/* Desktop table view */} {!loading && processed.length > 0 && (
{columns.map((col) => ( ))} {processed.map((app) => { const pk = app.applicationPk; const isExpanded = expandedPk === pk; const solved = app.hasSolution === 1; return ( setExpandedPk(isExpanded ? null : pk)} > {columns.map((col) => ( ))} {isExpanded && ( )} ); })}
handleSort(col.key)} > {col.label}

Nr. {app.appNo} — click descarca toate

{app.applicationObject || "-"}

Status: {app.statusName || app.stateCode}

Rezolutie: {app.resolutionName || "-"}

Termen: {fmtTs(app.dueDate)}

{app.identifiers && (

{app.identifiers}

)}
{col.key === "statusName" ? ( {col.render(app)} ) : col.key === "resolutionName" ? ( {col.render(app)} ) : ( col.render(app) )} {isExpanded ? ( ) : ( )}
)} {!loading && applications.length > 0 && processed.length === 0 && (

Nicio lucrare gasita pentru filtrul selectat.

Schimba filtrul sau termenul de cautare.

)} {!loading && applications.length === 0 && !error && (

Apasa "Incarca lucrari" pentru a incepe.

)}
); } /* ================================================================== */ /* Simplified Basemap Switcher (3 options only) */ /* ================================================================== */ const PORTAL_BASEMAPS: { id: BasemapId; label: string; icon: typeof MapIcon }[] = [ { id: "liberty", label: "Harta", icon: MapIcon }, { id: "dark", label: "Noapte", icon: Moon }, { id: "google", label: "Google", icon: Satellite }, ]; function PortalBasemapSwitcher({ value, onChange }: { value: BasemapId; onChange: (id: BasemapId) => void }) { return (
{PORTAL_BASEMAPS.map((b) => { const Icon = b.icon; const active = value === b.id; return ( ); })}
); } /* ================================================================== */ /* UAT type for selector */ /* ================================================================== */ type UatItem = { siruta: string; name: string; county: string; localFeatures: number; }; /* ================================================================== */ /* Map layer IDs — must match map-viewer.tsx */ /* ================================================================== */ const BASE_LAYERS = [ "l-terenuri-fill", "l-terenuri-line", "l-terenuri-label", "l-cladiri-fill", "l-cladiri-line", ]; /* ================================================================== */ /* Harta (Map) Tab */ /* ================================================================== */ type MapLike = { getLayer(id: string): unknown; getSource(id: string): unknown; setFilter(id: string, filter: unknown[] | null): void; setLayoutProperty(id: string, prop: string, value: unknown): void; fitBounds( bounds: [number, number, number, number], opts?: Record, ): void; isStyleLoaded(): boolean; }; function asMap(handle: MapViewerHandle | null): MapLike | null { const m = handle?.getMap(); return m ? (m as unknown as MapLike) : null; } function HartaContent() { const mapHandleRef = useRef(null); const [basemap, setBasemap] = useState("liberty"); const [clickedFeature, setClickedFeature] = useState(null); const [selectionMode, setSelectionMode] = useState("off"); const [selectedFeatures, setSelectedFeatures] = useState([]); const [mapReady, setMapReady] = useState(false); // UAT selector state const [uats, setUats] = useState([]); const [uatsLoading, setUatsLoading] = useState(false); const [selectedSiruta, setSelectedSiruta] = useState(""); const [uatSearch, setUatSearch] = useState(""); const [showUatDropdown, setShowUatDropdown] = useState(false); const uatInputRef = useRef(null); // Sync state const [syncing, setSyncing] = useState(false); const [syncMsg, setSyncMsg] = useState(""); // Bounds state const boundsRef = useRef<[number, number, number, number] | null>(null); const appliedSirutaRef = useRef(""); const boundsFittedForSirutaRef = useRef(""); const prevBoundsSirutaRef = useRef(""); // Layer visibility: show terenuri + cladiri const [layerVisibility] = useState({ terenuri: true, cladiri: true, administrativ: false, }); // Load UATs on mount useEffect(() => { setUatsLoading(true); fetch("/api/eterra/uats") .then((r) => r.ok ? r.json() : Promise.reject()) .then((data: { uats: UatItem[] }) => { setUats(data.uats ?? []); }) .catch(() => {}) .finally(() => setUatsLoading(false)); }, []); // Selected UAT info const selectedUat = useMemo( () => uats.find((u) => u.siruta === selectedSiruta), [uats, selectedSiruta], ); // Filtered UATs for dropdown const filteredUats = useMemo(() => { if (!uatSearch.trim()) return uats.slice(0, 50); const q = removeDiacritics(uatSearch.toLowerCase()); return uats.filter((u) => removeDiacritics(u.name.toLowerCase()).includes(q) || u.siruta.includes(uatSearch) || removeDiacritics((u.county ?? "").toLowerCase()).includes(q) ).slice(0, 50); }, [uats, uatSearch]); // Detect map ready useEffect(() => { if (!selectedSiruta) return; const check = setInterval(() => { const map = asMap(mapHandleRef.current); if (!map || !map.isStyleLoaded()) { if (mapReady) { setMapReady(false); appliedSirutaRef.current = ""; } return; } if (!mapReady) { setMapReady(true); } }, 300); return () => clearInterval(check); }, [selectedSiruta, mapReady]); // Fetch UAT bounds when selected useEffect(() => { if (!selectedSiruta) return; if (prevBoundsSirutaRef.current === selectedSiruta) return; prevBoundsSirutaRef.current = selectedSiruta; fetch(`/api/geoportal/uat-bounds?siruta=${selectedSiruta}`) .then((r) => (r.ok ? r.json() : null)) .then((data: { bounds?: [[number, number], [number, number]] } | null) => { if (data?.bounds) { const [[minLng, minLat], [maxLng, maxLat]] = data.bounds; boundsRef.current = [minLng, minLat, maxLng, maxLat]; boundsFittedForSirutaRef.current = ""; const map = asMap(mapHandleRef.current); if (map) { map.fitBounds([minLng, minLat, maxLng, maxLat], { padding: 40, duration: 1500 }); boundsFittedForSirutaRef.current = selectedSiruta; } } }) .catch(() => {}); }, [selectedSiruta]); // Hide basemap admin boundaries + our UAT layers const cleanedBasemapRef = useRef(false); useEffect(() => { if (!mapReady) { cleanedBasemapRef.current = false; return; } if (cleanedBasemapRef.current) return; const map = asMap(mapHandleRef.current); if (!map) return; cleanedBasemapRef.current = true; try { const style = (map as unknown as { getStyle(): { layers?: { id: string }[] } }).getStyle(); if (style?.layers) { for (const layer of style.layers) { const id = layer.id.toLowerCase(); if (id.includes("boundary") || id.includes("admin") || (id.includes("place") && !id.includes("place-city") && !id.includes("place-town"))) { map.setLayoutProperty(layer.id, "visibility", "none"); } } } } catch { /* noop */ } const uatLayers = [ "l-uats-z0-line", "l-uats-z5-fill", "l-uats-z5-line", "l-uats-z8-fill", "l-uats-z8-line", "l-uats-z8-label", "l-uats-z12-fill", "l-uats-z12-line", "l-uats-z12-label", ]; for (const lid of uatLayers) { try { if (map.getLayer(lid)) map.setLayoutProperty(lid, "visibility", "none"); } catch { /* noop */ } } }, [mapReady]); // When map becomes ready, fitBounds ONCE per siruta useEffect(() => { if (!mapReady || !boundsRef.current || !selectedSiruta) return; if (boundsFittedForSirutaRef.current === selectedSiruta) return; const map = asMap(mapHandleRef.current); if (!map) return; map.fitBounds(boundsRef.current, { padding: 40, duration: 1500 }); boundsFittedForSirutaRef.current = selectedSiruta; }, [mapReady, selectedSiruta]); // Apply siruta filter to layers useEffect(() => { if (!mapReady || !selectedSiruta) return; if (appliedSirutaRef.current === selectedSiruta) return; const map = asMap(mapHandleRef.current); if (!map) return; appliedSirutaRef.current = selectedSiruta; const filter = ["==", ["get", "siruta"], selectedSiruta]; for (const layerId of BASE_LAYERS) { try { if (map.getLayer(layerId)) map.setFilter(layerId, filter); } catch { /* noop */ } } }, [mapReady, selectedSiruta]); const handleFeatureClick = useCallback((feature: ClickedFeature | null) => { if (!feature || !feature.properties) { setClickedFeature(null); return; } setClickedFeature(feature); }, []); const handleSelectionModeChange = useCallback((mode: SelectionMode) => { if (mode === "off") { mapHandleRef.current?.clearSelection(); setSelectedFeatures([]); } setSelectionMode(mode); }, []); const handleSelectUat = (uat: UatItem) => { setSelectedSiruta(uat.siruta); setUatSearch(uat.name + (uat.county ? ` (${uat.county})` : "")); setShowUatDropdown(false); // Reset map state for new UAT appliedSirutaRef.current = ""; prevBoundsSirutaRef.current = ""; setMapReady(false); setClickedFeature(null); setSelectedFeatures([]); setSelectionMode("off"); setSyncMsg(""); }; const handleSync = async () => { if (!selectedSiruta || syncing) return; setSyncing(true); setSyncMsg("Se porneste sincronizarea..."); try { const res = await fetch("/api/eterra/sync-background", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siruta: selectedSiruta, mode: "base" }), }); const data = await res.json(); if (res.ok) { setSyncMsg(`Sincronizare pornita (job: ${data.jobId ?? "?"}). Datele vor aparea dupa finalizare.`); } else { setSyncMsg(data.error ?? "Eroare la pornirea sincronizarii"); } } catch { setSyncMsg("Eroare de retea la sincronizare"); } setSyncing(false); }; const hasNoData = selectedUat && selectedUat.localFeatures === 0; return (
{!selectedSiruta ? ( /* No UAT selected — show selector */

Selecteaza un UAT

Alege o unitate administrativ-teritoriala pentru a vizualiza parcele si cladiri pe harta.

{ setUatSearch(e.target.value); setShowUatDropdown(true); }} onFocus={() => setShowUatDropdown(true)} placeholder={uatsLoading ? "Se incarca UAT-urile..." : "Cauta UAT (nume, SIRUTA, judet)..."} className="pl-10" disabled={uatsLoading} /> {showUatDropdown && filteredUats.length > 0 && (
{filteredUats.map((uat) => ( ))}
)}
) : ( /* UAT selected — show map */ <> {hasNoData ? ( /* No data for this UAT */

Nu exista date GIS pentru {selectedUat?.name}

Acest UAT nu a fost inca sincronizat. Porneste sincronizarea pentru a descarca parcele si cladiri de la eTerra.

{syncMsg && (

{syncMsg}

)}
) : ( /* Map view */
{/* Top-left: UAT info + change button (responsive) */}

{selectedUat?.name}

{selectedUat?.county && `${selectedUat.county} - `}SIRUTA: {selectedSiruta} {selectedUat && selectedUat.localFeatures > 0 && ( {selectedUat.localFeatures.toLocaleString()} parcele )}

{/* Top-right: basemap switcher + simple feature info */}
{clickedFeature && selectionMode === "off" && (

{String(clickedFeature.properties.cadastral_ref ?? clickedFeature.properties.object_id ?? "Parcela")}

{(() => { const p = clickedFeature.properties; const sir = String(p.siruta ?? ""); const cad = String(p.cadastral_ref ?? ""); const area = Number(p.area_value ?? 0); return ( <> {sir && (
SIRUTA {sir}
)} {cad && (
Nr. cadastral {cad}
)} {area > 0 && (
Suprafata {area.toLocaleString("ro-RO")} mp
)} ); })()}
)}
{/* Bottom: selection toolbar — centered on mobile */}
{ mapHandleRef.current?.clearSelection(); setSelectedFeatures([]); }} />
)} )}
); } /* ================================================================== */ /* Main Portal Page */ /* ================================================================== */ export default function PortalPage() { return (
{/* Header */}

Portal

RGI Harta
); }