fix(pos2): wait for branch before fetching menu + add left category sidebar
CI/CD / CI · API (dotnet build + test) (push) Failing after 3m20s
CI/CD / CI · Admin API (dotnet build) (push) Failing after 3m19s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 41s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 52s
CI/CD / Deploy · all services (push) Has been cancelled

Race fix: orderBranchId now returns `undefined` (not null) while the /branches
query is in flight. usePos2Menu treats undefined as "not yet determined" and
skips the fetch, preventing getBranchMenu(cafeId, null) → empty array.
Once branchesFetched=true, orderBranchId resolves to the correct branchId
(or null for café-wide fallback).

Layout: desktop order screen now shows a left vertical category sidebar
(116 px, md+) instead of horizontal chips, giving the classic POS sidebar
feel. Horizontal chips kept for mobile (<md). Menu grid columns adjusted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 07:10:03 +03:30
parent f02f78a97c
commit 59486cdf24
2 changed files with 46 additions and 11 deletions
@@ -69,20 +69,25 @@ export function Pos2Screen() {
// 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({
//
// IMPORTANT: orderBranchId returns `undefined` while branches are still loading.
// usePos2Menu treats `undefined` as "not yet determined" and pauses the query so
// we never fire getBranchMenu(cafeId, null) which returns an empty array.
const { data: branches = [], isFetched: branchesFetched } = useQuery({
queryKey: ["branches", cafeId],
queryFn: () => apiGet<{ id: string; name: string }[]>(`/api/cafes/${cafeId}/branches`),
enabled: !!cafeId,
});
useEffect(() => {
if (branches.length === 0) return;
if (!branchesFetched || 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(() => {
}, [branchesFetched, branches, branchId, setBranchId]);
const orderBranchId = useMemo<string | null | undefined>(() => {
if (!branchesFetched) return undefined; // still loading → pause the menu query
if (branchId && branches.some((b) => b.id === branchId)) return branchId;
return branches[0]?.id ?? null;
}, [branchId, branches]);
return branches[0]?.id ?? null; // null = no branches → café-wide fallback
}, [branchesFetched, branchId, branches]);
const { data: categories } = usePos2Categories(cafeId);
const { data: menu, isLoading: menuLoading, isError: menuError, refetch: refetchMenu } = usePos2Menu(cafeId, orderBranchId);
@@ -447,8 +452,29 @@ export function Pos2Screen() {
</header>
<div className="flex min-h-0 flex-1">
{/* ── Left: vertical category sidebar (desktop) ── */}
<nav className="hidden w-[116px] shrink-0 flex-col gap-0.5 overflow-y-auto border-e border-border bg-card p-2 md:flex">
{catChips.map((c) => (
<button
key={c.id}
type="button"
onClick={() => setCat(c.id)}
className={cn(
"w-full cursor-pointer rounded-xl px-2 py-3 text-center text-xs font-semibold leading-tight transition-colors",
cat === c.id
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-accent hover:text-foreground",
)}
>
{c.name}
</button>
))}
</nav>
{/* ── Center: menu items ── */}
<main className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="flex shrink-0 gap-2 overflow-x-auto border-b border-border px-4 py-2.5">
{/* Horizontal chips — mobile only */}
<div className="flex shrink-0 gap-2 overflow-x-auto border-b border-border px-4 py-2.5 md:hidden">
{catChips.map((c) => (
<button
key={c.id}
@@ -463,6 +489,7 @@ export function Pos2Screen() {
</button>
))}
</div>
{menuLoading ? (
<div className="flex flex-1 items-center justify-center text-muted-foreground">
<Loader2 className="size-6 animate-spin" />
@@ -475,7 +502,7 @@ export function Pos2Screen() {
</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-3 sm:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4">
{visibleItems.map((it) => (
<button
key={it.id}
@@ -499,7 +526,8 @@ export function Pos2Screen() {
)}
</main>
<aside className="hidden w-[380px] shrink-0 border-s border-border bg-card lg:flex lg:flex-col">
{/* ── Right: order ticket (desktop) ── */}
<aside className="hidden w-[360px] shrink-0 border-s border-border bg-card lg:flex lg:flex-col">
<Ticket {...ticketProps} />
</aside>
</div>
+9 -2
View File
@@ -22,7 +22,13 @@ export function usePos2Categories(cafeId?: string | null) {
}
/** Branch-scoped menu (effective prices) when a branch is selected; otherwise the
* café-wide menu. Both normalize to MenuItem so the cart store can consume them. */
* café-wide menu. Both normalize to MenuItem so the cart store can consume them.
*
* Pass `branchId = undefined` (not null) while still determining which branch to
* use — the query will pause until branchId is resolved. Once resolved:
* • string → branch-scoped menu via getBranchMenu
* • null → café-wide fallback via /menu/items
*/
export function usePos2Menu(cafeId?: string | null, branchId?: string | null) {
return useQuery({
queryKey: ["pos2-menu", cafeId, branchId ?? "cafe"],
@@ -33,7 +39,8 @@ export function usePos2Menu(cafeId?: string | null, branchId?: string | null) {
}
return apiGet<MenuItem[]>(`/api/cafes/${cafeId}/menu/items`);
},
enabled: !!cafeId,
// branchId === undefined means "still determining" — don't fire yet
enabled: !!cafeId && branchId !== undefined,
staleTime: 30_000,
});
}