Enhance admin menu CRUD forms and item metadata

This commit is contained in:
Ho Ngoc Hai
2026-06-05 18:53:50 +07:00
parent e5bf34a379
commit f1ffccf107
14 changed files with 134 additions and 7 deletions

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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"> 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"> 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"> 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>

View File

@@ -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;