From 72abf05a5f4097d877dbba6636ac547939f4c630 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 22 Jun 2026 15:54:02 +0330 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20review=20fixes=20=E2=80=94=20?= =?UTF-8?q?error=20toasts,=20dedupe=20socket,=20POS=20guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Global MutationCache.onError safety net so mutations without their own onError no longer fail silently (skips ones that handle errors → no double toast). - Notifications feed no longer opens its own SignalR connection; it reuses the one in useOrderAlerts (was double sockets + double cache churn per session). - "Send test notification" now works on the settings page (force flag bypasses the tab-visible guard) instead of silently doing nothing. - POS: re-entry guard on payment confirm (no duplicate payment on double-tap); notes on already-sent lines are read-only (a note-only edit was silently lost); ORDER_ALREADY_CLOSED surfaced with a clear Persian message. - Reservation Confirm/Cancel/Complete buttons disabled while pending. Co-Authored-By: Claude Opus 4.8 --- .../src/components/pos2/pos2-screen.tsx | 26 ++++++++-- web/dashboard/src/components/providers.tsx | 17 ++++++- .../reservations/reservations-screen.tsx | 3 ++ .../settings/settings-notifications-panel.tsx | 2 +- .../src/lib/hooks/use-notifications-feed.ts | 47 ++++--------------- .../use-desktop-notifications.ts | 4 +- 6 files changed, 53 insertions(+), 46 deletions(-) diff --git a/web/dashboard/src/components/pos2/pos2-screen.tsx b/web/dashboard/src/components/pos2/pos2-screen.tsx index 4155912..c11d27d 100644 --- a/web/dashboard/src/components/pos2/pos2-screen.tsx +++ b/web/dashboard/src/components/pos2/pos2-screen.tsx @@ -7,7 +7,7 @@ // Mounted at /[locale]/pos (and /pos2). Design mirrors pos2-prototype.tsx. // ───────────────────────────────────────────────────────────────────────────── -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Search, Plus, Minus, Trash2, Send, CreditCard, SplitSquareHorizontal, @@ -123,6 +123,7 @@ export function Pos2Screen() { const [payLoyalty, setPayLoyalty] = useState(0); // Order just paid — kept after the cart is cleared so the receipt stays printable. const [paidOrderId, setPaidOrderId] = useState(null); + const payingRef = useRef(false); // re-entry guard for the payment confirm const [online, setOnline] = useState(true); useEffect(() => { @@ -275,7 +276,8 @@ export function Pos2Screen() { }; const confirmPay = async (payments: Payment[], loyaltyRedeem: number) => { - if (!payTarget) return; + if (!payTarget || payingRef.current) return; // guard against a double-tap + payingRef.current = true; setBusy(true); try { const cardTotal = payments.filter((p) => p.method === "Card").reduce((s, p) => s + p.amount, 0); @@ -306,10 +308,13 @@ export function Pos2Screen() { notify.error(posDeviceMsg(e)); } else if (e instanceof ApiClientError && e.code === "NO_OPEN_SHIFT") { notify.error("برای پرداخت باید شیفت باز باشد"); + } else if (e instanceof ApiClientError && e.code === "ORDER_ALREADY_CLOSED") { + notify.error("این سفارش قبلاً تسویه شده است"); } else { notify.error(errMsg(e, "ثبت پرداخت ناموفق بود")); } } finally { + payingRef.current = false; setBusy(false); } }; @@ -355,7 +360,11 @@ export function Pos2Screen() { ); const ticketProps = { - cafeId, lines: live, subtotal, discount, tax, total, count, pendingCount, + cafeId, + // mark fully-sent lines so their note becomes read-only (a note-only change on + // an already-sent line would otherwise be silently dropped on the next send). + lines: live.map((l) => ({ ...l, synced: (syncedQty[l.menuItem.id] ?? 0) >= l.quantity })), + subtotal, discount, tax, total, count, pendingCount, onBump: (id: string, d: number) => { const l = items.find((x) => x.menuItem.id === id); if (l) updateQty(id, l.quantity + d); }, onRemove: removeItem, onSend: send, onPay: openPay, onSplit: openPay, onNote: (id: string, notes: string) => setNotes(id, notes), @@ -732,7 +741,7 @@ function Pos2Extras({ cafeId }: { cafeId: string }) { } // ── Order ticket ───────────────────────────────────────────────────────────── -type TicketLine = { menuItem: MenuItem; quantity: number; notes?: string }; +type TicketLine = { menuItem: MenuItem; quantity: number; notes?: string; synced?: boolean }; function Ticket({ cafeId, lines, subtotal, discount, tax, total, count, pendingCount, onBump, onRemove, onNote, onSend, onPay, onSplit, canPrint, onPrintReceipt, }: { @@ -775,7 +784,14 @@ function Ticket({ - {noteFor === l.menuItem.id || l.notes ? ( + {l.synced ? ( + // Already sent to the kitchen — note is read-only (can't be changed now). + l.notes ? ( +

+ {l.notes} +

+ ) : null + ) : noteFor === l.menuItem.id || l.notes ? ( onNote(l.menuItem.id, e.target.value)} diff --git a/web/dashboard/src/components/providers.tsx b/web/dashboard/src/components/providers.tsx index d50ca10..00ebeee 100644 --- a/web/dashboard/src/components/providers.tsx +++ b/web/dashboard/src/components/providers.tsx @@ -1,16 +1,31 @@ "use client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider, MutationCache } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { ConfirmProvider } from "@/components/providers/confirm-provider"; import { MeeziToaster } from "@/components/ui/meezi-toaster"; import { useAuthStore } from "@/lib/stores/auth.store"; import { restoreQueryCache, startPersisting } from "@/lib/offline/query-persister"; +import { notify, getErrorMessage } from "@/lib/notify"; + +/** Generic, locale-aware fallback for mutations that don't handle their own error. */ +function globalMutationErrorFallback(): string { + const lang = typeof document !== "undefined" ? document.documentElement.lang : "fa"; + return lang === "en" ? "Something went wrong" : lang === "ar" ? "حدث خطأ ما" : "خطایی رخ داد"; +} export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( () => new QueryClient({ + // Safety net: surface a toast for any mutation that doesn't define its own + // onError (skips ones that already handle it, so no double toasts). + mutationCache: new MutationCache({ + onError: (error, _vars, _ctx, mutation) => { + if (mutation.options.onError) return; + notify.error(getErrorMessage(error, globalMutationErrorFallback())); + }, + }), defaultOptions: { queries: { staleTime: 30_000, diff --git a/web/dashboard/src/components/reservations/reservations-screen.tsx b/web/dashboard/src/components/reservations/reservations-screen.tsx index 36d86e8..a5c2917 100644 --- a/web/dashboard/src/components/reservations/reservations-screen.tsx +++ b/web/dashboard/src/components/reservations/reservations-screen.tsx @@ -234,6 +234,7 @@ export function ReservationsScreen() {