Files
meezi/web/admin/src/lib/stores/cart.store.ts
T
soroush.asadi 0a33497d40 feat(admin-web): add web/admin to repo
Initial commit of the Super-Admin web panel (Next.js + TypeScript).
CI admin-web-check job was failing because the directory was never
tracked in git.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 18:46:25 +03:30

206 lines
5.4 KiB
TypeScript

import { create } from "zustand";
import type { Customer, MenuItem, Order } from "@/lib/api/types";
import { iranMobileForApi } from "@/lib/phone";
export interface CartItem {
menuItem: MenuItem;
quantity: number;
notes?: string;
orderItemId?: string;
isVoided?: boolean;
}
export interface AppliedCoupon {
id: string;
code: string;
discountAmount: number;
}
interface CartState {
items: CartItem[];
syncedQtyByMenuId: Record<string, number>;
couponCode: string;
appliedCoupon: AppliedCoupon | null;
tableId: string | null;
activeOrderId: string | null;
customerId: string | null;
guestName: string;
guestPhone: string;
getPendingLines: () => { menuItemId: string; quantity: number; notes?: string }[];
addItem: (item: MenuItem) => void;
removeItem: (menuItemId: string) => void;
updateQty: (menuItemId: string, quantity: number) => void;
setCouponCode: (code: string) => void;
setAppliedCoupon: (coupon: AppliedCoupon | null) => void;
clearCoupon: () => void;
setTableId: (tableId: string | null) => void;
setActiveOrderId: (orderId: string | null) => void;
setGuestName: (name: string) => void;
setGuestPhone: (phone: string) => void;
setCustomer: (customer: Customer | null) => void;
clearCustomer: () => void;
hydrateFromOrder: (order: Order, menuById: Map<string, MenuItem>) => void;
clearCart: () => void;
clearSession: () => void;
subtotal: () => number;
}
const clearCouponState = {
couponCode: "",
appliedCoupon: null as AppliedCoupon | null,
};
function orderLineToMenuItem(
line: Order["items"][number],
menuById: Map<string, MenuItem>
): MenuItem {
const existing = menuById.get(line.menuItemId);
if (existing) return existing;
return {
id: line.menuItemId,
categoryId: "",
name: line.menuItemName,
price: line.unitPrice,
isAvailable: true,
};
}
export const useCartStore = create<CartState>((set, get) => ({
items: [],
syncedQtyByMenuId: {},
couponCode: "",
appliedCoupon: null,
tableId: null,
activeOrderId: null,
customerId: null,
guestName: "",
guestPhone: "",
getPendingLines: () => {
const { items, syncedQtyByMenuId } = get();
const pending: { menuItemId: string; quantity: number; notes?: string }[] = [];
for (const line of items) {
const synced = syncedQtyByMenuId[line.menuItem.id] ?? 0;
const delta = line.quantity - synced;
if (delta > 0) {
pending.push({
menuItemId: line.menuItem.id,
quantity: delta,
notes: line.notes,
});
}
}
return pending;
},
addItem: (menuItem) => {
const existing = get().items.find((i) => i.menuItem.id === menuItem.id);
if (existing) {
set({
items: get().items.map((i) =>
i.menuItem.id === menuItem.id
? { ...i, quantity: i.quantity + 1 }
: i
),
...clearCouponState,
});
} else {
set({ items: [...get().items, { menuItem, quantity: 1 }], ...clearCouponState });
}
},
removeItem: (menuItemId) =>
set({
items: get().items.filter((i) => i.menuItem.id !== menuItemId),
...clearCouponState,
}),
updateQty: (menuItemId, quantity) => {
if (quantity <= 0) {
get().removeItem(menuItemId);
return;
}
set({
items: get().items.map((i) =>
i.menuItem.id === menuItemId ? { ...i, quantity } : i
),
...clearCouponState,
});
},
setCouponCode: (code) => set({ couponCode: code }),
setAppliedCoupon: (coupon) => set({ appliedCoupon: coupon }),
clearCoupon: () => set(clearCouponState),
setTableId: (tableId) => set({ tableId }),
setActiveOrderId: (activeOrderId) => set({ activeOrderId }),
setGuestName: (guestName) =>
set((s) => ({
guestName,
customerId: s.customerId && guestName !== s.guestName ? null : s.customerId,
})),
setGuestPhone: (guestPhone) =>
set((s) => ({
guestPhone,
customerId: s.customerId && guestPhone !== s.guestPhone ? null : s.customerId,
})),
setCustomer: (customer) =>
set({
customerId: customer?.id ?? null,
guestName: customer?.name ?? "",
guestPhone: customer?.phone
? (iranMobileForApi(customer.phone) ?? customer.phone)
: "",
}),
clearCustomer: () => set({ customerId: null }),
hydrateFromOrder: (order, menuById) => {
const syncedQtyByMenuId: Record<string, number> = {};
for (const line of order.items) {
syncedQtyByMenuId[line.menuItemId] = line.quantity;
}
set({
activeOrderId: order.id,
tableId: order.tableId ?? null,
customerId: order.customerId ?? null,
guestName: order.guestName ?? order.customerName ?? "",
guestPhone: order.guestPhone ?? order.customerPhone ?? "",
syncedQtyByMenuId,
items: order.items.map((line) => ({
menuItem: orderLineToMenuItem(line, menuById),
quantity: line.quantity,
notes: line.notes,
orderItemId: line.id,
isVoided: line.isVoided ?? false,
})),
...clearCouponState,
});
},
clearCart: () =>
set({
items: [],
...clearCouponState,
}),
clearSession: () =>
set({
items: [],
syncedQtyByMenuId: {},
tableId: null,
activeOrderId: null,
customerId: null,
guestName: "",
guestPhone: "",
...clearCouponState,
}),
subtotal: () =>
get().items.reduce(
(sum, i) =>
i.isVoided ? sum : sum + i.menuItem.price * i.quantity,
0
),
}));