Files
ArchiTools/src/modules/parcel-sync/components/epay-tab.tsx
T
Claude VM b62132ab9e fix(epay): 4 regressions from adversarial review of the hardening diff
Adversarial review (9 agents) of f7f7c59..28c870f found 4 confirmed bugs in
the hardening itself; all fixed:

1. Parallel-download index race: two items with the SAME nrCadastral in one
   batch both scanned MinIO, both computed index 1, the second putObject
   silently overwrote the first paid extract. Pre-allocate per-cadastral
   indices sequentially before the parallel block; storeCfExtract takes an
   explicit index (epay-queue.ts, epay-storage.ts).
2. Metadata-fail orphan charge: on saveMetadata failure the row was popped
   from cleanup tracking even when deleteCartItem was NOT confirmed, leaving
   an undeletable metadata-less row in the global cart that submitOrder would
   check out and charge. Now: pop only on confirmed delete; if unconfirmed,
   mark cartDirty and ABORT before submit (epay-queue.ts).
3. Recover vs live queue race: the widened recover WHERE (orderId:null +
   cart/ordering/... states) could scoop a concurrently-processing batch's
   rows and re-stamp them with the wrong orderId. Block recover while
   getQueueStatus().processing (recover/route.ts).
4. 'review' status leaked as 'done' in the geoportal CF-order modal (minioPath
   short-circuit) — handed an unverified PDF as a finished extract. Check
   review/failed BEFORE the minioPath fallback (cf-order-modal.tsx).

Plus 2 nits: download-zip excludes 'review' rows server-side; retry button
surfaces recover errors/results instead of swallowing them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 00:17:12 +03:00

958 lines
36 KiB
TypeScript

