feat(pos): implement integrated full-screen POS point-of-sale system with inventory deduction and thermal receipt

This commit is contained in:
Your Name committed 2026-09-24 19:35:56 +07:00
1 parent b585277214
commit 5796e4d868
10 files changed
+1549 -23

No files matched your search

+30
View File
@@ -66,4 +66,34 @@ export const customersRoute = new Elysia({ prefix: "/customers" })
pointsDelta: t.Number(),
reason: t.Optional(t.String())
})
})
.post("/", async ({ body, set }) => {
try {
const { displayName, phone, email } = body as { displayName?: string, phone?: string, email?: string };
// Auto-generate a unique lineUserId placeholder for in-store/POS customers
const lineUserId = `POS_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
const newCustomer = await prisma.customer.create({
data: {
lineUserId,
displayName: displayName || "ลูกค้าหน้าร้าน",
phone: phone || null,
email: email || null,
points: 0
}
});
return newCustomer;
} catch (err: any) {
console.error("[Customers] Create error:", err);
set.status = 400;
return { error: err.message || "Failed to create customer" };
}
}, {
body: t.Object({
displayName: t.Optional(t.String()),
phone: t.Optional(t.String()),
email: t.Optional(t.String())
})
});
+51 -10
View File
@@ -48,13 +48,17 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
})
}
const isPos = data.channel === "POS";
const initialStatus = isPos ? "COMPLETED" : "PENDING_PAYMENT";
const order = await prisma.order.create({
data: {
channel: data.channel || "ONLINE",
customerId: data.customerId,
customerName: data.customerName,
customerEmail: data.customerEmail,
customerPhone: data.customerPhone,
shippingAddress: data.shippingAddress,
customerName: data.customerName || (isPos ? "ลูกค้าหน้าร้าน (Walk-in)" : null),
customerEmail: data.customerEmail || null,
customerPhone: data.customerPhone || null,
shippingAddress: data.shippingAddress || (isPos ? "หน้าร้าน (Takeaway)" : null),
subtotal: data.subtotal || 0,
promoLevel1Name: data.promoLevel1Name,
promoLevel1Disc: data.promoLevel1Disc || 0,
@@ -65,8 +69,13 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
pointsUsed: data.pointsUsed || 0,
pointsDiscount: data.pointsDiscount || 0,
pointsEarned: pointsEarned,
shippingFee: data.shippingFee,
shippingFee: data.shippingFee || 0,
totalAmount: data.totalAmount,
status: initialStatus,
paymentMethod: data.paymentMethod || (isPos ? "CASH" : null),
receivedAmount: data.receivedAmount,
changeAmount: data.changeAmount,
cashierId: data.cashierId,
items: {
create: data.items.map((item: any) => ({
productId: item.productId,
@@ -81,6 +90,33 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
},
include: { items: true }
});
// For POS orders: Deduct stock immediately and credit points
if (isPos) {
for (const item of data.items) {
if (item.variantId) {
try {
await prisma.productVariant.update({
where: { id: item.variantId },
data: { stock: { decrement: item.quantity } }
});
} catch (err) {
console.warn(`[POS] Failed to decrement stock for variant ${item.variantId}:`, err);
}
}
}
if (data.customerId && pointsEarned > 0) {
try {
await prisma.customer.update({
where: { id: data.customerId },
data: { points: { increment: pointsEarned } }
});
} catch (err) {
console.warn(`[POS] Failed to credit points for customer ${data.customerId}:`, err);
}
}
}
// If a coupon was used, increment its usedCount
if (data.couponCode) {
@@ -107,13 +143,18 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
}
}, {
body: t.Object({
channel: t.Optional(t.String()),
customerId: t.Optional(t.Nullable(t.Number())),
customerName: t.String(),
customerEmail: t.String(),
customerPhone: t.String(),
shippingAddress: t.String(),
customerName: t.Optional(t.Nullable(t.String())),
customerEmail: t.Optional(t.Nullable(t.String())),
customerPhone: t.Optional(t.Nullable(t.String())),
shippingAddress: t.Optional(t.Nullable(t.String())),
paymentMethod: t.Optional(t.Nullable(t.String())),
receivedAmount: t.Optional(t.Nullable(t.Number())),
changeAmount: t.Optional(t.Nullable(t.Number())),
cashierId: t.Optional(t.Nullable(t.Number())),
totalAmount: t.Number(),
shippingFee: t.Number(),
shippingFee: t.Optional(t.Number()),
subtotal: t.Optional(t.Number()),
promoLevel1Name: t.Optional(t.Nullable(t.String())),
promoLevel1Disc: t.Optional(t.Number()),
+5
View File
@@ -133,8 +133,13 @@ export const productsRoute = new Elysia({ prefix: "/products" })
{ translations: { some: { name: { contains: search, mode: "insensitive" as const } } } },
{ brand: { name: { contains: search, mode: "insensitive" as const } } },
{ category: { name: { contains: search, mode: "insensitive" as const } } },
{ variants: { some: { OR: [
{ barcode: { contains: search, mode: "insensitive" as const } },
{ sku: { contains: search, mode: "insensitive" as const } }
] } } },
],
}),
} as const;
const [products, total] = await Promise.all([