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);
|
const cbRef = useRef(onStatusChange);
|
||||||
cbRef.current = onStatusChange;
|
cbRef.current = onStatusChange;
|
||||||
const autoConnectAttempted = useRef(false);
|
const autoConnectAttempted = useRef(false);
|
||||||
|
const autoConnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const fetchStatus = useCallback(async () => {
|
const fetchStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -55,15 +56,23 @@ export function EpayConnect({
|
|||||||
setStatus(data);
|
setStatus(data);
|
||||||
cbRef.current?.(data);
|
cbRef.current?.(data);
|
||||||
if (data.connected) setError("");
|
if (data.connected) setError("");
|
||||||
|
return data;
|
||||||
} catch {
|
} catch {
|
||||||
/* silent */
|
/* silent */
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Poll every 30s
|
// Poll every 30s — detect disconnection and allow re-connect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchStatus();
|
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 () => {
|
return () => {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (pollRef.current) clearInterval(pollRef.current);
|
||||||
};
|
};
|
||||||
@@ -92,13 +101,70 @@ export function EpayConnect({
|
|||||||
}
|
}
|
||||||
}, [connecting, status.connected, fetchStatus]);
|
}, [connecting, status.connected, fetchStatus]);
|
||||||
|
|
||||||
// Auto-connect when triggerConnect becomes true
|
// Auto-connect when triggerConnect becomes true, with retry on failure
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (triggerConnect && !status.connected && !connecting && !autoConnectAttempted.current) {
|
if (!triggerConnect || status.connected || connecting || autoConnectAttempted.current) return;
|
||||||
autoConnectAttempted.current = true;
|
autoConnectAttempted.current = true;
|
||||||
void connect();
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}, [triggerConnect, status.connected, connecting, connect]);
|
|
||||||
|
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 () => {
|
const disconnect = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -108,6 +174,10 @@ export function EpayConnect({
|
|||||||
body: JSON.stringify({ action: "disconnect" }),
|
body: JSON.stringify({ action: "disconnect" }),
|
||||||
});
|
});
|
||||||
autoConnectAttempted.current = false;
|
autoConnectAttempted.current = false;
|
||||||
|
if (autoConnectTimerRef.current) {
|
||||||
|
clearTimeout(autoConnectTimerRef.current);
|
||||||
|
autoConnectTimerRef.current = null;
|
||||||
|
}
|
||||||
await fetchStatus();
|
await fetchStatus();
|
||||||
} catch {
|
} catch {
|
||||||
/* silent */
|
/* silent */
|
||||||
|
|||||||
@@ -750,6 +750,32 @@ export function EpayTab() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</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 && (
|
{expired && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -774,25 +800,6 @@ export function EpayTab() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -621,6 +621,7 @@ export function ParcelSyncModule() {
|
|||||||
setUatResults([]);
|
setUatResults([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const timer = setTimeout(() => {
|
||||||
const isDigit = /^\d+$/.test(raw);
|
const isDigit = /^\d+$/.test(raw);
|
||||||
const query = normalizeText(raw);
|
const query = normalizeText(raw);
|
||||||
// Filter and sort: UAT name matches first, then county-only matches
|
// Filter and sort: UAT name matches first, then county-only matches
|
||||||
@@ -642,6 +643,8 @@ export function ParcelSyncModule() {
|
|||||||
// UAT name matches first (priority), then county-only matches
|
// UAT name matches first (priority), then county-only matches
|
||||||
const results = [...nameMatches, ...countyOnlyMatches].slice(0, 12);
|
const results = [...nameMatches, ...countyOnlyMatches].slice(0, 12);
|
||||||
setUatResults(results);
|
setUatResults(results);
|
||||||
|
}, 150);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}, [uatQuery, uatData]);
|
}, [uatQuery, uatData]);
|
||||||
|
|
||||||
/* ════════════════════════════════════════════════════════════ */
|
/* ════════════════════════════════════════════════════════════ */
|
||||||
@@ -2925,7 +2928,12 @@ export function ParcelSyncModule() {
|
|||||||
CSV din lista
|
CSV din lista
|
||||||
</Button>
|
</Button>
|
||||||
{/* Download all valid CF extracts as ZIP */}
|
{/* Download all valid CF extracts as ZIP */}
|
||||||
{searchList.some((p) => cfStatusMap[p.nrCad] === "valid") && (
|
{searchList.some((p) => cfStatusMap[p.nrCad] === "valid") && (() => {
|
||||||
|
const validCount = searchList.filter((p) => cfStatusMap[p.nrCad] === "valid").length;
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -2940,9 +2948,25 @@ export function ParcelSyncModule() {
|
|||||||
)}
|
)}
|
||||||
Descarca Extrase CF
|
Descarca Extrase CF
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{`Descarca ZIP cu ${validCount} extrase valide din lista`}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{/* Order CF extracts for list */}
|
{/* Order CF extracts for list */}
|
||||||
{epayStatus.connected && (
|
{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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={listCfOrdering}
|
disabled={listCfOrdering}
|
||||||
@@ -2955,7 +2979,14 @@ export function ParcelSyncModule() {
|
|||||||
)}
|
)}
|
||||||
Scoate Extrase CF
|
Scoate Extrase CF
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{`Comanda ${newCount} extrase noi + ${updateCount} actualizari = ${totalCredits} credite. ${validCount} existente valide raman.`}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2999,6 +3030,7 @@ export function ParcelSyncModule() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{searchList.map((p, idx) => {
|
{searchList.map((p, idx) => {
|
||||||
const cfStatus = cfStatusMap[p.nrCad];
|
const cfStatus = cfStatusMap[p.nrCad];
|
||||||
|
const cfExpiry = cfExpiryDates[p.nrCad];
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={`list-${p.nrCad}-${p.immovablePk}`}
|
key={`list-${p.nrCad}-${p.immovablePk}`}
|
||||||
@@ -3022,23 +3054,38 @@ export function ParcelSyncModule() {
|
|||||||
{p.proprietari || "\u2014"}
|
{p.proprietari || "\u2014"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-center">
|
<td className="px-3 py-2 text-center">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
{cfStatus === "valid" ? (
|
{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">
|
<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
|
Valid
|
||||||
</span>
|
</span>
|
||||||
) : cfStatus === "expired" ? (
|
) : 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">
|
<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
|
Expirat
|
||||||
</span>
|
</span>
|
||||||
) : cfStatus === "processing" ? (
|
) : 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">
|
<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
|
Procesare
|
||||||
</span>
|
</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">
|
<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
|
Lipsa
|
||||||
</span>
|
</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>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user