"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<EpaySessionStatus>({
connected: false,
});
/* -- Orders list ------------------------------------------------- */
const [orders, setOrders] = useState<CfExtractRecord[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
/* -- Filter ------------------------------------------------------ */
const [filterTab, setFilterTab] = useState<FilterValue>("all");
/* -- Selection (ordered — index = numbering in ZIP) -------------- */
const [selectedIds, setSelectedIds] = useState<string[]>([]);
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<GisUatResult[]>([]);
const [showSirutaResults, setShowSirutaResults] = useState(false);
/* -- Downloading all --------------------------------------------- */
const [downloadingAll, setDownloadingAll] = useState(false);
/* -- Polling ----------------------------------------------------- */
const pollRef = useRef<ReturnType<typeof setInterval> | 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<string | null>(null);
const [retryNotice, setRetryNotice] = useState<string | null>(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 (
<div className="space-y-4">
{/* -- Header ------------------------------------------------- */}
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-muted-foreground" />
<h3 className="text-sm font-semibold">Extrase CF</h3>
<span className="text-xs text-muted-foreground">
{total} extras{total !== 1 ? "e" : ""}
</span>
</div>
<div className="flex items-center gap-2">
{/* Download selection */}
{selectedIds.length > 0 && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="sm"
className="h-7 text-xs"
disabled={downloadingSelection}
onClick={() => void handleDownloadSelection()}
>
{downloadingSelection ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<Download className="h-3 w-3 mr-1" />
)}
Descarca selectie ({selectedIds.length})
</Button>
</TooltipTrigger>
<TooltipContent>
ZIP cu {selectedIds.length} extrase numerotate in ordinea selectarii
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Download all valid */}
{validCount > 0 && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-7 text-xs"
disabled={downloadingAll}
onClick={() => void handleDownloadAll()}
>
{downloadingAll ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<Archive className="h-3 w-3 mr-1" />
)}
Descarca tot ({validCount})
</Button>
</TooltipTrigger>
<TooltipContent>
ZIP cu toate extrasele valabile
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
disabled={refreshing}
onClick={() => void fetchOrders(true)}
>
{refreshing ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<RefreshCw className="h-3 w-3 mr-1" />
)}
Reincarca
</Button>
</div>
</div>
{/* -- Search bar --------------------------------------------- */}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
<Input
placeholder="Cauta dupa nr. cadastral, UAT, judet..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-8 pl-8 text-xs"
/>
</div>
</div>
{/* -- Filter tabs -------------------------------------------- */}
<div className="flex gap-1 p-0.5 bg-muted rounded-md w-fit">
{FILTER_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
className={cn(
"px-2.5 py-1 text-xs rounded-sm transition-colors",
filterTab === opt.value
? "bg-background text-foreground shadow-sm font-medium"
: "text-muted-foreground hover:text-foreground",
)}
onClick={() => setFilterTab(opt.value)}
>
{opt.label}
{opt.value === "active" && hasActive && (
<span className="ml-1 inline-flex h-1.5 w-1.5 rounded-full bg-yellow-500" />
)}
</button>
))}
</div>
{/* -- Orders table ------------------------------------------- */}
{loading ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
<Loader2 className="h-8 w-8 mx-auto mb-3 animate-spin opacity-40" />
<p>Se incarca extrasele...</p>
</CardContent>
</Card>
) : filteredOrders.length === 0 ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
<FileText className="h-10 w-10 mx-auto mb-3 opacity-30" />
<p className="font-medium">
{orders.length === 0
? "Niciun extras CF"
: "Niciun rezultat pentru filtrele selectate"}
</p>
<p className="text-xs mt-1">
{orders.length === 0
? "Foloseste butonul de pe fiecare parcela din tab-ul Cautare sau Lista mea."
: "Incearca sa schimbi filtrul sau sa stergi cautarea."}
</p>
</CardContent>
</Card>
) : (
<div className="overflow-x-auto rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/40">
<th className="px-3 py-2 text-center font-medium w-8">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<input
type="checkbox"
className="rounded cursor-pointer"
checked={
filteredOrders.length > 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));
}
}}
/>
</TooltipTrigger>
<TooltipContent>Selecteaza/deselecteaza tot</TooltipContent>
</Tooltip>
</TooltipProvider>
</th>
<th className="px-3 py-2 text-left font-medium w-8">#</th>
<th className="px-3 py-2 text-left font-medium">
Nr. Cadastral
</th>
<th className="px-3 py-2 text-left font-medium">UAT</th>
<th className="px-3 py-2 text-left font-medium">Status</th>
<th className="px-3 py-2 text-left font-medium">Data</th>
<th className="px-3 py-2 text-left font-medium">Expira</th>
<th className="px-3 py-2 text-right font-medium">
Actiuni
</th>
</tr>
</thead>
<tbody>
{filteredOrders.map((order, idx) => {
const badge = statusBadge(order.status, order.expiresAt);
const expired =
order.status === "completed" && isExpired(order.expiresAt);
return (
<tr
key={order.id}
className={cn(
"border-b last:border-0 transition-colors hover:bg-muted/30",
expired && "opacity-70",
)}
>
{/* Checkbox */}
<td className="px-3 py-2 text-center">
{order.status === "completed" && order.minioPath ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<input
type="checkbox"
className="rounded cursor-pointer"
checked={selectedIds.includes(order.id)}
onChange={() => toggleSelect(order.id)}
/>
</TooltipTrigger>
<TooltipContent>
{selectedIds.includes(order.id)
? `#${selectedIds.indexOf(order.id) + 1} in ZIP`
: "Adauga in selectie"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span className="text-muted-foreground/30"></span>
)}
</td>
{/* # */}
<td className="px-3 py-2 text-xs text-muted-foreground tabular-nums">
{idx + 1}
</td>
{/* Nr. Cadastral */}
<td className="px-3 py-2">
<div className="flex flex-col">
<span className="font-medium tabular-nums">
{order.nrCadastral}
</span>
{order.nrCF &&
order.nrCF !== order.nrCadastral && (
<span className="text-[10px] text-muted-foreground">
CF: {order.nrCF}
</span>
)}
{order.version > 1 && (
<span className="text-[10px] text-muted-foreground">
v{order.version}
</span>
)}
</div>
</td>
{/* UAT */}
<td className="px-3 py-2">
<div className="flex flex-col">
<span>{order.uatName}</span>
<span className="text-[10px] text-muted-foreground">
jud. {order.judetName}
</span>
</div>
</td>
{/* Status */}
<td className="px-3 py-2">
<div className="flex flex-wrap items-center gap-1">
<span
className={cn(
"inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium",
badge.className,
badge.pulse && "animate-pulse",
)}
>
{badge.label}
</span>
<span
className={cn(
"inline-flex items-center rounded-full border px-1.5 py-0.5 text-[9px] font-medium",
order.type === "intern"
? "border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-300"
: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
)}
title={
order.type === "intern"
? "CF intern (gratuit, copycf)"
: "Extras CF ePay (1 credit ANCPI)"
}
>
{order.type === "intern" ? "intern" : "ePay"}
</span>
</div>
{order.errorMessage && (
<p
className="text-[10px] text-destructive mt-0.5 max-w-48 truncate"
title={order.errorMessage}
>
{order.errorMessage}
</p>
)}
</td>
{/* Data */}
<td className="px-3 py-2 text-xs text-muted-foreground tabular-nums">
{formatDate(order.completedAt ?? order.createdAt)}
</td>
{/* Expira */}
<td className="px-3 py-2 text-xs tabular-nums">
{order.expiresAt ? (
<span
className={cn(
expired
? "text-orange-600 dark:text-orange-400"
: "text-muted-foreground",
)}
>
{expired && (
<Clock className="inline h-3 w-3 mr-0.5 -mt-0.5" />
)}
{formatShortDate(order.expiresAt)}
</span>
) : (
<span className="text-muted-foreground">{"—"}</span>
)}
</td>
{/* Actiuni */}
<td className="px-3 py-2 text-right">
<div className="flex items-center justify-end gap-1">
{order.status === "completed" &&
order.minioPath &&
!expired && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
asChild
>
<a
href={cfDownloadUrl(order)}
target="_blank"
rel="noopener noreferrer"
>
<Download className="h-3 w-3 mr-1" />
Descarca
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
Descarca extras CF ({order.nrCadastral})
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{expired && order.status === "completed" && order.minioPath && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs text-muted-foreground"
asChild
>
<a
href={cfDownloadUrl(order)}
target="_blank"
rel="noopener noreferrer"
>
<Download className="h-3 w-3 mr-1" />
Descarca
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
{`Descarca versiunea expirata (${formatShortDate(order.expiresAt)})`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{expired && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="sm"
className="h-7 px-3 text-xs bg-orange-600 hover:bg-orange-700 text-white"
disabled={!epayStatus.connected}
onClick={() => void handleReorder(order)}
>
<RefreshCw className="h-3 w-3 mr-1" />
Actualizeaza
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Comanda extras CF nou (1 credit)</p>
<p className="text-xs text-muted-foreground">
Extrasul actual a expirat
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Review row — PDF exists but match was ambiguous;
let the operator download + verify (QW3/R4). */}
{order.status === "review" && order.minioPath && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs text-amber-700 dark:text-amber-400"
asChild
>
<a
href={cfDownloadUrl(order)}
target="_blank"
rel="noopener noreferrer"
>
<Download className="h-3 w-3 mr-1" />
Verifica
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
Descarca PDF pentru verificare manuala
(potrivire ambigua)
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Failed-with-order row — retry poll+download, no
new charge (QW3). */}
{(order.status === "failed" ||
order.status === "review") && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
disabled={
!epayStatus.connected ||
retryingId === order.id
}
onClick={() =>
void handleRetryDownload(order)
}
>
{retryingId === order.id ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<RefreshCw className="h-3 w-3 mr-1" />
)}
Reincearca
</Button>
</TooltipTrigger>
<TooltipContent>
Reia descarcarea (fara cost nou) daca comanda
exista la ANCPI
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{/* -- Retry notice ------------------------------------------- */}
{retryNotice && (
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-3 py-2 text-xs">
<span>{retryNotice}</span>
<button
type="button"
className="text-muted-foreground hover:text-foreground"
onClick={() => setRetryNotice(null)}
>
</button>
</div>
)}
{/* -- Active orders indicator -------------------------------- */}
{hasActive && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
<span>
Se actualizeaza automat la fiecare 10 secunde...
</span>
</div>
)}
</div>
);
}