Files
frontend/components/organisms/manager/MenuItemsTab.tsx
T
2026-05-13 02:41:32 +00:00

267 lines
8.9 KiB
TypeScript

"use client";
import { eateryClient } from "@/lib/apollo-clients";
import { useAuth } from "@/lib/auth-context";
import {
MenuItemEntity,
addMenuItemMutation,
allEateriesQuery,
} from "@/lib/types";
import { gql } from "@apollo/client";
import { useMutation, useQuery } from "@apollo/client/react";
import { useCallback, useEffect, useState } from "react";
const GET_EATERY_MENU = gql`{ allEateries { id menuItems { id name } } }`;
const ADD_MENU_ITEM = gql`
mutation addMenuItem($menuItem: AddMenuItemInput!) {
addMenuItem(menuItem: $menuItem) {
id
name
price
}
}
`;
function formatPrice(price: number) {
return price.toLocaleString("vi-VN") + "đ";
}
export default function MenuItemsTab() {
const { user } = useAuth();
const [menuItems, setMenuItems] = useState<MenuItemEntity[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showForm, setShowForm] = useState(false);
const [newName, setNewName] = useState("");
const [newPrice, setNewPrice] = useState("");
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const {
data,
loading: gqlLoading,
error: gqlError,
} = useQuery<allEateriesQuery>(GET_EATERY_MENU, {
client: eateryClient,
fetchPolicy: "no-cache",
});
useEffect(() => {
setLoading(true);
setError(null);
if (!gqlLoading) {
if (data) setMenuItems(data.allEateries[0].menuItems);
else
setError(
gqlError?.message ||
"Không thể tải dữ liệu menu. Kiểm tra kết nối backend.",
);
}
setLoading(false);
}, [data, gqlLoading, gqlError]);
const handleAdd = async (e: React.FormEvent) => {
e.preventDefault();
if (!newName.trim() || !newPrice) return;
setSubmitting(true);
setSubmitError(null);
try {
const [addMenuItem, { data }] = useMutation<addMenuItemMutation>(
GET_EATERY_MENU,
{
client: eateryClient,
fetchPolicy: "no-cache",
},
);
await addMenuItem({
variables: {
name: newName,
price: newPrice,
},
});
if (data) setMenuItems((prev) => [...prev, data.addMenuItem]);
setNewName("");
setNewPrice("");
setShowForm(false);
} catch {
setSubmitError("Không thể thêm món. Vui lòng thử lại.");
} finally {
setSubmitting(false);
}
};
const closeForm = () => {
setShowForm(false);
setSubmitError(null);
setNewName("");
setNewPrice("");
};
if (loading) {
return (
<div className="flex items-center justify-center py-16 text-(--color-text-muted)">
<i className="fa-solid fa-spinner mr-2 animate-spin"></i>
Đang tải menu...
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center py-16 text-red-500">
<i className="fa-solid fa-circle-exclamation mb-2 text-2xl"></i>
<p className="text-sm">{error}</p>
</div>
);
}
return (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex items-center justify-between">
<p className="text-sm text-(--color-text-muted)">
<strong className="text-foreground">{menuItems.length}</strong> món
trong menu
</p>
<button
onClick={() => setShowForm(true)}
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>
{/* 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)">
Tên món
</th>
<th className="px-4 py-3 text-right font-semibold text-(--color-text-secondary)">
Giá
</th>
</tr>
</thead>
<tbody className="divide-y divide-(--color-border-light)">
{menuItems.length === 0 ? (
<tr>
<td
colSpan={2}
className="py-12 text-center text-(--color-text-muted)"
>
<i className="fa-solid fa-bowl-food mb-2 block text-3xl opacity-30"></i>
Chưa món nào trong menu
</td>
</tr>
) : (
menuItems.map((item) => (
<tr
key={item.id}
className="hover:bg-background transition-colors"
>
<td className="px-4 py-3">
<p className="text-foreground font-medium">{item.name}</p>
</td>
<td className="px-4 py-3 text-right font-semibold text-(--color-primary)">
{formatPrice(item.price)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Add Item Modal */}
{showForm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 backdrop-blur-sm">
<div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-lg">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-foreground font-bold">Thêm món mới</h2>
<button
onClick={closeForm}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--color-text-muted) hover:text-(--color-primary)"
>
<i className="fa-solid fa-xmark"></i>
</button>
</div>
<form onSubmit={handleAdd} className="space-y-4">
<div>
<label className="mb-1.5 block text-sm font-medium text-(--color-text-secondary)">
Tên món <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="Nhập tên món..."
required
className="text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-(--color-text-secondary)">
Giá (VNĐ) <span className="text-red-500">*</span>
</label>
<input
type="number"
value={newPrice}
onChange={(e) => setNewPrice(e.target.value)}
placeholder="Nhập giá..."
required
min={0}
step={500}
className="text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20"
/>
</div>
{submitError && (
<p className="text-sm text-red-500">
<i className="fa-solid fa-circle-exclamation mr-1"></i>
{submitError}
</p>
)}
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={closeForm}
className="hover:bg-background flex-1 cursor-pointer rounded-xl border border-(--color-border-light) bg-transparent py-2 text-sm font-medium text-(--color-text-secondary) transition"
>
Hủy
</button>
<button
type="submit"
disabled={submitting}
className="flex-1 cursor-pointer rounded-xl border-none bg-(--color-primary) py-2 text-sm font-semibold text-white transition hover:bg-(--color-primary-dark) disabled:opacity-60"
>
{submitting ? (
<>
<i className="fa-solid fa-spinner mr-1 animate-spin"></i>
Đang lưu...
</>
) : (
"Thêm món"
)}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}