Files
meezi/web/dashboard/src/lib/pos/submit-order.ts
T

192 lines
6.1 KiB
TypeScript
Raw Normal View History

import { apiPost } from "@/lib/api/client";
import type { Order, OrderItemLine } from "@/lib/api/types";
import type { CartItem } from "@/lib/stores/cart.store";
import { iranMobileForApi } from "@/lib/phone";
import { enqueueOfflineItem, getQueueCount } from "@/lib/offline/offline-db";
import { useSyncQueueStore } from "@/lib/stores/sync-queue.store";
export type SubmitOrderCart = {
getPendingLines: () => { menuItemId: string; quantity: number; notes?: string }[];
activeOrderId: string | null;
tableId: string | null;
guestName: string;
guestPhone: string;
customerId: string | null;
appliedCoupon: { id: string } | null;
};
export type SubmitOrderParams = {
cafeId: string;
orderBranchId: string | undefined;
cart: SubmitOrderCart;
reservationId: string | null;
/** Cart items (needed to build the offline mock order) */
cartItems?: CartItem[];
};
// ─── Offline helpers ──────────────────────────────────────────────────────────
function isNetworkError(err: unknown): boolean {
if (err instanceof TypeError) {
const msg = err.message.toLowerCase();
return (
msg.includes("failed to fetch") ||
msg.includes("networkerror") ||
msg.includes("load failed") ||
msg.includes("network request failed")
);
}
return false;
}
function newLocalId(): string {
return `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
/** Build a synthetic Order that keeps the POS cart functional while offline */
function buildLocalOrder(
params: SubmitOrderParams,
cartItems: CartItem[]
): Order {
const pending = params.cart.getPendingLines();
const localId = newLocalId();
const items: OrderItemLine[] = pending.map((p) => {
const ci = cartItems.find((c) => c.menuItem.id === p.menuItemId);
return {
id: newLocalId(),
menuItemId: p.menuItemId,
menuItemName: ci?.menuItem.name ?? p.menuItemId,
quantity: p.quantity,
unitPrice: ci?.menuItem.price ?? 0,
notes: p.notes,
isVoided: false,
};
});
const subtotal = items.reduce((s, i) => s + i.unitPrice * i.quantity, 0);
const taxTotal = Math.round(subtotal * 0.09);
const total = subtotal + taxTotal;
return {
id: localId,
cafeId: params.cafeId,
branchId: params.orderBranchId,
tableId: params.cart.tableId ?? undefined,
guestName: params.cart.guestName.trim() || undefined,
guestPhone: iranMobileForApi(params.cart.guestPhone) ?? undefined,
customerId: params.cart.customerId ?? undefined,
orderType: "DineIn",
status: "Open",
subtotal,
taxTotal,
discountAmount: 0,
total,
paidAmount: 0,
createdAt: new Date().toISOString(),
displayNumber: 0,
items,
payments: [],
};
}
async function queueAndBuildLocalOrder(
params: SubmitOrderParams,
cartItems: CartItem[]
): Promise<Order> {
const pending = params.cart.getPendingLines();
if (pending.length === 0) throw new Error("nothing pending");
const isAddToExisting =
!!params.cart.activeOrderId &&
!params.cart.activeOrderId.startsWith("local_");
await enqueueOfflineItem({
id: newLocalId(),
type: isAddToExisting ? "add_items" : "create_order",
cafeId: params.cafeId,
targetOrderId: isAddToExisting ? params.cart.activeOrderId : null,
payload: isAddToExisting
? {
cafeId: params.cafeId,
orderId: params.cart.activeOrderId!,
body: { items: pending },
}
: {
cafeId: params.cafeId,
body: {
orderType: "DineIn",
branchId: params.orderBranchId,
tableId: params.cart.tableId ?? undefined,
reservationId: params.reservationId ?? undefined,
guestName: params.cart.guestName.trim() || undefined,
guestPhone: iranMobileForApi(params.cart.guestPhone),
customerId: params.cart.customerId ?? undefined,
couponId: params.cart.appliedCoupon?.id,
items: pending,
},
},
createdAt: new Date().toISOString(),
});
// Update global queue count
const count = await getQueueCount();
useSyncQueueStore.getState().setQueueCount(count);
return buildLocalOrder(params, cartItems);
}
// ─── Main export ──────────────────────────────────────────────────────────────
export async function submitOrderToApi({
cafeId,
orderBranchId,
cart,
reservationId,
cartItems = [],
}: SubmitOrderParams): Promise<Order> {
const pending = cart.getPendingLines();
if (pending.length === 0) throw new Error("nothing pending");
const tryOnline = async (): Promise<Order> => {
if (cart.activeOrderId && !cart.activeOrderId.startsWith("local_")) {
return apiPost<Order>(`/api/cafes/${cafeId}/orders/${cart.activeOrderId}/items`, {
items: pending,
});
}
return apiPost<Order>(`/api/cafes/${cafeId}/orders`, {
orderType: "DineIn",
branchId: orderBranchId,
tableId: cart.tableId ?? undefined,
reservationId: reservationId ?? undefined,
guestName: cart.guestName.trim() || undefined,
guestPhone: iranMobileForApi(cart.guestPhone),
customerId: cart.customerId ?? undefined,
couponId: cart.appliedCoupon?.id,
items: pending,
});
};
// Try online first
if (navigator.onLine) {
try {
return await tryOnline();
} catch (err) {
// If it's a network error despite onLine flag, fall through to offline path
if (!isNetworkError(err)) throw err;
}
}
// Offline path: queue and return a local mock order
return queueAndBuildLocalOrder({ cafeId, orderBranchId, cart, reservationId, cartItems }, cartItems);
}
export function orderAmountDue(order: Order): number {
return Math.max(0, order.total - (order.paidAmount ?? 0));
}
/** True when the order was created locally (offline) and not yet synced */
export function isLocalOrder(orderId: string | null): boolean {
return !!orderId?.startsWith("local_");
}