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>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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
|
||||
),
|
||||
}));
|
||||
Reference in New Issue
Block a user