Enhance admin menu CRUD forms and item metadata
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
@@ -4,8 +4,15 @@
|
||||
|
||||
## 01-menu
|
||||
|
||||
- `10-admin-cafe-menu-after-product-category-crud.png`: tạo/sửa sản phẩm và danh mục.
|
||||
- `10h-menu-after-category-delete-reload.png`: danh mục đã ẩn sau reload.
|
||||
- `01-menu-enhanced-forms/01-menu-enhanced-forms.png`: form menu mới có danh mục, SKU, barcode, tồn ban đầu, mô tả, edit category rõ field.
|
||||
- `02-menu-category-created/02-menu-category-created.png`: tạo danh mục QA mới.
|
||||
- `03-menu-product-created/03-menu-product-created.png`: tạo product QA gắn category, SKU, barcode, tồn ban đầu.
|
||||
- `04-menu-product-edit-prefill/04-menu-product-edit-prefill.png`: form edit product prefill đúng category, SKU, barcode và `PreparedFood`.
|
||||
- `05-menu-product-updated/05-menu-product-updated.png`: cập nhật product QA, giá, SKU, barcode.
|
||||
- `06-menu-category-updated/06-menu-category-updated.png`: cập nhật category QA, mô tả và thứ tự.
|
||||
- `07-menu-product-hidden-after-reload/07-menu-product-hidden-after-reload.png`: product QA đã ẩn sau reload.
|
||||
- `08-menu-category-hidden-after-reload/08-menu-category-hidden-after-reload.png`: category QA đã ẩn sau reload.
|
||||
- Ảnh `10-*` cũ đã chuyển sang `99-diagnostics/menu-stale/`, không dùng làm demo chính.
|
||||
|
||||
## 02-inventory
|
||||
|
||||
@@ -62,3 +69,4 @@
|
||||
## 99-diagnostics
|
||||
|
||||
Ảnh trong thư mục này là lỗi/case điều tra, không dùng làm demo chính.
|
||||
- `menu-stale/`: ảnh menu CRUD cũ trước khi tách product/category canonical.
|
||||
|
||||
@@ -228,8 +228,41 @@ async function loadItems(section: string, shop?: Shop | null, allowedShopIds?: s
|
||||
if (["menu", "products", "services", "packages", "treatments", "combos"].includes(section) && shopId) {
|
||||
const [categories, products] = await Promise.all([listCatalogCategoriesByShop(shopId), listCatalogProductsByShop(shopId)]);
|
||||
return [
|
||||
...categories.map((category) => ({ id: category.id, kind: "category", title: category.name, meta: category.description ?? "Danh mục", value: `${category.displayOrder}`, href: `/admin/shop/${shopId}/${section}` })),
|
||||
...products.map((product) => ({ id: product.id, kind: "product", title: product.name, meta: product.categoryName ?? product.productType, value: formatMoney(product.price), href: `/admin/shop/${shopId}/${section}` }))
|
||||
...categories.map((category) => ({
|
||||
id: category.id,
|
||||
kind: "category",
|
||||
title: category.name,
|
||||
meta: category.description ?? "Danh mục",
|
||||
value: `Thứ tự ${category.displayOrder}`,
|
||||
href: `/admin/shop/${shopId}/${section}`,
|
||||
edit: {
|
||||
description: category.description ?? "",
|
||||
displayOrder: String(category.displayOrder),
|
||||
isActive: true
|
||||
}
|
||||
})),
|
||||
...products.map((product) => ({
|
||||
id: product.id,
|
||||
kind: "product",
|
||||
title: product.name,
|
||||
meta: [
|
||||
product.categoryName ?? "Chưa phân loại",
|
||||
product.productType,
|
||||
product.sku ? `SKU ${product.sku}` : null,
|
||||
product.barcode ? `Barcode ${product.barcode}` : null
|
||||
].filter(Boolean).join(" · "),
|
||||
value: formatMoney(product.price),
|
||||
href: `/admin/shop/${shopId}/${section}`,
|
||||
edit: {
|
||||
description: product.description ?? "",
|
||||
amount: String(product.price),
|
||||
status: product.productType,
|
||||
categoryId: product.categoryId ?? "",
|
||||
sku: product.sku ?? "",
|
||||
barcode: product.barcode ?? "",
|
||||
isActive: product.isActive
|
||||
}
|
||||
}))
|
||||
];
|
||||
}
|
||||
if (section === "recipes" && shopId) {
|
||||
|
||||
@@ -128,10 +128,11 @@ const numericPayloadKeys = new Set([
|
||||
"discountValue",
|
||||
"totalVouchers",
|
||||
"taxRate",
|
||||
"byteSize"
|
||||
"byteSize",
|
||||
"typeId"
|
||||
]);
|
||||
|
||||
const booleanPayloadKeys = new Set(["enabled", "isDefault", "showLogo", "showQr", "showTax", "kitchenCopy"]);
|
||||
const booleanPayloadKeys = new Set(["enabled", "isDefault", "showLogo", "showQr", "showTax", "kitchenCopy", "isActive"]);
|
||||
|
||||
function AdminCrudConsole({ section, shop, vertical, items }: { section: string; shop: Shop; vertical: VerticalKind; items: AdminSectionItem[] }) {
|
||||
const router = useRouter();
|
||||
@@ -249,6 +250,7 @@ function AdminCrudConsole({ section, shop, vertical, items }: { section: string;
|
||||
const statusLabel = selectedKind === "file" ? "Quyền truy cập" : selectedKind === "folder" ? "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);
|
||||
|
||||
return (
|
||||
<section className="admin-panel admin-crud-console" id="crud-console">
|
||||
@@ -274,7 +276,74 @@ function AdminCrudConsole({ section, shop, vertical, items }: { section: string;
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{selectedKind === "receipt-template" ? (
|
||||
{selectedKind === "product" ? (
|
||||
<>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Tên món</span>
|
||||
<input className="admin-form-input" name="name" defaultValue={selectedItem?.title ?? ""} />
|
||||
</label>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Danh mục</span>
|
||||
<select className="admin-form-input" name="categoryId" defaultValue={selectedItem?.edit?.categoryId ?? ""}>
|
||||
<option value="">Chưa phân loại</option>
|
||||
{categoryItems.map((item) => <option key={item.id} value={item.id}>{item.title}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Mô tả</span>
|
||||
<input className="admin-form-input" name="description" defaultValue={selectedItem?.edit?.description ?? ""} placeholder="Ghi chú menu / định lượng" />
|
||||
</label>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Giá bán</span>
|
||||
<input className="admin-form-input" name="price" inputMode="numeric" defaultValue={selectedItem?.edit?.amount ?? ""} />
|
||||
</label>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Loại hàng</span>
|
||||
<select className="admin-form-input" name="typeId" defaultValue={selectedItem?.edit?.status === "Service" ? "2" : selectedItem?.edit?.status === "Physical" ? "1" : "3"}>
|
||||
<option value="3">PreparedFood - vào quầy pha chế</option>
|
||||
<option value="1">Physical - bán lẻ/tồn kho</option>
|
||||
<option value="2">Service - dịch vụ</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">SKU</span>
|
||||
<input className="admin-form-input" name="sku" defaultValue={selectedItem?.edit?.sku ?? ""} placeholder="CAFE-LATTE" />
|
||||
</label>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Barcode</span>
|
||||
<input className="admin-form-input" name="barcode" defaultValue={selectedItem?.edit?.barcode ?? ""} placeholder="893..." />
|
||||
</label>
|
||||
</div>
|
||||
<div className="admin-toggle-grid">
|
||||
<BooleanFormToggle name="isActive" label="Đang bán" defaultChecked={selectedItem?.edit?.isActive ?? true} />
|
||||
</div>
|
||||
</>
|
||||
) : selectedKind === "category" ? (
|
||||
<>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Tên danh mục</span>
|
||||
<input className="admin-form-input" name="name" defaultValue={selectedItem?.title ?? ""} />
|
||||
</label>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Thứ tự hiển thị</span>
|
||||
<input className="admin-form-input" name="displayOrder" inputMode="numeric" defaultValue={selectedItem?.edit?.displayOrder ?? "0"} />
|
||||
</label>
|
||||
</div>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Mô tả</span>
|
||||
<input className="admin-form-input" name="description" defaultValue={selectedItem?.edit?.description ?? ""} placeholder="Nhóm đồ uống / món bán" />
|
||||
</label>
|
||||
<div className="admin-toggle-grid">
|
||||
<BooleanFormToggle name="isActive" label="Đang hiển thị" defaultChecked={selectedItem?.edit?.isActive ?? true} />
|
||||
</div>
|
||||
</>
|
||||
) : selectedKind === "receipt-template" ? (
|
||||
<>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group">
|
||||
@@ -467,6 +536,7 @@ function kindLabel(kind?: string) {
|
||||
|
||||
function renderCreateForms(section: string, onSubmit: (event: FormEvent<HTMLFormElement>) => void, isPending: boolean, vertical: VerticalKind, items: AdminSectionItem[]) {
|
||||
if (section === "menu" || section === "products") {
|
||||
const categoryItems = items.filter((item) => item.kind === "category" && item.id);
|
||||
return (
|
||||
<>
|
||||
<form className="admin-crud-card" onSubmit={onSubmit}>
|
||||
@@ -476,9 +546,20 @@ function renderCreateForms(section: string, onSubmit: (event: FormEvent<HTMLForm
|
||||
<label className="admin-form-group"><span className="admin-form-label">Tên món</span><input className="admin-form-input" name="name" required placeholder="Cold brew" /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Giá</span><input className="admin-form-input" name="price" required inputMode="numeric" placeholder="55000" /></label>
|
||||
</div>
|
||||
<label className="admin-form-group">
|
||||
<span className="admin-form-label">Danh mục</span>
|
||||
<select className="admin-form-input" name="categoryId" defaultValue="">
|
||||
<option value="">Chưa phân loại</option>
|
||||
{categoryItems.map((item) => <option key={item.id} value={item.id}>{item.title}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group"><span className="admin-form-label">SKU</span><input className="admin-form-input" name="sku" placeholder="CAFE-COLD-BREW" /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Barcode</span><input className="admin-form-input" name="barcode" placeholder="893..." /></label>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
<label className="admin-form-group"><span className="admin-form-label">Tồn ban đầu</span><input className="admin-form-input" name="initialQuantity" inputMode="numeric" defaultValue="0" /></label>
|
||||
<label className="admin-form-group"><span className="admin-form-label">Mô tả</span><input className="admin-form-input" name="description" placeholder="Ghi chú pha chế" /></label>
|
||||
</div>
|
||||
<input type="hidden" name="vertical" value={vertical} />
|
||||
<button className="admin-btn-primary admin-btn-primary--small" disabled={isPending}>Tạo món</button>
|
||||
|
||||
@@ -47,6 +47,11 @@ export type AdminSectionItem = {
|
||||
footerText?: string;
|
||||
taxLabel?: string;
|
||||
taxRate?: string;
|
||||
categoryId?: string;
|
||||
sku?: string;
|
||||
barcode?: string;
|
||||
displayOrder?: string;
|
||||
isActive?: boolean;
|
||||
showLogo?: boolean;
|
||||
showQr?: boolean;
|
||||
showTax?: boolean;
|
||||
|
||||