ae8134fd64
Release package / release (push) Failing after 7m21s
Co-authored-by: TakahashiNg <83152264+TakahashiNguyen@users.noreply.github.com> Reviewed-on: #38
257 lines
8.5 KiB
TypeScript
257 lines
8.5 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 { useEffect, useState } from "react";
|
|
|
|
const GET_EATERY_MENU = gql`
|
|
query GetEateryMenu {
|
|
allEateries {
|
|
id
|
|
menuItems {
|
|
id
|
|
name
|
|
price
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
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 {
|
|
data,
|
|
loading: gqlLoading,
|
|
error: gqlError,
|
|
} = useQuery<allEateriesQuery>(GET_EATERY_MENU, {
|
|
client: eateryClient,
|
|
fetchPolicy: "network-only",
|
|
});
|
|
|
|
const [mutateAddMenuItem] = useMutation<addMenuItemMutation>(ADD_MENU_ITEM, {
|
|
client: eateryClient,
|
|
});
|
|
|
|
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);
|
|
|
|
useEffect(() => {
|
|
if (data?.allEateries?.[0]) {
|
|
setMenuItems(data.allEateries[0].menuItems);
|
|
}
|
|
}, [data]);
|
|
|
|
const handleAdd = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!newName.trim() || !newPrice) return;
|
|
|
|
setSubmitting(true);
|
|
setSubmitError(null);
|
|
|
|
try {
|
|
const { data: mutationResult } = await mutateAddMenuItem({
|
|
variables: {
|
|
menuItem: {
|
|
name: newName,
|
|
price: parseFloat(newPrice),
|
|
},
|
|
},
|
|
});
|
|
|
|
if (mutationResult?.addMenuItem) {
|
|
setMenuItems((prev) => [...prev, mutationResult.addMenuItem]);
|
|
closeForm();
|
|
}
|
|
} catch (err: any) {
|
|
console.error("Mutation Error:", err);
|
|
setSubmitError(err.message || "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 (gqlLoading && menuItems.length === 0)
|
|
return <div className="py-16 text-center">Đang tải menu...</div>;
|
|
if (gqlError)
|
|
return (
|
|
<div className="py-16 text-center text-red-500">
|
|
Lỗi: {gqlError.message}
|
|
</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>
|
|
);
|
|
}
|