diff --git a/src/modules/parcel-sync/components/epay-connect.tsx b/src/modules/parcel-sync/components/epay-connect.tsx index 80281d8..f4b35c3 100644 --- a/src/modules/parcel-sync/components/epay-connect.tsx +++ b/src/modules/parcel-sync/components/epay-connect.tsx @@ -141,17 +141,21 @@ export function EpayConnect({ if (cancelled) return; setError("Eroare retea"); shouldRetry = attempt < maxRetries; + } finally { + // ALWAYS clear the connecting spinner unless we're about to retry — + // including the `cancelled` early-returns above. Otherwise a re-run + // of this effect (e.g. when status.connected flips true) cancels the + // in-flight attempt and leaves connecting stuck true → a perpetual + // spinner on an already-connected (green) pill. + if (!shouldRetry) setConnecting(false); } if (cancelled) return; if (shouldRetry) { - // Keep connecting state true during retry wait autoConnectTimerRef.current = setTimeout(() => { void attemptConnect(attempt + 1); }, 3000); - } else { - setConnecting(false); } }; @@ -164,7 +168,11 @@ export function EpayConnect({ autoConnectTimerRef.current = null; } }; - }, [triggerConnect, status.connected, connecting, fetchStatus]); + // `connecting` intentionally excluded: setConnecting(true) inside this + // effect would otherwise re-trigger it and cancel its own in-flight + // attempt. autoConnectAttempted (a ref) already prevents double-starts. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [triggerConnect, status.connected, fetchStatus]); const disconnect = async () => { try { @@ -202,10 +210,10 @@ export function EpayConnect({ : "border-muted-foreground/20 bg-muted/50 text-muted-foreground", )} > - {connecting ? ( - - ) : status.connected ? ( + {status.connected ? ( + ) : connecting ? ( + ) : null} ePay diff --git a/src/modules/parcel-sync/components/epay-order-button.tsx b/src/modules/parcel-sync/components/epay-order-button.tsx index 314e2e2..c3a1651 100644 --- a/src/modules/parcel-sync/components/epay-order-button.tsx +++ b/src/modules/parcel-sync/components/epay-order-button.tsx @@ -57,6 +57,10 @@ export function EpayOrderButton({ const [ordering, setOrdering] = useState(false); const [ordered, setOrdered] = useState(false); + // After enqueue the order keeps processing in the background (~1–2 min on + // the legacy queue): cart → submit → poll → download. Show that instead of + // flipping straight to a misleading "valid" the instant it's queued. + const [processing, setProcessing] = useState(false); const [error, setError] = useState(""); const [epayStatus, setEpayStatus] = useState({ connected: false, @@ -111,7 +115,10 @@ export function EpayOrderButton({ }); if (mountedRef.current) { if (result.ok) { - setOrdered(true); + // Queued, not done — enter the processing state and let the poll + // effect below flip to "valid" only once the extract is actually + // ready (or surface a failure). + setProcessing(true); } else { setError(result.error ?? "Eroare comanda"); } @@ -119,6 +126,36 @@ export function EpayOrderButton({ } }, [nrCadastral, siruta, judetName, uatName, useGisAc]); + // Poll while processing: flip to "valid" only when a completed extract + // actually exists. Caps at ~3 min, then stops (the parent list refresh + // will reflect the final state). + useEffect(() => { + if (!processing) return; + let cancelled = false; + let attempts = 0; + const tick = async () => { + attempts += 1; + try { + const has = await fetchCfHasCompletedForCadastral(useGisAc, nrCadastral); + if (cancelled) return; + if (has) { + setOrdered(true); + setProcessing(false); + return; + } + } catch { + /* keep polling */ + } + if (!cancelled && attempts >= 36) setProcessing(false); // ~3 min + }; + const id = setInterval(() => void tick(), 5000); + void tick(); + return () => { + cancelled = true; + clearInterval(id); + }; + }, [processing, useGisAc, nrCadastral]); + // On the (future) gis.ac path, the orchestrator dispatches ePay calls // through a shared account pool — no personally-connected ePay session // needed. The legacy queue (current route while the guard is on) @@ -143,6 +180,29 @@ export function EpayOrderButton({ return tooltipText ?? "Comanda extras CF (1 credit)"; }; + if (processing) { + return ( + + + + + + + Comanda CF este în curs (coș → plată → descărcare, ~1–2 min) + + + + ); + } + if (ordered) { return (