"use client"; import { useState, useCallback, useEffect } from "react"; import { Search, Loader2, Plus, FileDown, ClipboardCopy, Trash2, XCircle, Download, Archive, FileText, User, } from "lucide-react"; 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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/shared/components/ui/tooltip"; import { cn } from "@/shared/lib/utils"; import { EpayOrderButton } from "../epay-order-button"; import type { EpaySessionStatus } from "../epay-connect"; import type { SessionStatus, UatEntry, ParcelDetail, OwnerSearchResult, } from "../parcel-sync-types"; import { formatArea, formatShortDate } from "../parcel-sync-types"; /* ------------------------------------------------------------------ */ /* Props */ /* ------------------------------------------------------------------ */ type SearchTabProps = { siruta: string; workspacePk: number | null; sirutaValid: boolean; session: SessionStatus; selectedUat?: UatEntry; epayStatus: EpaySessionStatus; }; /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export function SearchTab({ siruta, workspacePk, sirutaValid, session, selectedUat, epayStatus, }: SearchTabProps) { /* ── Parcel search ────────────────────────────────────────── */ const [searchMode, setSearchMode] = useState<"cadastral" | "owner">( "cadastral", ); const [searchResults, setSearchResults] = useState([]); const [searchList, setSearchList] = useState([]); const [featuresSearch, setFeaturesSearch] = useState(""); const [loadingFeatures, setLoadingFeatures] = useState(false); const [searchError, setSearchError] = useState(""); /* ── Owner search ──────────────────────────────────────────── */ const [ownerSearch, setOwnerSearch] = useState(""); const [ownerResults, setOwnerResults] = useState([]); const [ownerLoading, setOwnerLoading] = useState(false); const [ownerError, setOwnerError] = useState(""); const [ownerNote, setOwnerNote] = useState(""); /* ── CF extract statuses ──────────────────────────────────── */ const [cfStatusMap, setCfStatusMap] = useState>({}); const [cfLatestIds, setCfLatestIds] = useState>({}); const [cfExpiryDates, setCfExpiryDates] = useState>( {}, ); const [cfStatusLoading, setCfStatusLoading] = useState(false); const [listCfOrdering, setListCfOrdering] = useState(false); const [listCfOrderResult, setListCfOrderResult] = useState(""); const [listCfDownloading, setListCfDownloading] = useState(false); /* ── Search handlers ──────────────────────────────────────── */ const handleSearch = useCallback(async () => { if (!siruta || !/^\d+$/.test(siruta)) return; if (!featuresSearch.trim()) { setSearchResults([]); setSearchError(""); return; } setLoadingFeatures(true); setSearchError(""); try { const res = await fetch("/api/eterra/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siruta, search: featuresSearch.trim(), ...(workspacePk ? { workspacePk } : {}), }), }); const data = (await res.json()) as { results?: ParcelDetail[]; total?: number; error?: string; }; if (data.error) { setSearchResults([]); setSearchError(data.error); } else { setSearchResults(data.results ?? []); setSearchError(""); } } catch { setSearchError("Eroare de rețea."); } setLoadingFeatures(false); }, [siruta, featuresSearch, workspacePk]); const handleSearchKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); void handleSearch(); } }, [handleSearch], ); /* ── Owner search ──────────────────────────────────────────── */ const handleOwnerSearch = useCallback(async () => { if (!siruta || !/^\d+$/.test(siruta)) return; if (!ownerSearch.trim() || ownerSearch.trim().length < 2) { setOwnerError("Minim 2 caractere."); return; } setOwnerLoading(true); setOwnerError(""); setOwnerNote(""); try { const res = await fetch("/api/eterra/search-owner", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siruta, ownerName: ownerSearch.trim(), ...(workspacePk ? { workspacePk } : {}), }), }); const data = (await res.json()) as { results?: OwnerSearchResult[]; total?: number; dbSearched?: boolean; eterraSearched?: boolean; eterraNote?: string; error?: string; }; if (data.error) { setOwnerResults([]); setOwnerError(data.error); } else { setOwnerResults(data.results ?? []); const notes: string[] = []; if (data.dbSearched) notes.push("DB local"); if (data.eterraSearched) notes.push("eTerra API"); if (data.eterraNote) notes.push(data.eterraNote); setOwnerNote( notes.length > 0 ? `Surse: ${notes.join(" + ")}${data.total ? ` · ${data.total} rezultate` : ""}` : "", ); } } catch { setOwnerError("Eroare de rețea."); } setOwnerLoading(false); }, [siruta, ownerSearch, workspacePk]); const handleOwnerKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); void handleOwnerSearch(); } }, [handleOwnerSearch], ); const ownerResultToParcelDetail = useCallback( (r: OwnerSearchResult): ParcelDetail => ({ nrCad: r.nrCad, nrCF: r.nrCF, nrCFVechi: "", nrTopo: "", intravilan: r.intravilan, categorieFolosinta: r.categorieFolosinta, adresa: r.adresa, proprietari: r.proprietari || r.proprietariVechi, proprietariActuali: r.proprietari, proprietariVechi: r.proprietariVechi, suprafata: typeof r.suprafata === "number" ? r.suprafata : null, solicitant: "", immovablePk: r.immovablePk, }), [], ); /* ── List management ───────────────────────────────────────── */ const addToList = useCallback((item: ParcelDetail) => { setSearchList((prev) => { if ( prev.some( (p) => p.nrCad === item.nrCad && p.immovablePk === item.immovablePk, ) ) return prev; return [...prev, item]; }); }, []); const removeFromList = useCallback((nrCad: string) => { setSearchList((prev) => prev.filter((p) => p.nrCad !== nrCad)); }, []); const csvEscape = useCallback((val: string | number | null | undefined) => { const s = val != null ? String(val) : ""; return `"${s.replace(/"/g, '""')}"`; }, []); const downloadCSV = useCallback(() => { const items = searchList.length > 0 ? searchList : searchResults; if (items.length === 0) return; const headers = [ "NR_CAD", "NR_CF", "NR_CF_VECHI", "NR_TOPO", "SUPRAFATA", "INTRAVILAN", "CATEGORIE_FOLOSINTA", "ADRESA", "PROPRIETARI_ACTUALI", "PROPRIETARI_VECHI", "SOLICITANT", ]; const rows = items.map((p) => [ csvEscape(p.nrCad), csvEscape(p.nrCF), csvEscape(p.nrCFVechi), csvEscape(p.nrTopo), csvEscape(p.suprafata), csvEscape(p.intravilan), csvEscape(p.categorieFolosinta), csvEscape(p.adresa), csvEscape(p.proprietariActuali ?? p.proprietari), csvEscape(p.proprietariVechi), csvEscape(p.solicitant), ]); const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n"); const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `parcele_${siruta}_${Date.now()}.csv`; a.click(); URL.revokeObjectURL(url); }, [searchList, searchResults, siruta, csvEscape]); /* ── CF extract status fetching ────────────────────────────── */ const fetchCfStatuses = useCallback(async (cadastralNumbers: string[]) => { if (cadastralNumbers.length === 0) return; setCfStatusLoading(true); try { const nrs = cadastralNumbers.join(","); const res = await fetch( `/api/ancpi/orders?nrCadastral=${encodeURIComponent(nrs)}&limit=1`, ); const data = (await res.json()) as { statusMap?: Record; latestById?: Record< string, { id: string; expiresAt: string | null } >; }; if (data.statusMap) { setCfStatusMap((prev) => ({ ...prev, ...data.statusMap })); } if (data.latestById) { const idMap: Record = {}; const expiryMap: Record = {}; for (const [nr, rec] of Object.entries(data.latestById)) { if (rec && typeof rec === "object" && "id" in rec) { idMap[nr] = (rec as { id: string }).id; const expires = (rec as { expiresAt: string | null }).expiresAt; if (expires) { expiryMap[nr] = expires; } } } setCfLatestIds((prev) => ({ ...prev, ...idMap })); setCfExpiryDates((prev) => ({ ...prev, ...expiryMap })); } } catch { /* silent */ } finally { setCfStatusLoading(false); } }, []); const refreshCfStatuses = useCallback(() => { const allNrs = new Set(); for (const r of searchResults) { if (r.nrCad) allNrs.add(r.nrCad); } for (const p of searchList) { if (p.nrCad) allNrs.add(p.nrCad); } if (allNrs.size > 0) { void fetchCfStatuses(Array.from(allNrs)); } }, [searchResults, searchList, fetchCfStatuses]); // Auto-fetch CF statuses when search results change useEffect(() => { const nrs = searchResults.map((r) => r.nrCad).filter(Boolean); if (nrs.length > 0) void fetchCfStatuses(nrs); }, [searchResults, fetchCfStatuses]); // Auto-fetch CF statuses when list changes useEffect(() => { const nrs = searchList.map((p) => p.nrCad).filter(Boolean); if (nrs.length > 0) void fetchCfStatuses(nrs); }, [searchList, fetchCfStatuses]); /* ── List CF ordering + ZIP download ──────────────────────── */ const handleListCfOrder = useCallback(async () => { if (!siruta || searchList.length === 0 || listCfOrdering) return; const toOrder: typeof searchList = []; const toReorder: typeof searchList = []; const alreadyValid: typeof searchList = []; for (const p of searchList) { const status = cfStatusMap[p.nrCad]; if (status === "valid") { alreadyValid.push(p); } else if (status === "expired") { toReorder.push(p); } else { if (status !== "processing") toOrder.push(p); } } const newCount = toOrder.length; const updateCount = toReorder.length; const existingCount = alreadyValid.length; if (newCount === 0 && updateCount === 0) { setListCfOrderResult( `Toate cele ${existingCount} extrase sunt valide.`, ); return; } const msg = [ newCount > 0 ? `${newCount} extrase noi` : null, updateCount > 0 ? `${updateCount} actualizari` : null, existingCount > 0 ? `${existingCount} existente (skip)` : null, ] .filter(Boolean) .join(", "); if (!window.confirm(`Comanda extrase CF:\n${msg}\n\nContinui?`)) return; setListCfOrdering(true); setListCfOrderResult(""); try { const allToProcess = [...toOrder, ...toReorder]; const parcels = allToProcess.map((p) => ({ nrCadastral: p.nrCad, siruta, judetIndex: 0, judetName: selectedUat?.county ?? "", uatId: 0, uatName: selectedUat?.name ?? "", })); const res = await fetch("/api/ancpi/order", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parcels }), }); const data = (await res.json()) as { orders?: unknown[]; error?: string; }; if (!res.ok || data.error) { setListCfOrderResult( `Eroare: ${data.error ?? "Eroare la comanda"}`, ); } else { const count = data.orders?.length ?? allToProcess.length; setListCfOrderResult( `${count} extras${count > 1 ? "e" : ""} CF trimis${count > 1 ? "e" : ""} la procesare.`, ); const pollInterval = setInterval(() => { void refreshCfStatuses(); }, 10_000); setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000); } } catch { setListCfOrderResult("Eroare retea."); } finally { setListCfOrdering(false); } }, [ siruta, searchList, listCfOrdering, cfStatusMap, selectedUat, refreshCfStatuses, ]); const handleListCfDownloadZip = useCallback(async () => { if (searchList.length === 0 || listCfDownloading) return; const ids: string[] = []; for (const p of searchList) { const status = cfStatusMap[p.nrCad]; const extractId = cfLatestIds[p.nrCad]; if (status === "valid" && extractId) ids.push(extractId); } if (ids.length === 0) { setListCfOrderResult("Niciun extras CF valid in lista."); return; } setListCfDownloading(true); try { const res = await fetch(`/api/ancpi/download-zip?ids=${ids.join(",")}`); if (!res.ok) throw new Error("Eroare descarcare ZIP"); const blob = await res.blob(); const cd = res.headers.get("Content-Disposition") ?? ""; const match = /filename="?([^"]+)"?/.exec(cd); const filename = match?.[1] ? decodeURIComponent(match[1]) : "Extrase_CF_lista.zip"; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch { setListCfOrderResult("Eroare la descarcarea ZIP."); } finally { setListCfDownloading(false); } }, [searchList, cfStatusMap, cfLatestIds, listCfDownloading]); /* ── CF status badge helper ────────────────────────────────── */ const CfStatusBadge = useCallback( ({ nrCad, immovablePk, }: { nrCad: string; immovablePk?: number | string | null; }) => { if (!immovablePk || !sirutaValid) return null; const cfStatus = cfStatusMap[nrCad]; const extractId = cfLatestIds[nrCad]; const cfExpiry = cfExpiryDates[nrCad]; if (cfStatus === "valid") { return (
Extras CF {cfExpiry ? `Valid pana la ${formatShortDate(cfExpiry)}` : "Extras CF valid"} {extractId && ( Descarca extras CF )}
); } if (cfStatus === "expired") { return (
Expirat {cfExpiry ? `Expirat pe ${formatShortDate(cfExpiry)}` : "Extras CF expirat"}
); } if (cfStatus === "processing") { return ( Se proceseaza... Comanda in curs de procesare ); } // "none" or unknown return ( ); }, [cfStatusMap, cfLatestIds, cfExpiryDates, sirutaValid, siruta, selectedUat], ); /* ── Render ─────────────────────────────────────────────────── */ if (!sirutaValid) { return (

Selectează un UAT mai sus pentru a căuta parcele.

); } return ( <> {/* Search input — mode toggle + input */} {/* Mode toggle */}
{/* Cadastral search input */} {searchMode === "cadastral" && (
setFeaturesSearch(e.target.value)} onKeyDown={handleSearchKeyDown} disabled={!session.connected} />
{!session.connected && (

Necesită conexiune eTerra. Folosește modul Proprietar pentru a căuta offline în DB.

)}
)} {/* Owner search input */} {searchMode === "owner" && (
setOwnerSearch(e.target.value)} onKeyDown={handleOwnerKeyDown} />
)} {searchMode === "cadastral" && searchError && (

{searchError}

)} {searchMode === "owner" && ownerError && (

{ownerError}

)} {searchMode === "owner" && ownerNote && (

{ownerNote}

)}
{/* ─── Cadastral search results ────────────── */} {searchMode === "cadastral" && ( <> {loadingFeatures && searchResults.length === 0 && (

Se caută în eTerra...

Prima căutare pe un UAT nou poate dura ~10-30s (se încarcă lista de județe).

)} {searchResults.length > 0 && ( <> {/* Action bar */}
{searchResults.length} rezultat {searchResults.length > 1 ? "e" : ""} {searchList.length > 0 && ( · {searchList.length} în listă )}
{searchResults.length > 0 && ( )}
{/* Detail cards */}
{searchResults.map((p, idx) => (

Nr. Cad. {p.nrCad}

{!p.immovablePk && (

Parcela nu a fost găsită în eTerra.

)}
{p.immovablePk && (
Nr. CF {p.nrCF || "—"}
{p.nrCFVechi && (
CF vechi {p.nrCFVechi}
)}
Nr. Topo {p.nrTopo || "—"}
Suprafață {p.suprafata != null ? formatArea(p.suprafata) : "—"}
Intravilan {p.intravilan || "—"}
{p.categorieFolosinta && (
Categorii folosință {p.categorieFolosinta}
)} {p.adresa && (
Adresă {p.adresa}
)} {(p.proprietariActuali || p.proprietariVechi) && (
{p.proprietariActuali && (
Proprietari actuali {p.proprietariActuali}
)} {p.proprietariVechi && (
Proprietari anteriori {p.proprietariVechi}
)} {!p.proprietariActuali && !p.proprietariVechi && p.proprietari && (
Proprietari {p.proprietari}
)}
)} {p.solicitant && (
Solicitant {p.solicitant}
)}
)}
))}
)} {/* Empty state */} {searchMode === "cadastral" && searchResults.length === 0 && !loadingFeatures && !searchError && (

Introdu un număr cadastral și apasă Caută.

Poți căuta mai multe parcele simultan, separate prin virgulă.

)} )} {/* ─── Owner search results ────────────────── */} {searchMode === "owner" && ( <> {ownerLoading && ownerResults.length === 0 && (

Se caută proprietar...

Caută mai întâi în DB local (date îmbogățite), apoi pe eTerra.

)} {ownerResults.length > 0 && ( <>
{ownerResults.length} rezultat {ownerResults.length > 1 ? "e" : ""} pentru " {ownerSearch}"
{ownerResults.map((r, idx) => (

Nr. Cad. {r.nrCad}

{r.source === "db" ? "din baza de date" : "eTerra online"}
{r.nrCF && (
Nr. CF {r.nrCF}
)} {r.suprafata && (
Suprafață {typeof r.suprafata === "number" ? formatArea(r.suprafata) : `${r.suprafata} mp`}
)} {r.intravilan && (
Intravilan {r.intravilan}
)} {r.categorieFolosinta && (
Categorii folosință {r.categorieFolosinta}
)} {r.adresa && (
Adresă {r.adresa}
)} {r.proprietari && (
Proprietari actuali {r.proprietari}
)} {r.proprietariVechi && (
Proprietari anteriori {r.proprietariVechi}
)}
))}
)} {ownerResults.length === 0 && !ownerLoading && !ownerError && (

Introdu numele proprietarului și apasă Caută.

Caută în datele îmbogățite (DB local) și pe eTerra.
Pentru rezultate complete, lansează "Sync fundal — Magic" în tab-ul Export.

)} )} {/* Saved list */} {searchList.length > 0 && (

Lista mea ({searchList.length} parcele)

{/* Download all valid CF extracts as ZIP */} {searchList.some( (p) => cfStatusMap[p.nrCad] === "valid", ) && (() => { const validCount = searchList.filter( (p) => cfStatusMap[p.nrCad] === "valid", ).length; return ( {`Descarca ZIP cu ${validCount} extrase valide din lista`} ); })()} {/* Order CF extracts for list */} {epayStatus.connected && (() => { const newCount = searchList.filter((p) => { const s = cfStatusMap[p.nrCad]; return ( s !== "valid" && s !== "expired" && s !== "processing" ); }).length; const updateCount = searchList.filter( (p) => cfStatusMap[p.nrCad] === "expired", ).length; const totalCredits = newCount + updateCount; const validCount = searchList.filter( (p) => cfStatusMap[p.nrCad] === "valid", ).length; return ( {`Comanda ${newCount} extrase noi + ${updateCount} actualizari = ${totalCredits} credite. ${validCount} existente valide raman.`} ); })()}
{/* Order result message */} {listCfOrderResult && (

{listCfOrderResult}

)}
{searchList.map((p, idx) => { const cfStatus = cfStatusMap[p.nrCad]; const cfExpiry = cfExpiryDates[p.nrCad]; return ( ); })}
# Nr. Cad Nr. CF Suprafata Proprietari Extras CF
{idx + 1} {p.nrCad} {p.nrCF || "—"} {p.suprafata != null ? formatArea(p.suprafata) : "—"} {p.proprietari || "—"} {cfStatus === "valid" ? ( Valid ) : cfStatus === "expired" ? ( Expirat ) : cfStatus === "processing" ? ( Procesare ) : ( Lipsa )} {cfStatus === "valid" ? cfExpiry ? `Extras CF valid pana la ${formatShortDate(cfExpiry)}` : "Extras CF valid" : cfStatus === "expired" ? cfExpiry ? `Extras CF expirat pe ${formatShortDate(cfExpiry)}. Va fi actualizat automat la 'Scoate Extrase CF'.` : "Extras CF expirat. Va fi actualizat automat la 'Scoate Extrase CF'." : cfStatus === "processing" ? "Comanda in curs de procesare" : "Nu exista extras CF. Apasa 'Scoate Extrase CF' pentru a comanda."}
)} ); }