"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { useSession } from "next-auth/react"; import { FileText, Download, RefreshCw, Loader2, Clock, Archive, CreditCard, Search, } from "lucide-react"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; 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 type { EpaySessionStatus } from "./epay-connect"; import { cfDownloadUrl, fetchCfOrdersList, placeCfOrder, type CfExtractRecord, } from "./cf-api-base"; type GisUatResult = { siruta: string; name: string; county: string | null; }; /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ function formatDate(iso?: string | null) { if (!iso) return "—"; return new Date(iso).toLocaleDateString("ro-RO", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); } function formatShortDate(iso?: string | null) { if (!iso) return "—"; return new Date(iso).toLocaleDateString("ro-RO", { day: "2-digit", month: "2-digit", year: "numeric", }); } function isExpired(expiresAt: string | null): boolean { if (!expiresAt) return false; return new Date(expiresAt) < new Date(); } function isActiveStatus(status: string): boolean { return ["pending", "queued", "cart", "searching", "ordering", "polling", "downloading"].includes( status, ); } type StatusStyle = { label: string; className: string; pulse?: boolean }; function statusBadge(status: string, expiresAt: string | null): StatusStyle { if (status === "completed" && isExpired(expiresAt)) { return { label: "Expirat", className: "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950/40 dark:text-orange-400 dark:border-orange-800", }; } switch (status) { case "pending": case "queued": return { label: "In coada", className: "bg-muted text-muted-foreground border-muted-foreground/20", }; case "cart": case "searching": case "ordering": case "polling": case "downloading": return { label: "Se proceseaza", className: "bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-950/40 dark:text-yellow-400 dark:border-yellow-800", pulse: true, }; case "completed": return { label: "Valid", className: "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800", }; case "failed": return { label: "Eroare", className: "bg-rose-100 text-rose-700 border-rose-200 dark:bg-rose-950/40 dark:text-rose-400 dark:border-rose-800", }; case "review": return { label: "De verificat", className: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950/40 dark:text-amber-400 dark:border-amber-800", }; case "cancelled": return { label: "Anulat", className: "bg-muted text-muted-foreground border-muted-foreground/20", }; default: return { label: status, className: "bg-muted text-muted-foreground border-muted-foreground/20", }; } } /* ------------------------------------------------------------------ */ /* Filter tabs */ /* ------------------------------------------------------------------ */ type FilterValue = "all" | "valid" | "expired" | "active"; const FILTER_OPTIONS: { value: FilterValue; label: string }[] = [ { value: "all", label: "Toate" }, { value: "valid", label: "Valabile" }, { value: "expired", label: "Expirate" }, { value: "active", label: "In procesare" }, ]; /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export function EpayTab() { /* -- Cutover flag (Plan 003, Faza F) ----------------------------- */ const { data: session } = useSession(); const useGisAc = Boolean( (session as { useGisAc?: boolean } | null)?.useGisAc, ); /* -- ePay session ------------------------------------------------ */ const [epayStatus, setEpayStatus] = useState({ connected: false, }); /* -- Orders list ------------------------------------------------- */ const [orders, setOrders] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); /* -- Filter ------------------------------------------------------ */ const [filterTab, setFilterTab] = useState("all"); /* -- Selection (ordered — index = numbering in ZIP) -------------- */ const [selectedIds, setSelectedIds] = useState([]); const [downloadingSelection, setDownloadingSelection] = useState(false); const toggleSelect = (id: string) => { setSelectedIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], ); }; const handleDownloadSelection = async () => { if (selectedIds.length === 0) return; setDownloadingSelection(true); try { // ZIP endpoint only exists on the legacy backend today. For pilot // users on the gis-ac path we fall back to triggering individual // PDF downloads (one-by-one) until gis-api ships a batch endpoint. if (useGisAc) { for (const id of selectedIds) { const a = document.createElement("a"); a.href = cfDownloadUrl(true, id); a.target = "_blank"; a.rel = "noopener noreferrer"; a.click(); } } else { const ids = selectedIds.join(","); const a = document.createElement("a"); a.href = `/api/ancpi/download-zip?ids=${encodeURIComponent(ids)}`; a.download = `Extrase_CF_selectie_${selectedIds.length}.zip`; a.click(); } } finally { setTimeout(() => setDownloadingSelection(false), 2000); } }; /* -- Search ------------------------------------------------------ */ const [searchQuery, setSearchQuery] = useState(""); /* -- SIRUTA autocomplete ----------------------------------------- */ const [sirutaSearch, setSirutaSearch] = useState(""); const [sirutaResults, setSirutaResults] = useState([]); const [showSirutaResults, setShowSirutaResults] = useState(false); /* -- Downloading all --------------------------------------------- */ const [downloadingAll, setDownloadingAll] = useState(false); /* -- Polling ----------------------------------------------------- */ const pollRef = useRef | null>(null); const hasActive = orders.some((o) => isActiveStatus(o.status)); /* -- Fetch session status ---------------------------------------- */ const fetchEpayStatus = useCallback(async () => { try { const res = await fetch("/api/ancpi/session"); const data = (await res.json()) as EpaySessionStatus; setEpayStatus(data); } catch { /* silent */ } }, []); /* -- Fetch orders ------------------------------------------------ */ const fetchOrders = useCallback( async (showRefreshing = false) => { if (showRefreshing) setRefreshing(true); try { const data = await fetchCfOrdersList(useGisAc, { limit: 200 }); setOrders(data.orders); setTotal(data.total); } catch { /* silent */ } finally { setLoading(false); setRefreshing(false); } }, [useGisAc], ); /* -- Initial load ------------------------------------------------ */ useEffect(() => { void fetchEpayStatus(); void fetchOrders(); }, [fetchEpayStatus, fetchOrders]); /* -- Auto-refresh when active orders exist ----------------------- */ useEffect(() => { if (pollRef.current) clearInterval(pollRef.current); if (hasActive) { pollRef.current = setInterval(() => { void fetchOrders(); void fetchEpayStatus(); }, 10_000); } return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, [hasActive, fetchOrders, fetchEpayStatus]); /* -- SIRUTA autocomplete ----------------------------------------- */ useEffect(() => { const raw = sirutaSearch.trim(); if (raw.length < 2) { setSirutaResults([]); return; } // Only search if it looks like a SIRUTA code (digits) if (!/^\d+$/.test(raw)) { setSirutaResults([]); return; } const controller = new AbortController(); void (async () => { try { const res = await fetch("/api/eterra/uats", { signal: controller.signal }); const data = (await res.json()) as { uats?: GisUatResult[] }; if (data.uats) { const matches = data.uats .filter((u) => u.siruta.startsWith(raw)) .slice(0, 8); setSirutaResults(matches); } } catch { /* silent */ } })(); return () => controller.abort(); }, [sirutaSearch]); /* -- Re-order (for expired extracts) ----------------------------- */ const handleReorder = async (order: CfExtractRecord) => { const result = await placeCfOrder(useGisAc, { nrCadastral: order.nrCadastral, nrCF: order.nrCF, siruta: order.siruta, judetName: order.judetName, uatName: order.uatName, }); if (result.ok) { void fetchOrders(true); void fetchEpayStatus(); } /* errors surfaced inline via downstream polling later */ }; /* -- Retry download (QW3) — re-runs poll+download for an already-paid * order, no new charge. For rows that failed at the download/poll * stage (the order exists at ANCPI but we never stored the PDF). -- */ const [retryingId, setRetryingId] = useState(null); const [retryNotice, setRetryNotice] = useState(null); const handleRetryDownload = async (order: CfExtractRecord) => { setRetryingId(order.id); setRetryNotice(null); try { const res = await fetch( `/api/ancpi/recover?extractId=${encodeURIComponent(order.id)}`, ); const data = (await res.json().catch(() => ({}))) as { error?: string; completed?: number; attempted?: number; }; if (!res.ok) { // 409 (queue busy / no orderId yet), 404, 500 — tell the user. setRetryNotice(data.error ?? `Reîncercare eșuată (${res.status}).`); } else if ((data.completed ?? 0) > 0) { setRetryNotice( `Recuperat: ${data.completed}/${data.attempted ?? data.completed} extrase.`, ); } else { setRetryNotice( "Nimic de recuperat — comanda nu există la ANCPI sau e deja finalizată.", ); } void fetchOrders(true); void fetchEpayStatus(); } catch { setRetryNotice("Eroare rețea la reîncercare."); } finally { setRetryingId(null); } }; /* -- Download all valid as ZIP ----------------------------------- */ const handleDownloadAll = async () => { const validOrders = filteredOrders.filter( (o) => o.status === "completed" && o.minioPath && !isExpired(o.expiresAt), ); if (validOrders.length === 0) return; setDownloadingAll(true); try { if (useGisAc) { // No bulk-zip endpoint on api.gis.ac yet — trigger individual // PDF downloads. Browser dedup will handle these as separate tabs. for (const o of validOrders) { const a = document.createElement("a"); a.href = cfDownloadUrl(true, o.id); a.target = "_blank"; a.rel = "noopener noreferrer"; a.click(); } } else { const ids = validOrders.map((o) => o.id).join(","); const res = await fetch(`/api/ancpi/download-zip?ids=${ids}`); 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.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 { /* silent */ } finally { setDownloadingAll(false); } }; /* -- Filter + search orders -------------------------------------- */ const filteredOrders = orders.filter((order) => { // Filter tab switch (filterTab) { case "valid": if (order.status !== "completed" || isExpired(order.expiresAt)) return false; break; case "expired": if (order.status !== "completed" || !isExpired(order.expiresAt)) return false; break; case "active": if (!isActiveStatus(order.status)) return false; break; } // Text search const query = searchQuery.trim().toLowerCase(); if (query) { const searchable = [ order.nrCadastral, order.nrCF, order.uatName, order.judetName, order.siruta, ] .filter(Boolean) .join(" ") .toLowerCase(); if (!searchable.includes(query)) return false; } return true; }); // Count valid extracts for the "Descarca tot" button const validCount = filteredOrders.filter( (o) => o.status === "completed" && o.minioPath && !isExpired(o.expiresAt), ).length; /* -- Render ------------------------------------------------------ */ return (
{/* -- Header ------------------------------------------------- */}

Extrase CF

{total} extras{total !== 1 ? "e" : ""}
{/* Download selection */} {selectedIds.length > 0 && ( ZIP cu {selectedIds.length} extrase numerotate in ordinea selectarii )} {/* Download all valid */} {validCount > 0 && ( ZIP cu toate extrasele valabile )}
{/* -- Search bar --------------------------------------------- */}
setSearchQuery(e.target.value)} className="h-8 pl-8 text-xs" />
{/* -- Filter tabs -------------------------------------------- */}
{FILTER_OPTIONS.map((opt) => ( ))}
{/* -- Orders table ------------------------------------------- */} {loading ? (

Se incarca extrasele...

) : filteredOrders.length === 0 ? (

{orders.length === 0 ? "Niciun extras CF" : "Niciun rezultat pentru filtrele selectate"}

{orders.length === 0 ? "Foloseste butonul de pe fiecare parcela din tab-ul Cautare sau Lista mea." : "Incearca sa schimbi filtrul sau sa stergi cautarea."}

) : (
{filteredOrders.map((order, idx) => { const badge = statusBadge(order.status, order.expiresAt); const expired = order.status === "completed" && isExpired(order.expiresAt); return ( {/* Checkbox */} {/* # */} {/* Nr. Cadastral */} {/* UAT */} {/* Status */} {/* Data */} {/* Expira */} {/* Actiuni */} ); })}
0 && filteredOrders .filter((o) => o.status === "completed" && o.minioPath) .every((o) => selectedIds.includes(o.id)) } onChange={() => { const downloadable = filteredOrders.filter( (o) => o.status === "completed" && o.minioPath, ); const allSelected = downloadable.every((o) => selectedIds.includes(o.id), ); if (allSelected) { setSelectedIds([]); } else { setSelectedIds(downloadable.map((o) => o.id)); } }} /> Selecteaza/deselecteaza tot # Nr. Cadastral UAT Status Data Expira Actiuni
{order.status === "completed" && order.minioPath ? ( toggleSelect(order.id)} /> {selectedIds.includes(order.id) ? `#${selectedIds.indexOf(order.id) + 1} in ZIP` : "Adauga in selectie"} ) : ( )} {idx + 1}
{order.nrCadastral} {order.nrCF && order.nrCF !== order.nrCadastral && ( CF: {order.nrCF} )} {order.version > 1 && ( v{order.version} )}
{order.uatName} jud. {order.judetName}
{badge.label} {order.type === "intern" ? "intern" : "ePay"}
{order.errorMessage && (

{order.errorMessage}

)}
{formatDate(order.completedAt ?? order.createdAt)} {order.expiresAt ? ( {expired && ( )} {formatShortDate(order.expiresAt)} ) : ( {"—"} )}
{order.status === "completed" && order.minioPath && !expired && ( Descarca extras CF ({order.nrCadastral}) )} {expired && order.status === "completed" && order.minioPath && ( {`Descarca versiunea expirata (${formatShortDate(order.expiresAt)})`} )} {expired && (

Comanda extras CF nou (1 credit)

Extrasul actual a expirat

)} {/* Review row — PDF exists but match was ambiguous; let the operator download + verify (QW3/R4). */} {order.status === "review" && order.minioPath && ( Descarca PDF pentru verificare manuala (potrivire ambigua) )} {/* Failed-with-order row — retry poll+download, no new charge (QW3). */} {(order.status === "failed" || order.status === "review") && ( Reia descarcarea (fara cost nou) daca comanda exista la ANCPI )}
)} {/* -- Retry notice ------------------------------------------- */} {retryNotice && (
{retryNotice}
)} {/* -- Active orders indicator -------------------------------- */} {hasActive && (
Se actualizeaza automat la fiecare 10 secunde...
)}
); }