feat(ancpi): complete ePay UI redesign + ZIP download + smart batch ordering
UI Redesign:
- ePay auto-connect when UAT is selected (no manual button)
- Credit badge with tooltip ("N credite ePay disponibile")
- Search result cards show CF status: Valid (green), Expirat (orange),
Lipsă (gray), Se proceseaza (yellow pulse)
- Action buttons on each card: download/update/order CF extract
- "Lista mea" numbered rows + CF Status column + smart batch button
"Scoate Extrase CF": skips valid, re-orders expired, orders new
- "Descarca Extrase CF" button → ZIP archive with numbered files
- Extrase CF tab simplified: clean table, filters (Toate/Valabile/
Expirate/In procesare), search, download-all ZIP
Backend:
- GET /api/ancpi/download-zip?ids=... → JSZip streaming
- GET /api/ancpi/orders: multi-cadastral status check with statusMap
(valid/expired/none/processing) + latestById
Data:
- Simulated expired extract for 328611 (Cluj-Napoca, expired 2026-03-17)
- Cleaned old error records from DB
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,9 @@ import {
|
||||
Download,
|
||||
RefreshCw,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
CreditCard,
|
||||
Plus,
|
||||
Trash2,
|
||||
Clock,
|
||||
Archive,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
@@ -44,6 +42,12 @@ type CfExtractRecord = {
|
||||
completedAt: string | null;
|
||||
};
|
||||
|
||||
type GisUatResult = {
|
||||
siruta: string;
|
||||
name: string;
|
||||
county: string | null;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -111,7 +115,7 @@ function statusBadge(status: string, expiresAt: string | null): StatusStyle {
|
||||
};
|
||||
case "completed":
|
||||
return {
|
||||
label: "Finalizat",
|
||||
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",
|
||||
};
|
||||
@@ -136,39 +140,54 @@ function statusBadge(status: string, expiresAt: string | null): StatusStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 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() {
|
||||
/* ── ePay session ──────────────────────────────────────────────── */
|
||||
/* -- ePay session ------------------------------------------------ */
|
||||
const [epayStatus, setEpayStatus] = useState<EpaySessionStatus>({
|
||||
connected: false,
|
||||
});
|
||||
|
||||
/* ── Orders list ───────────────────────────────────────────────── */
|
||||
/* -- Orders list ------------------------------------------------- */
|
||||
const [orders, setOrders] = useState<CfExtractRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
/* ── Manual order input ────────────────────────────────────────── */
|
||||
const [manualInput, setManualInput] = useState("");
|
||||
const [manualSiruta, setManualSiruta] = useState("");
|
||||
const [manualCounty, setManualCounty] = useState("");
|
||||
const [manualUat, setManualUat] = useState("");
|
||||
const [orderSubmitting, setOrderSubmitting] = useState(false);
|
||||
const [orderError, setOrderError] = useState("");
|
||||
const [orderSuccess, setOrderSuccess] = useState("");
|
||||
/* -- Filter ------------------------------------------------------ */
|
||||
const [filterTab, setFilterTab] = useState<FilterValue>("all");
|
||||
|
||||
/* ── Filter ────────────────────────────────────────────────────── */
|
||||
const [filterStatus, setFilterStatus] = useState<string>("all");
|
||||
/* -- Search ------------------------------------------------------ */
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
/* ── Polling ───────────────────────────────────────────────────── */
|
||||
/* -- 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 ──────────────────────────────────────── */
|
||||
/* -- Fetch session status ---------------------------------------- */
|
||||
const fetchEpayStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/session");
|
||||
@@ -179,15 +198,12 @@ export function EpayTab() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* ── Fetch orders ──────────────────────────────────────────────── */
|
||||
/* -- Fetch orders ------------------------------------------------ */
|
||||
const fetchOrders = useCallback(
|
||||
async (showRefreshing = false) => {
|
||||
if (showRefreshing) setRefreshing(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ limit: "100" });
|
||||
if (filterStatus !== "all") params.set("status", filterStatus);
|
||||
|
||||
const res = await fetch(`/api/ancpi/orders?${params.toString()}`);
|
||||
const res = await fetch("/api/ancpi/orders?limit=200");
|
||||
const data = (await res.json()) as {
|
||||
orders: CfExtractRecord[];
|
||||
total: number;
|
||||
@@ -201,16 +217,16 @@ export function EpayTab() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[filterStatus],
|
||||
[],
|
||||
);
|
||||
|
||||
/* ── Initial load ──────────────────────────────────────────────── */
|
||||
/* -- Initial load ------------------------------------------------ */
|
||||
useEffect(() => {
|
||||
void fetchEpayStatus();
|
||||
void fetchOrders();
|
||||
}, [fetchEpayStatus, fetchOrders]);
|
||||
|
||||
/* ── Auto-refresh when active orders exist ─────────────────────── */
|
||||
/* -- Auto-refresh when active orders exist ----------------------- */
|
||||
useEffect(() => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
|
||||
@@ -226,59 +242,39 @@ export function EpayTab() {
|
||||
};
|
||||
}, [hasActive, fetchOrders, fetchEpayStatus]);
|
||||
|
||||
/* ── Submit manual order ───────────────────────────────────────── */
|
||||
const submitManualOrder = async () => {
|
||||
setOrderSubmitting(true);
|
||||
setOrderError("");
|
||||
setOrderSuccess("");
|
||||
|
||||
const cadNumbers = manualInput
|
||||
.split(/[,\n;]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (cadNumbers.length === 0) {
|
||||
setOrderError("Introdu cel putin un numar cadastral.");
|
||||
setOrderSubmitting(false);
|
||||
/* -- 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;
|
||||
}
|
||||
|
||||
try {
|
||||
const parcels = cadNumbers.map((nr) => ({
|
||||
nrCadastral: nr,
|
||||
siruta: manualSiruta || undefined,
|
||||
judetIndex: 0,
|
||||
judetName: manualCounty || "N/A",
|
||||
uatId: 0,
|
||||
uatName: manualUat || "N/A",
|
||||
}));
|
||||
|
||||
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) {
|
||||
setOrderError(data.error ?? "Eroare la trimiterea comenzii.");
|
||||
} else {
|
||||
const count = data.orders?.length ?? cadNumbers.length;
|
||||
setOrderSuccess(
|
||||
`${count} extras${count > 1 ? "e" : ""} CF trimis${count > 1 ? "e" : ""} la procesare.`,
|
||||
);
|
||||
setManualInput("");
|
||||
void fetchOrders(true);
|
||||
void fetchEpayStatus();
|
||||
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 */
|
||||
}
|
||||
} catch {
|
||||
setOrderError("Eroare retea.");
|
||||
} finally {
|
||||
setOrderSubmitting(false);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
/* ── Re-order (for expired extracts) ───────────────────────────── */
|
||||
return () => controller.abort();
|
||||
}, [sirutaSearch]);
|
||||
|
||||
/* -- Re-order (for expired extracts) ----------------------------- */
|
||||
const handleReorder = async (order: CfExtractRecord) => {
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/order", {
|
||||
@@ -310,41 +306,108 @@ export function EpayTab() {
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Render ────────────────────────────────────────────────────── */
|
||||
/* -- 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;
|
||||
|
||||
const statusOptions = [
|
||||
{ value: "all", label: "Toate" },
|
||||
{ value: "queued", label: "In coada" },
|
||||
{ value: "polling", label: "In procesare" },
|
||||
{ value: "completed", label: "Finalizate" },
|
||||
{ value: "failed", label: "Erori" },
|
||||
];
|
||||
setDownloadingAll(true);
|
||||
try {
|
||||
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: ePay status + credits ───────────────────────── */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* -- 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>
|
||||
{epayStatus.connected ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400"
|
||||
>
|
||||
<CreditCard className="mr-0.5 h-2.5 w-2.5" />
|
||||
{epayStatus.credits ?? "?"} credite
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px] text-muted-foreground">
|
||||
ePay neconectat
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{total} extras{total !== 1 ? "e" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Download all valid */}
|
||||
{validCount > 0 && (
|
||||
<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>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -362,117 +425,42 @@ export function EpayTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Manual order section ────────────────────────────────── */}
|
||||
{epayStatus.connected && (
|
||||
<Card>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Comanda noua</span>
|
||||
</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>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Numere cadastrale (separate prin virgula sau rand nou)
|
||||
</label>
|
||||
<Input
|
||||
placeholder="ex: 50001, 50002, 50003"
|
||||
value={manualInput}
|
||||
onChange={(e) => {
|
||||
setManualInput(e.target.value);
|
||||
setOrderError("");
|
||||
setOrderSuccess("");
|
||||
}}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
SIRUTA (optional)
|
||||
</label>
|
||||
<Input
|
||||
placeholder="57582"
|
||||
value={manualSiruta}
|
||||
onChange={(e) => setManualSiruta(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Judet
|
||||
</label>
|
||||
<Input
|
||||
placeholder="Cluj"
|
||||
value={manualCounty}
|
||||
onChange={(e) => setManualCounty(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
UAT
|
||||
</label>
|
||||
<Input
|
||||
placeholder="Feleacu"
|
||||
value={manualUat}
|
||||
onChange={(e) => setManualUat(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={orderSubmitting || !manualInput.trim()}
|
||||
onClick={() => void submitManualOrder()}
|
||||
>
|
||||
{orderSubmitting ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<FileText className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
Comanda extrase
|
||||
</Button>
|
||||
{orderError && (
|
||||
<span className="text-xs text-destructive flex items-center gap-1">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
{orderError}
|
||||
</span>
|
||||
)}
|
||||
{orderSuccess && (
|
||||
<span className="text-xs text-emerald-600 dark:text-emerald-400">
|
||||
{orderSuccess}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ── Filter bar ──────────────────────────────────────────── */}
|
||||
{/* -- Filter tabs -------------------------------------------- */}
|
||||
<div className="flex gap-1 p-0.5 bg-muted rounded-md w-fit">
|
||||
{statusOptions.map((opt) => (
|
||||
{FILTER_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-2.5 py-1 text-xs rounded-sm transition-colors",
|
||||
filterStatus === opt.value
|
||||
filterTab === opt.value
|
||||
? "bg-background text-foreground shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
onClick={() => setFilterStatus(opt.value)}
|
||||
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 ────────────────────────────────────────── */}
|
||||
{/* -- Orders table ------------------------------------------- */}
|
||||
{loading ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center text-muted-foreground">
|
||||
@@ -480,15 +468,19 @@ export function EpayTab() {
|
||||
<p>Se incarca extrasele...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : orders.length === 0 ? (
|
||||
) : 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">Niciun extras CF</p>
|
||||
<p className="font-medium">
|
||||
{orders.length === 0
|
||||
? "Niciun extras CF"
|
||||
: "Niciun rezultat pentru filtrele selectate"}
|
||||
</p>
|
||||
<p className="text-xs mt-1">
|
||||
{epayStatus.connected
|
||||
? "Foloseste sectiunea de mai sus sau butonul de pe fiecare parcela."
|
||||
: "Conecteaza-te la ePay ANCPI pentru a comanda extrase CF."}
|
||||
{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>
|
||||
@@ -497,6 +489,7 @@ export function EpayTab() {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40">
|
||||
<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>
|
||||
@@ -510,7 +503,7 @@ export function EpayTab() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.map((order) => {
|
||||
{filteredOrders.map((order, idx) => {
|
||||
const badge = statusBadge(order.status, order.expiresAt);
|
||||
const expired =
|
||||
order.status === "completed" && isExpired(order.expiresAt);
|
||||
@@ -523,6 +516,11 @@ export function EpayTab() {
|
||||
expired && "opacity-70",
|
||||
)}
|
||||
>
|
||||
{/* # */}
|
||||
<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">
|
||||
@@ -662,7 +660,7 @@ export function EpayTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Active orders indicator ─────────────────────────────── */}
|
||||
{/* -- Active orders indicator -------------------------------- */}
|
||||
{hasActive && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user