diff --git a/web/dashboard/src/components/layout/sync-status-indicator.tsx b/web/dashboard/src/components/layout/sync-status-indicator.tsx index 4a3f5d9..e018c0a 100644 --- a/web/dashboard/src/components/layout/sync-status-indicator.tsx +++ b/web/dashboard/src/components/layout/sync-status-indicator.tsx @@ -1,61 +1,76 @@ "use client"; -import { WifiOff, CloudUpload, RefreshCw } from "lucide-react"; +import { WifiOff, CloudUpload, RefreshCw, AlertTriangle } from "lucide-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useLocale } from "next-intl"; import { cn } from "@/lib/utils"; import { useSyncQueueStore } from "@/lib/stores/sync-queue.store"; -import { useLocale } from "next-intl"; +import { getQueueCount } from "@/lib/offline/offline-db"; import { - getAllQueueItems, - getQueueCount, - removeQueueItem, - markQueueItemFailed, -} from "@/lib/offline/offline-db"; -import { apiPost } from "@/lib/api/client"; - -/** Manual retry — fires one sync pass immediately (used as onClick). */ -async function runManualSync( - setSyncing: (v: boolean) => void, - setQueueCount: (n: number) => void -) { - if (!navigator.onLine) return; - setSyncing(true); - try { - const items = await getAllQueueItems(); - for (const item of items) { - try { - if (item.type === "create_order") { - const { cafeId, body } = item.payload as { cafeId: string; body: unknown }; - await apiPost(`/api/cafes/${cafeId}/orders`, body as Record); - } else if (item.type === "add_items") { - const { cafeId, orderId, body } = item.payload as { - cafeId: string; - orderId: string; - body: unknown; - }; - await apiPost( - `/api/cafes/${cafeId}/orders/${orderId}/items`, - body as Record - ); - } - await removeQueueItem(item.id); - } catch { - await markQueueItemFailed(item.id); - } - } - } finally { - setSyncing(false); - setQueueCount(await getQueueCount()); - } -} + drainOutbox, + getActiveOutboxCount, + getFailedOutboxCount, + discardFailedOps, +} from "@/lib/offline/outbox"; export function SyncStatusIndicator() { - const { queueCount, isSyncing, isOnline, setSyncing, setQueueCount } = - useSyncQueueStore(); + const { + queueCount, + failedCount, + isSyncing, + isOnline, + setSyncing, + setQueueCount, + setFailedCount, + } = useSyncQueueStore(); + const queryClient = useQueryClient(); const locale = useLocale(); const isFa = locale !== "en"; - const show = !isOnline || queueCount > 0 || isSyncing; - if (!show) return null; + const recount = async () => { + setQueueCount((await getActiveOutboxCount()) + (await getQueueCount())); + setFailedCount(await getFailedOutboxCount()); + }; + + // Manual retry — drains the REAL outbox (the engine the app actually uses), + // then refreshes server data and the counts. + const retry = async () => { + if (typeof navigator !== "undefined" && !navigator.onLine) return; + if (isSyncing) return; + setSyncing(true); + try { + const res = await drainOutbox(); + if (res.sent > 0) await queryClient.invalidateQueries(); + } finally { + setSyncing(false); + await recount(); + } + }; + + // Poisoned ops can never sync (permanent 4xx) — let the user clear them so the + // badge doesn't sit stuck forever. + const clearFailed = async () => { + await discardFailedOps(); + await recount(); + }; + + const showPending = !isOnline || queueCount > 0 || isSyncing; + const showFailed = !showPending && failedCount > 0; + if (!showPending && !showFailed) return null; + + if (showFailed) { + return ( + + ); + } const label = isFa ? !isOnline @@ -72,13 +87,9 @@ export function SyncStatusIndicator() { return (