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 (