fix(dashboard): review fixes — error toasts, dedupe socket, POS guards
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m11s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 47s
CI/CD / CI · Koja (tsc) (push) Successful in 52s
CI/CD / Deploy · all services (push) Successful in 3m20s
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m11s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 47s
CI/CD / CI · Koja (tsc) (push) Successful in 52s
CI/CD / Deploy · all services (push) Successful in 3m20s
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(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({
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
{noteFor === l.menuItem.id || l.notes ? (
|
||||
{l.synced ? (
|
||||
// Already sent to the kitchen — note is read-only (can't be changed now).
|
||||
l.notes ? (
|
||||
<p className="mt-1.5 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<StickyNote className="size-3.5 shrink-0" /> {l.notes}
|
||||
</p>
|
||||
) : null
|
||||
) : noteFor === l.menuItem.id || l.notes ? (
|
||||
<input
|
||||
value={l.notes ?? ""}
|
||||
onChange={(e) => onNote(l.menuItem.id, e.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user