"use client"; import { useMemo, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useLocale, useTranslations } from "next-intl"; import { useIsRtl } from "@/lib/use-is-rtl"; import { Box, Pencil, Video } from "lucide-react"; import { Menu3dUpload } from "@/components/media/menu-3d-upload"; import { MenuAi3dGenerate } from "@/components/media/menu-ai-3d-generate"; import { CategoryVisual } from "@/components/menu/category-visual"; import { CategoryMediaFields } from "@/components/menu/category-media-fields"; import type { CategoryIconSelection } from "@/components/menu/category-preset-picker"; import { DEFAULT_CATEGORY_ICON_STYLE } from "@/lib/category-icon-presets"; import { apiGet, apiPatch, apiPost } from "@/lib/api/client"; import { useAuthStore } from "@/lib/stores/auth.store"; import { formatCurrency, formatNumber } from "@/lib/format"; import { PageHeader } from "@/components/layout/page-header"; import { MediaPairUpload } from "@/components/media/media-pair-upload"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { LabeledField } from "@/components/ui/labeled-field"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { MenuItemLabels } from "@/components/menu/menu-item-labels"; import { MenuItemMedia } from "@/components/menu/menu-item-media"; import { buildCategoryNameMap, inferMenuItemKind } from "@/lib/menu-item-image"; interface MenuCategory { id: string; name: string; nameEn?: string; nameAr?: string; sortOrder: number; discountPercent: number; icon?: string; iconPresetId?: string; iconStyle?: string; imageUrl?: string; isActive: boolean; } interface MenuItem { id: string; categoryId: string; name: string; nameEn?: string; nameAr?: string; price: number; discountPercent: number; imageUrl?: string; videoUrl?: string; model3dUrl?: string; isAvailable: boolean; } function discountedPrice(price: number, percent: number) { if (percent <= 0) return price; return Math.round(price * (1 - percent / 100)); } function mediaField(url: string) { return url.trim() === "" ? "" : url; } export function MenuAdminScreen() { const t = useTranslations("menuAdmin"); const tCommon = useTranslations("common"); const isRtl = useIsRtl(); const locale = useLocale(); const numberLocale = locale === "en" ? "en-US" : "fa-IR"; const cafeId = useAuthStore((s) => s.user?.cafeId); const queryClient = useQueryClient(); const [editingId, setEditingId] = useState(null); const [editingCategoryId, setEditingCategoryId] = useState(null); const [catName, setCatName] = useState(""); const [catIcon, setCatIcon] = useState(""); const [catIconPreset, setCatIconPreset] = useState({ iconPresetId: null, iconStyle: DEFAULT_CATEGORY_ICON_STYLE, }); const [catImageUrl, setCatImageUrl] = useState(""); const [editCatName, setEditCatName] = useState(""); const [editCatIcon, setEditCatIcon] = useState(""); const [editCatIconPreset, setEditCatIconPreset] = useState({ iconPresetId: null, iconStyle: DEFAULT_CATEGORY_ICON_STYLE, }); const [editCatImageUrl, setEditCatImageUrl] = useState(""); const [itemName, setItemName] = useState(""); const [itemNameEn, setItemNameEn] = useState(""); const [itemPrice, setItemPrice] = useState(""); const [itemDiscount, setItemDiscount] = useState("0"); const [itemCategoryId, setItemCategoryId] = useState(""); const [itemImageUrl, setItemImageUrl] = useState(""); const [itemVideoUrl, setItemVideoUrl] = useState(""); const [itemModel3dUrl, setItemModel3dUrl] = useState(""); const [editName, setEditName] = useState(""); const [editNameEn, setEditNameEn] = useState(""); const [editPrice, setEditPrice] = useState(""); const [editDiscount, setEditDiscount] = useState("0"); const [editImageUrl, setEditImageUrl] = useState(""); const [editVideoUrl, setEditVideoUrl] = useState(""); const [editModel3dUrl, setEditModel3dUrl] = useState(""); const { data: categories = [] } = useQuery({ queryKey: ["menu-categories", cafeId], queryFn: () => apiGet(`/api/cafes/${cafeId}/menu/categories`), enabled: !!cafeId, }); const { data: items = [], isLoading } = useQuery({ queryKey: ["menu-items-all", cafeId], queryFn: () => apiGet(`/api/cafes/${cafeId}/menu/items`), enabled: !!cafeId, }); const categoryNameById = useMemo( () => buildCategoryNameMap(categories), [categories] ); const invalidateMenu = () => { queryClient.invalidateQueries({ queryKey: ["menu-items-all", cafeId] }); queryClient.invalidateQueries({ queryKey: ["menu-categories", cafeId] }); }; const addCategory = useMutation({ mutationFn: () => apiPost(`/api/cafes/${cafeId}/menu/categories`, { name: catName, sortOrder: categories.length + 1, discountPercent: 0, icon: catIcon.trim() || null, iconPresetId: catIconPreset.iconPresetId, iconStyle: catIconPreset.iconPresetId ? catIconPreset.iconStyle : null, imageUrl: catImageUrl.trim() || null, }), onSuccess: () => { setCatName(""); setCatIcon(""); setCatIconPreset({ iconPresetId: null, iconStyle: DEFAULT_CATEGORY_ICON_STYLE }); setCatImageUrl(""); invalidateMenu(); }, }); const updateCategory = useMutation({ mutationFn: (id: string) => apiPatch(`/api/cafes/${cafeId}/menu/categories/${id}`, { name: editCatName, icon: mediaField(editCatIcon), iconPresetId: editCatIconPreset.iconPresetId ?? "", iconStyle: editCatIconPreset.iconPresetId ? editCatIconPreset.iconStyle : "", imageUrl: mediaField(editCatImageUrl), }), onSuccess: () => { setEditingCategoryId(null); invalidateMenu(); }, }); const addItem = useMutation({ mutationFn: () => apiPost(`/api/cafes/${cafeId}/menu/items`, { categoryId: itemCategoryId, name: itemName, nameEn: itemNameEn.trim(), price: parseFloat(itemPrice), discountPercent: parseFloat(itemDiscount) || 0, imageUrl: itemImageUrl || null, videoUrl: itemVideoUrl || null, model3dUrl: itemModel3dUrl || null, }), onSuccess: () => { setItemName(""); setItemNameEn(""); setItemPrice(""); setItemDiscount("0"); setItemImageUrl(""); setItemVideoUrl(""); setItemModel3dUrl(""); invalidateMenu(); }, }); const updateItem = useMutation({ mutationFn: (id: string) => apiPatch(`/api/cafes/${cafeId}/menu/items/${id}`, { name: editName, nameEn: editNameEn.trim(), price: parseFloat(editPrice), discountPercent: parseFloat(editDiscount) || 0, imageUrl: mediaField(editImageUrl), videoUrl: mediaField(editVideoUrl), model3dUrl: mediaField(editModel3dUrl), }), onSuccess: () => { setEditingId(null); invalidateMenu(); }, }); const toggleItem = useMutation({ mutationFn: ({ id, isAvailable }: { id: string; isAvailable: boolean }) => apiPatch(`/api/cafes/${cafeId}/menu/items/${id}/availability`, { isAvailable }), onSuccess: invalidateMenu, }); const startCategoryEdit = (cat: MenuCategory) => { setEditingCategoryId(cat.id); setEditCatName(cat.name); setEditCatIcon(cat.icon ?? ""); setEditCatIconPreset({ iconPresetId: cat.iconPresetId ?? null, iconStyle: (cat.iconStyle as CategoryIconSelection["iconStyle"]) ?? DEFAULT_CATEGORY_ICON_STYLE, }); setEditCatImageUrl(cat.imageUrl ?? ""); }; const startEdit = (item: MenuItem) => { setEditingId(item.id); setEditName(item.name); setEditNameEn(item.nameEn ?? ""); setEditPrice(String(item.price)); setEditDiscount(String(item.discountPercent)); setEditImageUrl(item.imageUrl ?? ""); setEditVideoUrl(item.videoUrl ?? ""); setEditModel3dUrl(item.model3dUrl ?? ""); }; if (!cafeId) return null; return (

{t("categories")}

{t("addCategory")}
setCatName(e.target.value)} />
setCatImageUrl(url ?? "")} />
{categories.map((c) => { const isEditingCat = editingCategoryId === c.id; return (
{isEditingCat ? (
setEditCatName(e.target.value)} /> setEditCatImageUrl(url ?? "")} />
) : ( <> {c.name} )}
); })}

