f985deb233
Two bugs made "N در صف" persist even when online: - The badge counted poisoned ops (failed after 5 retries, never removed), so it never returned to 0. Now the badge counts only retryable (active) ops; poisoned ops are tracked separately as failedCount and surfaced as a red "N failed — clear" chip the user can tap to discard them. - The manual-retry click drained the LEGACY order_queue, not the real outbox the app actually uses — so clicking did nothing for stuck items. It now drains the outbox (drainOutbox), invalidates queries on success, and recounts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
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 { getQueueCount } from "@/lib/offline/offline-db";
|
|
import {
|
|
drainOutbox,
|
|
getActiveOutboxCount,
|
|
getFailedOutboxCount,
|
|
discardFailedOps,
|
|
} from "@/lib/offline/outbox";
|
|
|
|
export function SyncStatusIndicator() {
|
|
const {
|
|
queueCount,
|
|
failedCount,
|
|
isSyncing,
|
|
isOnline,
|
|
setSyncing,
|
|
setQueueCount,
|
|
setFailedCount,
|
|
} = useSyncQueueStore();
|
|
const queryClient = useQueryClient();
|
|
const locale = useLocale();
|
|
const isFa = locale !== "en";
|
|
|
|
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 (
|
|
<button
|
|
type="button"
|
|
onClick={() => void clearFailed()}
|
|
title={isFa ? "حذف موارد ناموفق همگامسازی" : "Clear failed sync items"}
|
|
className="flex cursor-pointer items-center gap-1.5 rounded-full bg-red-100 px-2.5 py-1 text-[11px] font-medium text-red-800 transition-colors hover:bg-red-200 dark:bg-red-900/30 dark:text-red-300"
|
|
>
|
|
<AlertTriangle className="h-3 w-3 shrink-0" aria-hidden />
|
|
<span>{isFa ? `${failedCount} ناموفق — پاک کردن` : `${failedCount} failed — clear`}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
const label = isFa
|
|
? !isOnline
|
|
? "آفلاین"
|
|
: isSyncing
|
|
? "همگامسازی..."
|
|
: `${queueCount} مورد در صف`
|
|
: !isOnline
|
|
? "Offline"
|
|
: isSyncing
|
|
? "Syncing..."
|
|
: `${queueCount} pending`;
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => void retry()}
|
|
disabled={isSyncing || !isOnline}
|
|
title={isFa ? "برای همگامسازی دستی کلیک کنید" : "Click to retry sync"}
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors",
|
|
"disabled:cursor-not-allowed",
|
|
!isOnline
|
|
? "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300"
|
|
: isSyncing
|
|
? "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
|
|
: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300"
|
|
)}
|
|
>
|
|
{!isOnline ? (
|
|
<WifiOff className="h-3 w-3 shrink-0" aria-hidden />
|
|
) : isSyncing ? (
|
|
<RefreshCw className="h-3 w-3 shrink-0 animate-spin" aria-hidden />
|
|
) : (
|
|
<CloudUpload className="h-3 w-3 shrink-0" aria-hidden />
|
|
)}
|
|
<span>{label}</span>
|
|
</button>
|
|
);
|
|
}
|