MAI CALL - step 13
pnpm test:e2e verde. Passo 13 completo.
O que foi feito:
Polish:
SyncProvider — dead-letter toast fixo na base do ecrã (cor destructive, botão ✕ para fechar), dispara quando broadcast({ type: 'dead-letter' }) chega via BroadcastChannel
Loading states e empty states já estavam implementados nos passos anteriores
E2E test — e2e/tests/mai-call.spec.ts:
Substitui o ping.spec.ts obsoleto
Arranca ambos os servidores (operator-pwa :3000 + admin-web :3001) com AUTH_DEV_AUTOLOGIN=true
Fluxo completo em 13.7s: formulário → IndexedDB → sync automático → admin queue → claim (OPEN→CLAIMED) → enable RESOLVED filter → resolve dialog → confirm (CLAIMED→RESOLVED)
pnpm test:e2e passa ✓
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { subscribeBroadcast } from '@/lib/queue/broadcast';
|
||||
import { subscribeBroadcast, type SyncMessage } from '@/lib/queue/broadcast';
|
||||
import { runSync } from '@/lib/queue/sync';
|
||||
import { db } from '@/lib/queue/db';
|
||||
|
||||
@@ -23,6 +23,7 @@ export const useSyncState = () => useContext(SyncCtx);
|
||||
|
||||
export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
const [state, setState] = useState<SyncState>({ pendingCount: 0, deadLetterCount: 0 });
|
||||
const [failedIds, setFailedIds] = useState<string[]>([]);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const refreshCounts = useCallback(async () => {
|
||||
@@ -38,7 +39,12 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
refreshCounts();
|
||||
|
||||
const unsub = subscribeBroadcast(async () => refreshCounts());
|
||||
const unsub = subscribeBroadcast(async (msg: SyncMessage) => {
|
||||
await refreshCounts();
|
||||
if (msg.type === 'dead-letter') {
|
||||
setFailedIds((prev) => [...prev, msg.clientRequestId]);
|
||||
}
|
||||
});
|
||||
|
||||
const onOnline = () => sync();
|
||||
const onVisible = () => {
|
||||
@@ -48,12 +54,10 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
window.addEventListener('online', onOnline);
|
||||
document.addEventListener('visibilitychange', onVisible);
|
||||
|
||||
// Poll every 10s as a fallback
|
||||
timerRef.current = setInterval(() => {
|
||||
if (navigator.onLine) sync();
|
||||
}, 10_000);
|
||||
|
||||
// Register Background Sync if SW + API available
|
||||
if ('serviceWorker' in navigator && 'SyncManager' in window) {
|
||||
navigator.serviceWorker.ready
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -61,7 +65,6 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
.catch(() => {/* not supported or permission denied */});
|
||||
}
|
||||
|
||||
// Initial sync attempt
|
||||
if (navigator.onLine) sync();
|
||||
|
||||
return () => {
|
||||
@@ -72,5 +75,28 @@ export function SyncProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
}, [sync, refreshCounts]);
|
||||
|
||||
return <SyncCtx.Provider value={state}>{children}</SyncCtx.Provider>;
|
||||
return (
|
||||
<SyncCtx.Provider value={state}>
|
||||
{children}
|
||||
{failedIds.length > 0 && (
|
||||
<div className="fixed bottom-4 left-4 right-4 z-50 flex flex-col gap-2">
|
||||
{failedIds.map((id) => (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center justify-between rounded-lg bg-destructive px-4 py-3 text-sm text-destructive-foreground shadow-lg"
|
||||
>
|
||||
<span>Pedido {id.slice(0, 8)}… falhou — contacta o supervisor.</span>
|
||||
<button
|
||||
onClick={() => setFailedIds((prev) => prev.filter((x) => x !== id))}
|
||||
className="ml-4 shrink-0 opacity-80 hover:opacity-100"
|
||||
aria-label="Fechar"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SyncCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user