Complete Cafe admin table and happy-hour CRUD
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,28 @@
|
||||
# Cafe Admin Table/QR/Happy-Hour Fixes - Port 3020
|
||||
|
||||
Chrome test target: `http://localhost:3020`
|
||||
|
||||
Shop: `GoodGo Cafe MVP` (`0bc5ab7c-0b6d-4746-a2ef-c61307b90199`)
|
||||
|
||||
## Workflow Screenshots
|
||||
|
||||
1. `01-admin-tables-crud-port-3020.png`
|
||||
- `/admin/shop/:shopId/tables`
|
||||
- Confirms the Cafe admin route is no longer 404.
|
||||
- Creates table `QA-920111` through the admin CRUD form.
|
||||
|
||||
2. `02-admin-qr-regenerate-port-3020.png`
|
||||
- `/admin/shop/:shopId/qr-codes`
|
||||
- Selects `QR bàn QA-920111`.
|
||||
- Regenerates QR from `/table/ca767ce906b94401` to `/table/c1b3754b580e419e`.
|
||||
|
||||
3. `03-admin-happy-hour-crud-port-3020.png`
|
||||
- `/admin/shop/:shopId/happy-hour`
|
||||
- Confirms route and campaign-backed CRUD console.
|
||||
- Creates campaign `Happy QA 81191`.
|
||||
|
||||
4. `04-pos-queue-display-port-3020.png`
|
||||
- `/pos/:shopId/cafe/queue-display`
|
||||
- Confirms the active queue lane title is `Chờ pha / đang pha`, matching tickets that are still `Chờ pha`.
|
||||
|
||||
Structured Chrome results are stored in `chrome-table-qr-results.json`.
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3020",
|
||||
"shopId": "0bc5ab7c-0b6d-4746-a2ef-c61307b90199",
|
||||
"tableNumber": "QA-920111",
|
||||
"happyHourName": "Happy QA 81191",
|
||||
"checks": {
|
||||
"tablesRoute": {
|
||||
"url": "http://localhost:3020/admin/shop/0bc5ab7c-0b6d-4746-a2ef-c61307b90199/tables",
|
||||
"title": "Bàn/phòng",
|
||||
"hasCrud": true,
|
||||
"createdVisible": true,
|
||||
"successMessage": "Tạo mới thành công"
|
||||
},
|
||||
"qrRoute": {
|
||||
"url": "http://localhost:3020/admin/shop/0bc5ab7c-0b6d-4746-a2ef-c61307b90199/qr-codes",
|
||||
"title": "QR bàn/phòng",
|
||||
"selectedText": "QR bàn QA-920111 · Bàn/phòng",
|
||||
"beforeToken": "/table/ca767ce906b94401",
|
||||
"afterToken": "/table/c1b3754b580e419e",
|
||||
"tokenChanged": true,
|
||||
"successMessage": "Tạo lại QR thành công"
|
||||
},
|
||||
"happyHourRoute": {
|
||||
"url": "http://localhost:3020/admin/shop/0bc5ab7c-0b6d-4746-a2ef-c61307b90199/happy-hour",
|
||||
"title": "Khung giờ ưu đãi",
|
||||
"hasCrud": true,
|
||||
"createdVisible": true,
|
||||
"successMessage": "Tạo mới thành công"
|
||||
},
|
||||
"queueDisplay": {
|
||||
"url": "http://localhost:3020/pos/0bc5ab7c-0b6d-4746-a2ef-c61307b90199/cafe/queue-display",
|
||||
"title": "Màn hình chờ lấy nước",
|
||||
"activeLaneTitle": "Chờ pha / đang pha",
|
||||
"titleMatchesPendingTickets": true
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"tablesCrud": "/Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/output/demo-cafe-e2e-audit-3020/22-admin-table-qr-fixes/01-admin-tables-crud-port-3020.png",
|
||||
"qrRegenerate": "/Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/output/demo-cafe-e2e-audit-3020/22-admin-table-qr-fixes/02-admin-qr-regenerate-port-3020.png",
|
||||
"happyHourCrud": "/Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/output/demo-cafe-e2e-audit-3020/22-admin-table-qr-fixes/03-admin-happy-hour-crud-port-3020.png",
|
||||
"queueDisplay": "/Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/output/demo-cafe-e2e-audit-3020/22-admin-table-qr-fixes/04-pos-queue-display-port-3020.png"
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,7 @@ async function loadItems(section: string, shop?: Shop | null, allowedShopIds?: s
|
||||
id: String(row.id),
|
||||
kind: "leave-request",
|
||||
title: String(row.reason ?? "Nghỉ phép"),
|
||||
meta: `${formatDate(row.from_date)} - ${formatDate(row.to_date)} · ${`${row.first_name ?? "Nhân viên"} ${row.last_name ?? ""}`.trim()}`,
|
||||
meta: `${formatDateOnly(row.from_date)} - ${formatDateOnly(row.to_date)} · ${`${row.first_name ?? "Nhân viên"} ${row.last_name ?? ""}`.trim()}`,
|
||||
value: displayStatus(row.status),
|
||||
edit: {
|
||||
staffId: String(row.staff_id ?? ""),
|
||||
@@ -405,7 +405,18 @@ async function loadItems(section: string, shop?: Shop | null, allowedShopIds?: s
|
||||
if (section === "promotions" || section === "happy-hour") {
|
||||
const campaigns = await listCampaigns(shopId);
|
||||
return campaigns
|
||||
.map((item) => ({ id: String(item.id), kind: "campaign", title: String(item.name), meta: String(item.description ?? "Chiến dịch"), value: displayStatus(item.status) }));
|
||||
.map((item) => ({
|
||||
id: String(item.id),
|
||||
kind: "campaign",
|
||||
title: String(item.name),
|
||||
meta: String(item.description ?? "Chiến dịch"),
|
||||
value: displayStatus(item.status),
|
||||
edit: {
|
||||
description: String(item.description ?? ""),
|
||||
amount: String(item.discount_value ?? item.face_value ?? ""),
|
||||
status: String(item.status ?? "draft")
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (section === "appointments" || section === "spa/appointments") {
|
||||
const appointments = shopId ? await listAppointments(shopId) : [];
|
||||
@@ -623,6 +634,10 @@ function displayStatus(value: unknown, fallback = "Chưa cấu hình") {
|
||||
draft: "Đang thiết lập",
|
||||
published: "Đang hoạt động",
|
||||
pending: "Chờ xử lý",
|
||||
checked_in: "Đã check-in",
|
||||
checked_out: "Đã check-out",
|
||||
approved: "Đã duyệt",
|
||||
rejected: "Từ chối",
|
||||
current: "Đang làm",
|
||||
next: "Tiếp theo",
|
||||
done: "Đã xong",
|
||||
@@ -715,7 +730,11 @@ function isKnownAdminSection(section: string, shop?: Shop | null) {
|
||||
"doctors",
|
||||
"history",
|
||||
"orders",
|
||||
"tables",
|
||||
"rooms",
|
||||
"zones",
|
||||
"qr-codes",
|
||||
"happy-hour",
|
||||
"reports/eod",
|
||||
"reports/revenue",
|
||||
"reports/staff"
|
||||
@@ -791,6 +810,14 @@ function formatDate(value: unknown) {
|
||||
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString("vi-VN");
|
||||
}
|
||||
|
||||
function formatDateOnly(value: unknown) {
|
||||
if (!value) return "";
|
||||
const date = new Date(String(value));
|
||||
return Number.isNaN(date.getTime())
|
||||
? String(value)
|
||||
: date.toLocaleDateString("vi-VN", { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
}
|
||||
|
||||
function formatReportDay(value: unknown) {
|
||||
if (!value) return "";
|
||||
const date = new Date(String(value));
|
||||
|
||||
@@ -562,7 +562,7 @@ export function WorkflowScreen({
|
||||
)) : <div className="customer-queue-empty">Chưa có món sẵn sàng</div>}
|
||||
</div>
|
||||
<div className="customer-queue-display__lane">
|
||||
<h2>Đang pha chế</h2>
|
||||
<h2>Chờ pha / đang pha</h2>
|
||||
{activeItems.length ? activeItems.map((item) => (
|
||||
<article key={String(item.id)} className="customer-queue-ticket">
|
||||
<span>{pickupNumberFromValue(item.order_id ?? item.id)}</span>
|
||||
|
||||
@@ -114,6 +114,7 @@ const crudSections = new Set([
|
||||
"staff",
|
||||
"customers",
|
||||
"promotions",
|
||||
"happy-hour",
|
||||
"drive",
|
||||
"receipt-templates",
|
||||
"ai-chat"
|
||||
@@ -526,8 +527,8 @@ function AdminCrudConsole({ section, shop, vertical, items }: { section: string;
|
||||
const amountName = selectedKind === "campaign" ? "discountValue" : selectedKind === "member" ? "points" : selectedKind === "inventory" ? "costPerUnit" : selectedKind === "file" ? "byteSize" : selectedKind === "table" ? "hourlyRate" : "price";
|
||||
const statusName = selectedKind === "staff" ? "role" : selectedKind === "inventory" ? "unit" : selectedKind === "file" ? "accessLevel" : selectedKind === "table" ? "statusId" : "status";
|
||||
const descriptionLabel = selectedKind === "file" ? "MIME type" : selectedKind === "folder" ? "ID thư mục cha" : selectedKind === "table" ? "Khu vực" : "Mô tả / ghi chú";
|
||||
const amountLabel = selectedKind === "file" ? "Dung lượng byte" : selectedKind === "table" ? "Giá theo giờ" : "Giá / chi phí / điểm";
|
||||
const statusLabel = selectedKind === "file" ? "Quyền truy cập" : selectedKind === "folder" ? "Trạng thái" : selectedKind === "table" ? "Trạng thái bàn/phòng" : "Trạng thái / vai trò / đơn vị";
|
||||
const amountLabel = selectedKind === "file" ? "Dung lượng byte" : selectedKind === "table" ? "Giá theo giờ" : selectedKind === "campaign" ? "Giá trị giảm" : "Giá / chi phí / điểm";
|
||||
const statusLabel = selectedKind === "file" ? "Quyền truy cập" : selectedKind === "folder" ? "Trạng thái" : selectedKind === "table" ? "Trạng thái bàn/phòng" : selectedKind === "campaign" ? "Trạng thái" : "Trạng thái / vai trò / đơn vị";
|
||||
const deleteLabel = selectedKind === "file" || selectedKind === "folder" ? "Ẩn metadata" : selectedKind === "receipt-template" ? "Tắt mẫu" : "Xóa / tắt";
|
||||
const canEditAmountAndStatus = selectedKind !== "folder";
|
||||
const categoryItems = items.filter((item) => item.kind === "category" && item.id);
|
||||
@@ -623,6 +624,44 @@ function AdminCrudConsole({ section, shop, vertical, items }: { section: string;
|
||||
<BooleanFormToggle name="isActive" label="Đang hiển thị" defaultChecked={selectedItem?.edit?.isActive ?? true} />
|
||||
</div>
|
||||
</>
|
||||
) : selectedKind === "table" ? (
|
||||
<>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">{vertical === "karaoke" ? "Số phòng" : "Số bàn"}</span>
|
||||
<input className="admin-form-input" name="tableNumber" defaultValue={selectedItem?.edit?.tableNumber ?? selectedItem?.title.replace(/^(Bàn|Phòng|QR bàn|QR phòng)\s+/i, "") ?? ""} />
|
||||
</label>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Sức chứa</span>
|
||||
<input className="admin-form-input" name="capacity" inputMode="numeric" defaultValue={selectedItem?.edit?.capacity ?? "2"} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Khu vực</span>
|
||||
<input className="admin-form-input" name="zone" defaultValue={selectedItem?.edit?.zone ?? ""} placeholder="Tầng 1 / Khu chính" />
|
||||
</label>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">{vertical === "karaoke" ? "Giá phòng/giờ" : "Giá theo giờ"}</span>
|
||||
<input className="admin-form-input" name="hourlyRate" inputMode="numeric" defaultValue={selectedItem?.edit?.hourlyRate ?? "0"} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Trạng thái</span>
|
||||
<select className="admin-form-input" name="statusId" defaultValue={selectedItem?.edit?.statusId ?? "1"}>
|
||||
<option value="1">Trống</option>
|
||||
<option value="2">Đang dùng</option>
|
||||
<option value="3">Đã đặt</option>
|
||||
<option value="4">Tạm khóa</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">QR token</span>
|
||||
<input className="admin-form-input" value={selectedItem?.edit?.qrToken ? `/table/${selectedItem.edit.qrToken}` : "Chưa tạo"} readOnly />
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
) : selectedKind === "receipt-template" ? (
|
||||
<>
|
||||
<div className="admin-form-grid">
|
||||
@@ -758,6 +797,13 @@ function AdminCrudConsole({ section, shop, vertical, items }: { section: string;
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{selectedItem?.kind === "table" ? (
|
||||
<div className="admin-crud-actions">
|
||||
<button className="admin-btn-secondary admin-btn-primary--small" type="button" disabled={isPending} onClick={handleTableQr}>Tạo lại QR</button>
|
||||
{selectedItem.edit?.qrToken ? <Link className="admin-panel__action" href={`/table/${selectedItem.edit.qrToken}`}>Mở menu QR</Link> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{message ? <div className={message.includes("Không") || message.includes("not") || message.includes("required") ? "admin-error-message admin-crud-message" : "admin-crud-message admin-crud-message--success"}>{message}</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
@@ -780,6 +826,7 @@ function createEndpoint(action: string) {
|
||||
product: "products",
|
||||
inventory: "inventory/items",
|
||||
recipe: "recipes",
|
||||
table: "tables",
|
||||
staff: "staff",
|
||||
member: "members",
|
||||
campaign: "campaigns",
|
||||
@@ -793,9 +840,10 @@ function createEndpoint(action: string) {
|
||||
|
||||
function crudCoverageLabel(section: string) {
|
||||
if (section === "ai-chat") return "Cấu hình";
|
||||
if (section === "drive") return "Metadata thư mục/tệp";
|
||||
if (section === "recipes") return "Recipe CRUD";
|
||||
return "Create · Update · Delete";
|
||||
if (section === "drive") return "Thông tin thư mục/tệp";
|
||||
if (section === "recipes") return "Tạo · Sửa · Xóa công thức";
|
||||
if (section === "tables" || section === "rooms" || section === "qr-codes") return "Bàn/phòng · QR";
|
||||
return "Tạo · Sửa · Xóa";
|
||||
}
|
||||
|
||||
function kindLabel(kind?: string) {
|
||||
@@ -804,6 +852,7 @@ function kindLabel(kind?: string) {
|
||||
product: "Sản phẩm",
|
||||
inventory: "Tồn kho",
|
||||
recipe: "Công thức",
|
||||
table: "Bàn/phòng",
|
||||
staff: "Nhân sự",
|
||||
member: "Khách hàng",
|
||||
campaign: "Khuyến mãi",
|
||||
@@ -857,6 +906,24 @@ function renderCreateForms(section: string, onSubmit: (event: FormEvent<HTMLForm
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (section === "tables" || section === "rooms" || section === "qr-codes") {
|
||||
const isRoom = section === "rooms" || vertical === "karaoke";
|
||||
return (
|
||||
<form className="admin-crud-card" onSubmit={onSubmit}>
|
||||
<input type="hidden" name="action" value="table" />
|
||||
<h4>{isRoom ? "Tạo phòng" : "Tạo bàn"}</h4>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group"><span className="admin-form-label">{isRoom ? "Số phòng" : "Số bàn"}</span><input className="admin-form-input" name="tableNumber" required placeholder={isRoom ? "VIP 01" : "A01"} /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Sức chứa</span><input className="admin-form-input" name="capacity" inputMode="numeric" defaultValue={isRoom ? "8" : "2"} /></label>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group"><span className="admin-form-label">Khu vực</span><input className="admin-form-input" name="zone" placeholder={isRoom ? "Tầng 2" : "Khu chính"} /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">{isRoom ? "Giá phòng/giờ" : "Giá theo giờ"}</span><input className="admin-form-input" name="hourlyRate" inputMode="numeric" defaultValue={isRoom ? "180000" : "0"} /></label>
|
||||
</div>
|
||||
<button className="admin-btn-primary admin-btn-primary--small" disabled={isPending}>{isRoom ? "Tạo phòng" : "Tạo bàn"}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
if (section === "inventory") {
|
||||
return (
|
||||
<form className="admin-crud-card" onSubmit={onSubmit}>
|
||||
@@ -917,18 +984,24 @@ function renderCreateForms(section: string, onSubmit: (event: FormEvent<HTMLForm
|
||||
</form>
|
||||
);
|
||||
}
|
||||
if (section === "promotions") {
|
||||
if (section === "promotions" || section === "happy-hour") {
|
||||
const isHappyHour = section === "happy-hour";
|
||||
return (
|
||||
<form className="admin-crud-card" onSubmit={onSubmit}>
|
||||
<input type="hidden" name="action" value="campaign" />
|
||||
<h4>Tạo khuyến mãi</h4>
|
||||
<h4>{isHappyHour ? "Tạo khung ưu đãi" : "Tạo khuyến mãi"}</h4>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group"><span className="admin-form-label">Tên chiến dịch</span><input className="admin-form-input" name="name" required placeholder="Morning coffee" /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Tên chiến dịch</span><input className="admin-form-input" name="name" required placeholder={isHappyHour ? "Happy hour 14h" : "Morning coffee"} /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Giá trị giảm</span><input className="admin-form-input" name="discountValue" inputMode="numeric" defaultValue="10000" /></label>
|
||||
</div>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Mô tả</span><input className="admin-form-input" name="description" placeholder={isHappyHour ? "Áp dụng tại quầy trong khung giờ thấp điểm" : "Điều kiện áp dụng"} /></label>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group"><span className="admin-form-label">Bắt đầu</span><input className="admin-form-input" name="startDate" type="datetime-local" /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Kết thúc</span><input className="admin-form-input" name="endDate" type="datetime-local" /></label>
|
||||
</div>
|
||||
<input type="hidden" name="discountType" value="fixed" />
|
||||
<input type="hidden" name="totalVouchers" value="100" />
|
||||
<button className="admin-btn-primary admin-btn-primary--small" disabled={isPending}>Tạo khuyến mãi</button>
|
||||
<button className="admin-btn-primary admin-btn-primary--small" disabled={isPending}>{isHappyHour ? "Tạo khung ưu đãi" : "Tạo khuyến mãi"}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export function shopPrimaryAction(shopId: string, vertical: VerticalKind, sectio
|
||||
return { href: `/admin/shop/${shopId}/ai-chat#crud-console`, label: "Cấu hình AI", Icon: Database };
|
||||
}
|
||||
if (["tables", "rooms", "zones", "qr-codes", "reservations"].includes(section)) {
|
||||
return { href: `/admin/shop/${shopId}/${section}`, label: section === "rooms" ? "Quản lý phòng" : "Quản lý bàn", Icon: PlusCircle };
|
||||
return { href: `/admin/shop/${shopId}/${section}#crud-console`, label: section === "rooms" ? "Quản lý phòng" : section === "qr-codes" ? "Tạo QR" : "Quản lý bàn", Icon: PlusCircle };
|
||||
}
|
||||
if (["history", "orders"].includes(section)) {
|
||||
return { href: `/pos/${shopId}/${vertical}?tab=history&filter=30d`, label: "Mở lịch sử bán hàng", Icon: Receipt };
|
||||
|
||||
Reference in New Issue
Block a user