272 lines
9.4 KiB
TypeScript
272 lines
9.4 KiB
TypeScript
"use client";
|
|
|
|
import { useAuth } from "@/lib/auth-context";
|
|
import { addManagerMenuItem, gqlFetch } from "@/lib/graphql";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
interface MenuItem {
|
|
id: string;
|
|
name: string;
|
|
price: number;
|
|
}
|
|
|
|
interface Eatery {
|
|
id: string;
|
|
ownerId: string;
|
|
}
|
|
|
|
function formatPrice(price: number) {
|
|
return price.toLocaleString("vi-VN") + "đ";
|
|
}
|
|
|
|
export default function MenuItemsTab() {
|
|
const { user } = useAuth();
|
|
const [eateryId, setEateryId] = useState<string | null>(null);
|
|
const [menuItems, setMenuItems] = useState<MenuItem[]>([]);
|
|
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 loadEateryId = useCallback(async (): Promise<string | null> => {
|
|
if (!user) return null;
|
|
const data = await gqlFetch<{ allEateries: Eatery[] }>(
|
|
"{ allEateries { id ownerId } }",
|
|
);
|
|
return (
|
|
data.allEateries.find((e) => e.ownerId === String(user.id))?.id ?? null
|
|
);
|
|
}, [user]);
|
|
|
|
const loadMenuItems = useCallback(async (id: string): Promise<MenuItem[]> => {
|
|
const data = await gqlFetch<{ menuItemsByEatery: MenuItem[] }>(
|
|
`query($eateryId: String!) {
|
|
menuItemsByEatery(eateryId: $eateryId) { id name price }
|
|
}`,
|
|
{ eateryId: id },
|
|
);
|
|
return data.menuItemsByEatery ?? [];
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
async function init() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const id = await loadEateryId();
|
|
setEateryId(id);
|
|
if (id) setMenuItems(await loadMenuItems(id));
|
|
else setMenuItems([]);
|
|
} catch {
|
|
setError("Không thể tải dữ liệu menu. Kiểm tra kết nối backend.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
init();
|
|
}, [loadEateryId, loadMenuItems]);
|
|
|
|
const handleAdd = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!newName.trim() || !newPrice) return;
|
|
setSubmitting(true);
|
|
setSubmitError(null);
|
|
try {
|
|
const added = await addManagerMenuItem(
|
|
newName.trim(),
|
|
parseFloat(newPrice),
|
|
);
|
|
if (added) setMenuItems((prev) => [...prev, added]);
|
|
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>
|
|
);
|
|
}
|
|
|
|
if (!eateryId) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-16 text-(--color-text-muted)">
|
|
<i className="fa-solid fa-store mb-2 block text-3xl opacity-30"></i>
|
|
<p className="text-sm">
|
|
Quán của bạn chưa được khởi tạo trong hệ thống.
|
|
</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 có 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>
|
|
);
|
|
}
|