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:
@@ -3,13 +3,17 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import {
|
||||
Loader2,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
LogOut,
|
||||
CreditCard,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/shared/components/ui/tooltip";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -30,8 +34,11 @@ export type EpaySessionStatus = {
|
||||
|
||||
export function EpayConnect({
|
||||
onStatusChange,
|
||||
triggerConnect,
|
||||
}: {
|
||||
onStatusChange?: (status: EpaySessionStatus) => void;
|
||||
/** When set to true, triggers auto-connect (e.g. when user types UAT) */
|
||||
triggerConnect?: boolean;
|
||||
}) {
|
||||
const [status, setStatus] = useState<EpaySessionStatus>({ connected: false });
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
@@ -39,6 +46,7 @@ export function EpayConnect({
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const cbRef = useRef(onStatusChange);
|
||||
cbRef.current = onStatusChange;
|
||||
const autoConnectAttempted = useRef(false);
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
@@ -61,7 +69,8 @@ export function EpayConnect({
|
||||
};
|
||||
}, [fetchStatus]);
|
||||
|
||||
const connect = async () => {
|
||||
const connect = useCallback(async () => {
|
||||
if (connecting || status.connected) return;
|
||||
setConnecting(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -77,11 +86,19 @@ export function EpayConnect({
|
||||
await fetchStatus();
|
||||
}
|
||||
} catch {
|
||||
setError("Eroare rețea");
|
||||
setError("Eroare retea");
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
}, [connecting, status.connected, fetchStatus]);
|
||||
|
||||
// Auto-connect when triggerConnect becomes true
|
||||
useEffect(() => {
|
||||
if (triggerConnect && !status.connected && !connecting && !autoConnectAttempted.current) {
|
||||
autoConnectAttempted.current = true;
|
||||
void connect();
|
||||
}
|
||||
}, [triggerConnect, status.connected, connecting, connect]);
|
||||
|
||||
const disconnect = async () => {
|
||||
try {
|
||||
@@ -90,6 +107,7 @@ export function EpayConnect({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "disconnect" }),
|
||||
});
|
||||
autoConnectAttempted.current = false;
|
||||
await fetchStatus();
|
||||
} catch {
|
||||
/* silent */
|
||||
@@ -97,74 +115,68 @@ export function EpayConnect({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Status pill */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium",
|
||||
status.connected
|
||||
? "border-emerald-200 bg-emerald-50/80 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-400"
|
||||
: error
|
||||
? "border-rose-200 bg-rose-50/80 text-rose-600 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-400"
|
||||
: "border-muted-foreground/20 bg-muted/50 text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{connecting ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : status.connected ? (
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
||||
</span>
|
||||
) : error ? (
|
||||
<WifiOff className="h-3 w-3" />
|
||||
) : (
|
||||
<Wifi className="h-3 w-3 opacity-50" />
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium",
|
||||
status.connected
|
||||
? "border-emerald-200 bg-emerald-50/80 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-400"
|
||||
: error
|
||||
? "border-rose-200 bg-rose-50/80 text-rose-600 dark:border-rose-800 dark:bg-rose-950/40 dark:text-rose-400"
|
||||
: connecting
|
||||
? "border-muted-foreground/20 bg-muted/50 text-muted-foreground"
|
||||
: "border-muted-foreground/20 bg-muted/50 text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{connecting ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : status.connected ? (
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<span className="hidden sm:inline">ePay</span>
|
||||
<span className="hidden sm:inline">ePay</span>
|
||||
|
||||
{status.connected && status.credits != null && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="ml-0.5 h-4 px-1.5 text-[10px] font-semibold"
|
||||
>
|
||||
<CreditCard className="mr-0.5 h-2.5 w-2.5" />
|
||||
{status.credits}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{status.connected && status.credits != null && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="ml-0.5 h-4 px-1.5 text-[10px] font-semibold"
|
||||
>
|
||||
<CreditCard className="mr-0.5 h-2.5 w-2.5" />
|
||||
{status.credits}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
{status.connected
|
||||
? `${status.credits ?? "?"} credite ePay disponibile`
|
||||
: error
|
||||
? error
|
||||
: connecting
|
||||
? "Se conecteaza..."
|
||||
: "Se conecteaza automat la selectia UAT"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
{/* Action button */}
|
||||
{status.connected ? (
|
||||
{/* Logout button — only when connected */}
|
||||
{status.connected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-1.5 text-[10px]"
|
||||
onClick={() => void disconnect()}
|
||||
title="Deconectare ePay"
|
||||
>
|
||||
<LogOut className="h-3 w-3" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-[10px]"
|
||||
disabled={connecting}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
) : null}
|
||||
Conectare
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Error tooltip */}
|
||||
{error && !status.connected && (
|
||||
<span className="text-[10px] text-rose-500 max-w-40 truncate" title={error}>
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user