fix(pos): POS v2 menu empty — resolve a valid branch like classic POS
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 31s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m37s

The menu/tables are branch-scoped. v2 used the raw stored branchId, which is
null or stale for users who never opened the classic POS (it has no branch
picker), so getBranchMenu returned an empty menu. Now v2 fetches /branches,
auto-selects the first valid branch (self-healing the stored id), and loads the
branch menu + tables + order submission against that resolved branch — matching
the classic POS exactly. Also adds a visible "menu failed to load / retry"
state instead of a silent empty grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 06:17:37 +03:30
parent cc0933c514
commit f02f78a97c
@@ -8,7 +8,7 @@
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
Search, Plus, Minus, Trash2, Send, CreditCard, Pause, SplitSquareHorizontal, Search, Plus, Minus, Trash2, Send, CreditCard, Pause, SplitSquareHorizontal,
X, WifiOff, ShoppingCart, Users, Coffee, ArrowRight, LayoutGrid, Armchair, X, WifiOff, ShoppingCart, Users, Coffee, ArrowRight, LayoutGrid, Armchair,
@@ -64,10 +64,29 @@ export function Pos2Screen() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const cafeId = useAuthStore((s) => s.user?.cafeId); const cafeId = useAuthStore((s) => s.user?.cafeId);
const branchId = useBranchStore((s) => s.branchId); const branchId = useBranchStore((s) => s.branchId);
const setBranchId = useBranchStore((s) => s.setBranchId);
// Resolve a VALID branch (auto-pick the first) exactly like the classic POS —
// the menu/tables are branch-scoped, so a null or stale stored branchId would
// otherwise load an empty menu. v2 has no branch picker, so it must self-heal.
const { data: branches = [] } = useQuery({
queryKey: ["branches", cafeId],
queryFn: () => apiGet<{ id: string; name: string }[]>(`/api/cafes/${cafeId}/branches`),
enabled: !!cafeId,
});
useEffect(() => {
if (branches.length === 0) return;
const valid = branchId && branches.some((b) => b.id === branchId);
if (!valid) setBranchId(branches[0]!.id);
}, [branches, branchId, setBranchId]);
const orderBranchId = useMemo(() => {
if (branchId && branches.some((b) => b.id === branchId)) return branchId;
return branches[0]?.id ?? null;
}, [branchId, branches]);
const { data: categories } = usePos2Categories(cafeId); const { data: categories } = usePos2Categories(cafeId);
const { data: menu, isLoading: menuLoading } = usePos2Menu(cafeId, branchId); const { data: menu, isLoading: menuLoading, isError: menuError, refetch: refetchMenu } = usePos2Menu(cafeId, orderBranchId);
const { data: tables, isLoading: tablesLoading, refetch: refetchTables } = usePos2Tables(cafeId, branchId); const { data: tables, isLoading: tablesLoading, refetch: refetchTables } = usePos2Tables(cafeId, orderBranchId);
const menuById = useMenuById(menu); const menuById = useMenuById(menu);
// cart store slices // cart store slices
@@ -175,7 +194,7 @@ export function Pos2Screen() {
if (cart.getPendingLines().length === 0) return null; if (cart.getPendingLines().length === 0) return null;
const order = await submitOrderToApi({ const order = await submitOrderToApi({
cafeId: cafeId as string, cafeId: cafeId as string,
orderBranchId: branchId ?? undefined, orderBranchId: orderBranchId ?? undefined,
cart, cart,
reservationId: null, reservationId: null,
cartItems: cart.items, cartItems: cart.items,
@@ -249,7 +268,7 @@ export function Pos2Screen() {
setBusy(true); setBusy(true);
try { try {
const cardTotal = payments.filter((p) => p.method === "Card").reduce((s, p) => s + p.amount, 0); const cardTotal = payments.filter((p) => p.method === "Card").reduce((s, p) => s + p.amount, 0);
const payBranchId = payTarget.branchId ?? branchId ?? undefined; const payBranchId = payTarget.branchId ?? orderBranchId ?? undefined;
if (cardTotal > 0 && payBranchId) { if (cardTotal > 0 && payBranchId) {
// push the card amount to the configured terminal (no-op/skip if none) // push the card amount to the configured terminal (no-op/skip if none)
await requestPosPayment(cafeId as string, payBranchId, payTarget.id, cardTotal); await requestPosPayment(cafeId as string, payBranchId, payTarget.id, cardTotal);
@@ -448,6 +467,13 @@ export function Pos2Screen() {
<div className="flex flex-1 items-center justify-center text-muted-foreground"> <div className="flex flex-1 items-center justify-center text-muted-foreground">
<Loader2 className="size-6 animate-spin" /> <Loader2 className="size-6 animate-spin" />
</div> </div>
) : menuError ? (
<div className="flex flex-1 flex-col items-center justify-center gap-3 p-6 text-center text-muted-foreground">
<p>بارگذاری منو ناموفق بود.</p>
<button type="button" onClick={() => refetchMenu()} className="rounded-xl bg-primary px-5 py-2.5 font-bold text-primary-foreground">
تلاش دوباره
</button>
</div>
) : ( ) : (
<div className="grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-3 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"> <div className="grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-3 overflow-y-auto p-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{visibleItems.map((it) => ( {visibleItems.map((it) => (