diff --git a/src/modules/parcel-sync/components/epay-connect.tsx b/src/modules/parcel-sync/components/epay-connect.tsx index ddf74a0..af7f72e 100644 --- a/src/modules/parcel-sync/components/epay-connect.tsx +++ b/src/modules/parcel-sync/components/epay-connect.tsx @@ -47,6 +47,7 @@ export function EpayConnect({ const cbRef = useRef(onStatusChange); cbRef.current = onStatusChange; const autoConnectAttempted = useRef(false); + const autoConnectTimerRef = useRef | 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 */ diff --git a/src/modules/parcel-sync/components/epay-tab.tsx b/src/modules/parcel-sync/components/epay-tab.tsx index 1e16e65..adf52f5 100644 --- a/src/modules/parcel-sync/components/epay-tab.tsx +++ b/src/modules/parcel-sync/components/epay-tab.tsx @@ -750,6 +750,32 @@ export function EpayTab() { )} + {expired && order.status === "completed" && order.minioPath && ( + + + + + + + {`Descarca versiunea expirata (${formatShortDate(order.expiresAt)})`} + + + + )} {expired && ( @@ -774,25 +800,6 @@ export function EpayTab() { )} - {order.status === "completed" && - order.minioPath && - expired && ( - - )} diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 4eb07ad..0be718d 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -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 {/* 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 ( + + + + + + {`Descarca ZIP cu ${validCount} extrase valide din lista`} + + + ); + })()} {/* 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 ( + + + + + + + {`Comanda ${newCount} extrase noi + ${updateCount} actualizari = ${totalCredits} credite. ${validCount} existente valide raman.`} + + + + ); + })()} @@ -2999,6 +3030,7 @@ export function ParcelSyncModule() { {searchList.map((p, idx) => { const cfStatus = cfStatusMap[p.nrCad]; + const cfExpiry = cfExpiryDates[p.nrCad]; return ( - {cfStatus === "valid" ? ( - - Valid - - ) : cfStatus === "expired" ? ( - - Expirat - - ) : cfStatus === "processing" ? ( - - Procesare - - ) : ( - - Lipsa - - )} + + + + {cfStatus === "valid" ? ( + + Valid + + ) : cfStatus === "expired" ? ( + + Expirat + + ) : cfStatus === "processing" ? ( + + Procesare + + ) : ( + + Lipsa + + )} + + + {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."} + + +