fix(ancpi): UAT debounce + list tooltips + expired download + ePay retry
1. UAT search: 150ms debounce prevents slow re-renders on keystroke 2. Lista mea tooltips: "Scoate Extrase CF" shows exact credit cost, status badges show expiry dates and clear instructions 3. Expired extracts: both Descarcă (old version) + Actualizează shown 4. ePay auto-connect: retry 2x with 3s delay, check session before connect, re-attempt on disconnect detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ export function EpayConnect({
|
||||
const cbRef = useRef(onStatusChange);
|
||||
cbRef.current = onStatusChange;
|
||||
const autoConnectAttempted = useRef(false);
|
||||
const autoConnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
@@ -55,15 +56,23 @@ export function EpayConnect({
|
||||
setStatus(data);
|
||||
cbRef.current?.(data);
|
||||
if (data.connected) setError("");
|
||||
return data;
|
||||
} catch {
|
||||
/* silent */
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Poll every 30s
|
||||
// Poll every 30s — detect disconnection and allow re-connect
|
||||
useEffect(() => {
|
||||
void fetchStatus();
|
||||
pollRef.current = setInterval(() => void fetchStatus(), 30_000);
|
||||
pollRef.current = setInterval(() => {
|
||||
void fetchStatus().then((data) => {
|
||||
if (data && !data.connected && autoConnectAttempted.current) {
|
||||
autoConnectAttempted.current = false;
|
||||
}
|
||||
});
|
||||
}, 30_000);
|
||||
return () => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
};
|
||||
@@ -92,13 +101,70 @@ export function EpayConnect({
|
||||
}
|
||||
}, [connecting, status.connected, fetchStatus]);
|
||||
|
||||
// Auto-connect when triggerConnect becomes true
|
||||
// Auto-connect when triggerConnect becomes true, with retry on failure
|
||||
useEffect(() => {
|
||||
if (triggerConnect && !status.connected && !connecting && !autoConnectAttempted.current) {
|
||||
autoConnectAttempted.current = true;
|
||||
void connect();
|
||||
}
|
||||
}, [triggerConnect, status.connected, connecting, connect]);
|
||||
if (!triggerConnect || status.connected || connecting || autoConnectAttempted.current) return;
|
||||
autoConnectAttempted.current = true;
|
||||
|
||||
let cancelled = false;
|
||||
const maxRetries = 2;
|
||||
|
||||
const attemptConnect = async (attempt: number) => {
|
||||
if (cancelled) return;
|
||||
|
||||
// On first attempt, check session to avoid unnecessary connect
|
||||
if (attempt === 0) {
|
||||
const current = await fetchStatus();
|
||||
if (cancelled) return;
|
||||
if (current?.connected) return;
|
||||
}
|
||||
|
||||
setConnecting(true);
|
||||
setError("");
|
||||
let shouldRetry = false;
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const data = (await res.json()) as { success?: boolean; credits?: number; error?: string };
|
||||
if (cancelled) return;
|
||||
|
||||
if (!res.ok || data.error) {
|
||||
setError(data.error ?? "Eroare conectare ePay");
|
||||
shouldRetry = attempt < maxRetries;
|
||||
} else {
|
||||
await fetchStatus();
|
||||
}
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setError("Eroare retea");
|
||||
shouldRetry = attempt < maxRetries;
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (shouldRetry) {
|
||||
// Keep connecting state true during retry wait
|
||||
autoConnectTimerRef.current = setTimeout(() => {
|
||||
void attemptConnect(attempt + 1);
|
||||
}, 3000);
|
||||
} else {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
void attemptConnect(0);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (autoConnectTimerRef.current) {
|
||||
clearTimeout(autoConnectTimerRef.current);
|
||||
autoConnectTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [triggerConnect, status.connected, connecting, fetchStatus]);
|
||||
|
||||
const disconnect = async () => {
|
||||
try {
|
||||
@@ -108,6 +174,10 @@ export function EpayConnect({
|
||||
body: JSON.stringify({ action: "disconnect" }),
|
||||
});
|
||||
autoConnectAttempted.current = false;
|
||||
if (autoConnectTimerRef.current) {
|
||||
clearTimeout(autoConnectTimerRef.current);
|
||||
autoConnectTimerRef.current = null;
|
||||
}
|
||||
await fetchStatus();
|
||||
} catch {
|
||||
/* silent */
|
||||
|
||||
@@ -750,6 +750,32 @@ export function EpayTab() {
|
||||
</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={`/api/ancpi/download?id=${order.id}`}
|
||||
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>
|
||||
@@ -774,25 +800,6 @@ export function EpayTab() {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{order.status === "completed" &&
|
||||
order.minioPath &&
|
||||
expired && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs text-muted-foreground"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`/api/ancpi/download?id=${order.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Download className="h-3 w-3 mr-1" />
|
||||
PDF
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -621,27 +621,30 @@ export function ParcelSyncModule() {
|
||||
setUatResults([]);
|
||||
return;
|
||||
}
|
||||
const isDigit = /^\d+$/.test(raw);
|
||||
const query = normalizeText(raw);
|
||||
// Filter and sort: UAT name matches first, then county-only matches
|
||||
const nameMatches: typeof uatData = [];
|
||||
const countyOnlyMatches: typeof uatData = [];
|
||||
const timer = setTimeout(() => {
|
||||
const isDigit = /^\d+$/.test(raw);
|
||||
const query = normalizeText(raw);
|
||||
// Filter and sort: UAT name matches first, then county-only matches
|
||||
const nameMatches: typeof uatData = [];
|
||||
const countyOnlyMatches: typeof uatData = [];
|
||||
|
||||
for (const item of uatData) {
|
||||
if (isDigit) {
|
||||
if (item.siruta.startsWith(raw)) nameMatches.push(item);
|
||||
} else {
|
||||
const nameMatch = normalizeText(item.name).includes(query);
|
||||
const countyMatch =
|
||||
item.county && normalizeText(item.county).includes(query);
|
||||
if (nameMatch) nameMatches.push(item);
|
||||
else if (countyMatch) countyOnlyMatches.push(item);
|
||||
for (const item of uatData) {
|
||||
if (isDigit) {
|
||||
if (item.siruta.startsWith(raw)) nameMatches.push(item);
|
||||
} else {
|
||||
const nameMatch = normalizeText(item.name).includes(query);
|
||||
const countyMatch =
|
||||
item.county && normalizeText(item.county).includes(query);
|
||||
if (nameMatch) nameMatches.push(item);
|
||||
else if (countyMatch) countyOnlyMatches.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UAT name matches first (priority), then county-only matches
|
||||
const results = [...nameMatches, ...countyOnlyMatches].slice(0, 12);
|
||||
setUatResults(results);
|
||||
// UAT name matches first (priority), then county-only matches
|
||||
const results = [...nameMatches, ...countyOnlyMatches].slice(0, 12);
|
||||
setUatResults(results);
|
||||
}, 150);
|
||||
return () => clearTimeout(timer);
|
||||
}, [uatQuery, uatData]);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
@@ -2925,37 +2928,65 @@ export function ParcelSyncModule() {
|
||||
CSV din lista
|
||||
</Button>
|
||||
{/* Download all valid CF extracts as ZIP */}
|
||||
{searchList.some((p) => cfStatusMap[p.nrCad] === "valid") && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400"
|
||||
disabled={listCfDownloading}
|
||||
onClick={() => void handleListCfDownloadZip()}
|
||||
>
|
||||
{listCfDownloading ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Archive className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
Descarca Extrase CF
|
||||
</Button>
|
||||
)}
|
||||
{searchList.some((p) => cfStatusMap[p.nrCad] === "valid") && (() => {
|
||||
const validCount = searchList.filter((p) => cfStatusMap[p.nrCad] === "valid").length;
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400"
|
||||
disabled={listCfDownloading}
|
||||
onClick={() => void handleListCfDownloadZip()}
|
||||
>
|
||||
{listCfDownloading ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Archive className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
Descarca Extrase CF
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{`Descarca ZIP cu ${validCount} extrase valide din lista`}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})()}
|
||||
{/* Order CF extracts for list */}
|
||||
{epayStatus.connected && (
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={listCfOrdering}
|
||||
onClick={() => void handleListCfOrder()}
|
||||
>
|
||||
{listCfOrdering ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<FileText className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
Scoate Extrase CF
|
||||
</Button>
|
||||
)}
|
||||
{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 (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={listCfOrdering}
|
||||
onClick={() => void handleListCfOrder()}
|
||||
>
|
||||
{listCfOrdering ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<FileText className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
Scoate Extrase CF
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{`Comanda ${newCount} extrase noi + ${updateCount} actualizari = ${totalCredits} credite. ${validCount} existente valide raman.`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2999,6 +3030,7 @@ export function ParcelSyncModule() {
|
||||
<tbody>
|
||||
{searchList.map((p, idx) => {
|
||||
const cfStatus = cfStatusMap[p.nrCad];
|
||||
const cfExpiry = cfExpiryDates[p.nrCad];
|
||||
return (
|
||||
<tr
|
||||
key={`list-${p.nrCad}-${p.immovablePk}`}
|
||||
@@ -3022,23 +3054,38 @@ export function ParcelSyncModule() {
|
||||
{p.proprietari || "\u2014"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{cfStatus === "valid" ? (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800">
|
||||
Valid
|
||||
</span>
|
||||
) : cfStatus === "expired" ? (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950/40 dark:text-orange-400 dark:border-orange-800">
|
||||
Expirat
|
||||
</span>
|
||||
) : cfStatus === "processing" ? (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-950/40 dark:text-yellow-400 dark:border-yellow-800 animate-pulse">
|
||||
Procesare
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-muted text-muted-foreground border-muted-foreground/20">
|
||||
Lipsa
|
||||
</span>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{cfStatus === "valid" ? (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800 cursor-default">
|
||||
Valid
|
||||
</span>
|
||||
) : cfStatus === "expired" ? (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950/40 dark:text-orange-400 dark:border-orange-800 cursor-default">
|
||||
Expirat
|
||||
</span>
|
||||
) : cfStatus === "processing" ? (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-950/40 dark:text-yellow-400 dark:border-yellow-800 animate-pulse cursor-default">
|
||||
Procesare
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium bg-muted text-muted-foreground border-muted-foreground/20 cursor-default">
|
||||
Lipsa
|
||||
</span>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{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."}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user