{t("items")}

setItemName(e.target.value)} /> setItemNameEn(e.target.value)} dir="ltr" className="text-start" /> setItemPrice(e.target.value)} inputMode="numeric" dir="ltr" className="text-end" /> setItemDiscount(e.target.value)} inputMode="numeric" dir="ltr" className="text-end" />
setItemImageUrl(url ?? "")} onVideoChange={(url) => setItemVideoUrl(url ?? "")} /> setItemModel3dUrl(url ?? "")} />
{isLoading ? (

{tCommon("loading")}

) : items.length === 0 ? (

{t("empty")}

) : (
{items.map((item) => { const kind = inferMenuItemKind( item.categoryId, categoryNameById.get(item.categoryId) ); const hasDiscount = item.discountPercent > 0; const salePrice = discountedPrice(item.price, item.discountPercent); const isEditing = editingId === item.id; return ( {hasDiscount ? ( {formatNumber(item.discountPercent)}٪ {t("discountBadge")} ) : null}
{item.videoUrl ? ( ) : null} {item.model3dUrl ? ( 3D ) : null}
{isEditing ? (
setEditName(e.target.value)} /> setEditNameEn(e.target.value)} dir="ltr" className="text-start" /> setEditPrice(e.target.value)} dir="ltr" className="text-end" /> setEditDiscount(e.target.value)} dir="ltr" className="text-end" /> setEditImageUrl(url ?? "")} onVideoChange={(url) => setEditVideoUrl(url ?? "")} /> setEditModel3dUrl(url ?? "")} /> setEditModel3dUrl(url)} />
) : ( <>
{hasDiscount ? ( <> {formatCurrency(item.price)} {formatCurrency(salePrice)} ) : ( {formatCurrency(item.price)} )}
)}
); })}
)}
); }