Files
frontend/components/organisms/manager/ProductsTab.tsx
T
Thanh Quy- wolf 1c8a23b4d1
Release package / release (pull_request) Has been cancelled
feat: add image upload functionality for menu items
2026-05-14 10:14:03 +07:00

227 lines
8.7 KiB
TypeScript

"use client";
import { useManager } from "@/lib/manager-context";
import type { MenuItemEntity } from "@/lib/types";
import { useState } from "react";
import DeleteConfirm from "./DeleteConfirm";
import ProductModal from "./ProductModal";
import StatusBadge from "./StatusBadge";
function formatPrice(price: number) {
return price.toLocaleString("vi-VN") + "đ";
}
function toDisplayUrl(filename: string) {
if (!filename) return "/imgs/products/placeholder.jpg";
if (filename.startsWith("/") || filename.startsWith("http")) return filename;
return `/api/file/${filename}`;
}
export default function ProductsTab() {
const {
products,
addProduct,
updateProduct,
deleteProduct,
toggleProductAvailability,
} = useManager();
const [filterStatus, setFilterStatus] = useState<
"all" | "available" | "unavailable"
>("all");
const [search, setSearch] = useState("");
const [modalProduct, setModalProduct] = useState<
MenuItemEntity | null | "new"
>(null);
const [deleteTarget, setDeleteTarget] = useState<MenuItemEntity>(null!);
const filtered = products.filter((p) => {
if (filterStatus === "available" && p.available === false) return false;
if (filterStatus === "unavailable" && p.available !== false) return false;
if (
search &&
!p.name.toLowerCase().includes(search.toLowerCase()) &&
!p.description.toLowerCase().includes(search.toLowerCase())
)
return false;
return true;
});
return (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex flex-wrap items-center gap-3">
{/* Search */}
<div className="relative min-w-2/5 flex-1">
<i className="fa-solid fa-magnifying-glass pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-sm text-(--color-text-muted)"></i>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Tìm kiếm món..."
className="text-foreground w-full rounded-xl border border-(--color-border) bg-white py-2 pr-9 pl-9 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20"
/>
{search && (
<button
title="Xóa tìm kiếm"
onClick={() => setSearch("")}
className="absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer border-none bg-transparent text-(--color-text-muted) hover:text-(--color-primary)"
>
<i className="fa-solid fa-xmark text-sm"></i>
</button>
)}
</div>
<select
value={filterStatus}
onChange={(e) =>
setFilterStatus(
e.target.value as "all" | "available" | "unavailable",
)
}
className="text-foreground cursor-pointer rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary)"
title="Lọc theo trạng thái"
>
<option value="all">Tất cả trạng thái</option>
<option value="available">Còn hàng</option>
<option value="unavailable">Tạm hết</option>
</select>
<button
title="Thêm món"
onClick={() => setModalProduct("new")}
className="flex cursor-pointer items-center gap-2 rounded-xl border-none bg-(--color-primary) px-4 py-2 text-sm font-semibold text-white transition hover:bg-(--color-primary-dark) active:scale-95"
>
<i className="fa-solid fa-plus"></i>
<span className="hidden sm:inline">Thêm món</span>
</button>
</div>
<p className="text-sm text-(--color-text-muted)">
Hiển thị <strong className="text-foreground">{filtered.length}</strong>{" "}
/ {products.length} món
</p>
{/* Table */}
<div className="overflow-x-auto rounded-2xl border border-(--color-border-light) bg-white shadow-sm">
<table className="min-w-full divide-y divide-(--color-border-light) text-sm">
<thead className="bg-background">
<tr>
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">
nh
</th>
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">
Tên món
</th>
<th className="px-4 py-3 text-right font-semibold text-(--color-text-secondary)">
Giá
</th>
<th className="px-4 py-3 text-center font-semibold text-(--color-text-secondary)">
Trạng thái
</th>
<th className="px-4 py-3 text-center font-semibold text-(--color-text-secondary)">
Thao tác
</th>
</tr>
</thead>
<tbody className="divide-y divide-(--color-border-light)">
{filtered.length === 0 ? (
<tr>
<td
colSpan={5}
className="py-12 text-center text-(--color-text-muted)"
>
<i className="fa-solid fa-mug-hot mb-2 block text-3xl opacity-30"></i>
Không tìm thấy món nào
</td>
</tr>
) : (
filtered.map((p) => (
<tr
key={p.id}
className="hover:bg-background transition-colors"
>
<td className="px-4 py-3">
<img
src={toDisplayUrl(p.imageUrl)}
alt={p.name}
className="h-10 w-10 rounded-lg border border-(--color-border-light) object-cover"
/>
</td>
<td className="px-4 py-3">
<div>
<p className="text-foreground font-medium">{p.name}</p>
{p.description && (
<p className="mt-0.5 max-w-xs truncate text-xs text-(--color-text-muted)">
{p.description}
</p>
)}
</div>
</td>
<td className="px-4 py-3 text-right font-semibold text-(--color-primary)">
{formatPrice(p.price)}
</td>
<td className="px-4 py-3 text-center">
<button
onClick={() => toggleProductAvailability(p)}
title="Nhấn để đổi trạng thái"
className="cursor-pointer border-none bg-transparent"
>
<StatusBadge available={p.available ?? true} />
</button>
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-center gap-1.5">
<button
onClick={() => setModalProduct(p)}
title="Chỉnh sửa"
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-(--color-border-light) bg-transparent text-(--color-text-muted) transition hover:border-(--color-primary-light) hover:text-(--color-primary)"
>
<i className="fa-solid fa-pen text-xs"></i>
</button>
<button
onClick={() => setDeleteTarget(p)}
title="Xóa"
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-transparent text-(--color-text-muted) transition hover:border-red-200 hover:bg-red-50 hover:text-red-500"
>
<i className="fa-solid fa-trash text-xs"></i>
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{modalProduct !== null && (
<ProductModal
product={modalProduct === "new" ? null : modalProduct}
onSave={(data: MenuItemEntity) => {
if ("id" in data) {
updateProduct(data);
} else {
addProduct(data);
}
setModalProduct(null);
}}
onClose={() => setModalProduct(null)}
/>
)}
{deleteTarget !== null && (
<DeleteConfirm
name={deleteTarget.name}
onConfirm={() => {
deleteProduct(deleteTarget.id!);
setDeleteTarget(null!);
}}
onClose={() => setDeleteTarget(null!)}
/>
)}
</div>
);
}