From 8f81a62ec9e7ce5a38a1dce128e03a75621364ca Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 28 May 2026 08:10:25 +0330 Subject: [PATCH] Redesign menu admin screen with two-panel layout and modal forms UX improvements: - Two-panel layout: sticky category sidebar (desktop) + items grid - Mobile: horizontal scrolling category pills above items - Category sidebar shows item count badge per category; edit on hover - Items search bar + instant filter (name, English name, Arabic name) - Category filter drives items grid (click sidebar = filter items) - Item cards: image hover reveals Edit button (progressive disclosure) - Out-of-stock overlay + inline toggle switch (replaces text button) - Add/Edit item opens in clean modal overlay (no jarring inline expansion) - Add/Edit category opens in separate modal - nameEn made optional (was blocking new item creation) - Consolidated form state with single object per form - Empty state with illustration and Add CTA - Skeleton loading grid - Branch overrides tab wired to branchId from store - New i18n keys for search, counts, states in en/fa/ar Co-Authored-By: Claude Sonnet 4.6 --- web/dashboard/messages/ar.json | 19 +- web/dashboard/messages/en.json | 19 +- web/dashboard/messages/fa.json | 19 +- .../src/components/menu/menu-admin-screen.tsx | 1208 +++++++++++------ 4 files changed, 840 insertions(+), 425 deletions(-) diff --git a/web/dashboard/messages/ar.json b/web/dashboard/messages/ar.json index fd6e558..74ce33c 100644 --- a/web/dashboard/messages/ar.json +++ b/web/dashboard/messages/ar.json @@ -651,7 +651,24 @@ "media": "صورة وفيديو", "tabCatalog": "الكتالوج", "tabBranch": "إعدادات الفرع", - "selectBranchForOverrides": "اختر فرعاً من الأعلى لإدارة قائمة الفرع." + "selectBranchForOverrides": "اختر فرعاً من الأعلى لإدارة قائمة الفرع.", + "allItems": "كل الأصناف", + "searchItemsPlaceholder": "ابحث عن أصناف…", + "itemCount": "{count} أصناف", + "noItemsInCategory": "لا أصناف في هذه الفئة بعد", + "noItemsMatchSearch": "لا أصناف تطابق بحثك", + "outOfStock": "نفد المخزون", + "newItem": "صنف جديد", + "newCategory": "فئة جديدة", + "editCategoryTitle": "تعديل الفئة", + "close": "إغلاق", + "saving": "جاري الحفظ…", + "model3d": "نموذج ثلاثي الأبعاد", + "nameEnOptional": "الاسم بالإنجليزية (اختياري)", + "addItemSuccess": "تمت إضافة الصنف", + "updateItemSuccess": "تم تحديث الصنف", + "addCategorySuccess": "تمت إضافة الفئة", + "updateCategorySuccess": "تم تحديث الفئة" }, "branchMenu": { "title": "قائمة الفرع", diff --git a/web/dashboard/messages/en.json b/web/dashboard/messages/en.json index 868f3ea..7b55f83 100644 --- a/web/dashboard/messages/en.json +++ b/web/dashboard/messages/en.json @@ -668,7 +668,24 @@ "media": "Image & video", "tabCatalog": "Catalog", "tabBranch": "Branch settings", - "selectBranchForOverrides": "Select a branch above to manage its menu overrides." + "selectBranchForOverrides": "Select a branch above to manage its menu overrides.", + "allItems": "All items", + "searchItemsPlaceholder": "Search items…", + "itemCount": "{count} items", + "noItemsInCategory": "No items in this category yet", + "noItemsMatchSearch": "No items match your search", + "outOfStock": "Out of stock", + "newItem": "New item", + "newCategory": "New category", + "editCategoryTitle": "Edit category", + "close": "Close", + "saving": "Saving…", + "model3d": "3D model", + "nameEnOptional": "English name (optional)", + "addItemSuccess": "Item added", + "updateItemSuccess": "Item updated", + "addCategorySuccess": "Category added", + "updateCategorySuccess": "Category updated" }, "branchMenu": { "title": "Branch Menu", diff --git a/web/dashboard/messages/fa.json b/web/dashboard/messages/fa.json index 056f493..a4d2f08 100644 --- a/web/dashboard/messages/fa.json +++ b/web/dashboard/messages/fa.json @@ -668,7 +668,24 @@ "media": "تصویر و ویدیو", "tabCatalog": "کاتالوگ", "tabBranch": "تنظیمات شعبه", - "selectBranchForOverrides": "برای تنظیم منوی شعبه، یک شعبه از بالا انتخاب کنید." + "selectBranchForOverrides": "برای تنظیم منوی شعبه، یک شعبه از بالا انتخاب کنید.", + "allItems": "همه آیتم‌ها", + "searchItemsPlaceholder": "جستجوی آیتم‌ها…", + "itemCount": "{count} آیتم", + "noItemsInCategory": "هنوز آیتمی در این دسته نیست", + "noItemsMatchSearch": "آیتمی با این عبارت یافت نشد", + "outOfStock": "ناموجود", + "newItem": "آیتم جدید", + "newCategory": "دسته جدید", + "editCategoryTitle": "ویرایش دسته", + "close": "بستن", + "saving": "در حال ذخیره…", + "model3d": "مدل سه‌بعدی", + "nameEnOptional": "نام انگلیسی (اختیاری)", + "addItemSuccess": "آیتم اضافه شد", + "updateItemSuccess": "آیتم به‌روز شد", + "addCategorySuccess": "دسته اضافه شد", + "updateCategorySuccess": "دسته به‌روز شد" }, "branchMenu": { "title": "منوی شعبه", diff --git a/web/dashboard/src/components/menu/menu-admin-screen.tsx b/web/dashboard/src/components/menu/menu-admin-screen.tsx index 7aed279..dace65f 100644 --- a/web/dashboard/src/components/menu/menu-admin-screen.tsx +++ b/web/dashboard/src/components/menu/menu-admin-screen.tsx @@ -4,7 +4,7 @@ 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 { Box, Pencil, Plus, Search, Video, X } 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"; @@ -13,18 +13,22 @@ import type { CategoryIconSelection } from "@/components/menu/category-preset-pi 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 { useBranchStore } from "@/lib/stores/branch.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 { Skeleton } from "@/components/ui/skeleton"; 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"; +import { menuItemMatchesSearch } from "@/lib/menu-display"; +import { BranchMenuOverrides } from "@/components/menu/branch-menu-overrides"; + +// ─── Types ──────────────────────────────────────────────────────────────────── interface MenuCategory { id: string; @@ -54,6 +58,26 @@ interface MenuItem { isAvailable: boolean; } +interface ItemForm { + categoryId: string; + name: string; + nameEn: string; + price: string; + discount: string; + imageUrl: string; + videoUrl: string; + model3dUrl: string; +} + +interface CatForm { + name: string; + icon: string; + iconPreset: CategoryIconSelection; + imageUrl: string; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + function discountedPrice(price: number, percent: number) { if (percent <= 0) return price; return Math.round(price * (1 - percent / 100)); @@ -63,6 +87,98 @@ function mediaField(url: string) { return url.trim() === "" ? "" : url; } +const defaultItemForm: ItemForm = { + categoryId: "", + name: "", + nameEn: "", + price: "", + discount: "0", + imageUrl: "", + videoUrl: "", + model3dUrl: "", +}; + +const defaultCatForm: CatForm = { + name: "", + icon: "", + iconPreset: { iconPresetId: null, iconStyle: DEFAULT_CATEGORY_ICON_STYLE }, + imageUrl: "", +}; + +// ─── Toggle Switch ──────────────────────────────────────────────────────────── + +function ToggleSwitch({ + checked, + onChange, + disabled, + label, +}: { + checked: boolean; + onChange: (v: boolean) => void; + disabled?: boolean; + label?: string; +}) { + return ( + + ); +} + +// ─── Modal wrapper ──────────────────────────────────────────────────────────── + +function Modal({ + open, + onClose, + title, + children, + maxWidth = "max-w-lg", +}: { + open: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; + maxWidth?: string; +}) { + if (!open) return null; + return ( +
+
+
+

{title}

+ +
+
{children}
+
+
+ ); +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + export function MenuAdminScreen() { const t = useTranslations("menuAdmin"); const tCommon = useTranslations("common"); @@ -70,41 +186,25 @@ export function MenuAdminScreen() { const locale = useLocale(); const numberLocale = locale === "en" ? "en-US" : "fa-IR"; const cafeId = useAuthStore((s) => s.user?.cafeId); + const branchId = useBranchStore((s) => s.branchId); 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(""); + // ── UI state ─────────────────────────────────────────────────────────────── + const [activeTab, setActiveTab] = useState<"catalog" | "branch">("catalog"); + const [selectedCategoryId, setSelectedCategoryId] = useState("all"); + const [itemSearch, setItemSearch] = 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(""); + // Item modal + const [itemModalOpen, setItemModalOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [itemForm, setItemForm] = useState(defaultItemForm); + // Category modal + const [catModalOpen, setCatModalOpen] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); + const [catForm, setCatForm] = useState(defaultCatForm); + + // ── Data queries ─────────────────────────────────────────────────────────── const { data: categories = [] } = useQuery({ queryKey: ["menu-categories", cafeId], queryFn: () => apiGet(`/api/cafes/${cafeId}/menu/categories`), @@ -122,450 +222,714 @@ export function MenuAdminScreen() { [categories] ); + // ── Derived data ─────────────────────────────────────────────────────────── + const itemCountByCategory = useMemo(() => { + const counts: Record = {}; + for (const item of items) { + counts[item.categoryId] = (counts[item.categoryId] ?? 0) + 1; + } + return counts; + }, [items]); + + const filteredItems = useMemo(() => { + let result = items; + if (selectedCategoryId !== "all") { + result = result.filter((i) => i.categoryId === selectedCategoryId); + } + const q = itemSearch.trim(); + if (q) { + result = result.filter((i) => menuItemMatchesSearch(i, q, locale)); + } + return result; + }, [items, selectedCategoryId, itemSearch, locale]); + + // ── Mutations ────────────────────────────────────────────────────────────── const invalidateMenu = () => { queryClient.invalidateQueries({ queryKey: ["menu-items-all", cafeId] }); queryClient.invalidateQueries({ queryKey: ["menu-categories", cafeId] }); + queryClient.invalidateQueries({ queryKey: ["menu-items", 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({ + const addItemMutation = 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, + categoryId: itemForm.categoryId, + name: itemForm.name, + nameEn: itemForm.nameEn.trim() || null, + price: parseFloat(itemForm.price), + discountPercent: parseFloat(itemForm.discount) || 0, + imageUrl: itemForm.imageUrl || null, + videoUrl: itemForm.videoUrl || null, + model3dUrl: itemForm.model3dUrl || null, }), onSuccess: () => { - setItemName(""); - setItemNameEn(""); - setItemPrice(""); - setItemDiscount("0"); - setItemImageUrl(""); - setItemVideoUrl(""); - setItemModel3dUrl(""); + setItemModalOpen(false); invalidateMenu(); }, }); - const updateItem = useMutation({ + const updateItemMutation = 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), + name: itemForm.name, + nameEn: itemForm.nameEn.trim() || null, + price: parseFloat(itemForm.price), + discountPercent: parseFloat(itemForm.discount) || 0, + imageUrl: mediaField(itemForm.imageUrl), + videoUrl: mediaField(itemForm.videoUrl), + model3dUrl: mediaField(itemForm.model3dUrl), }), onSuccess: () => { - setEditingId(null); + setItemModalOpen(false); invalidateMenu(); }, }); - const toggleItem = useMutation({ + const toggleItemMutation = 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, + const addCategoryMutation = useMutation({ + mutationFn: () => + apiPost(`/api/cafes/${cafeId}/menu/categories`, { + name: catForm.name, + sortOrder: categories.length + 1, + discountPercent: 0, + icon: catForm.icon.trim() || null, + iconPresetId: catForm.iconPreset.iconPresetId, + iconStyle: catForm.iconPreset.iconPresetId ? catForm.iconPreset.iconStyle : null, + imageUrl: catForm.imageUrl.trim() || null, + }), + onSuccess: () => { + setCatModalOpen(false); + invalidateMenu(); + }, + }); + + const updateCategoryMutation = useMutation({ + mutationFn: (id: string) => + apiPatch(`/api/cafes/${cafeId}/menu/categories/${id}`, { + name: catForm.name, + icon: mediaField(catForm.icon), + iconPresetId: catForm.iconPreset.iconPresetId ?? "", + iconStyle: catForm.iconPreset.iconPresetId ? catForm.iconPreset.iconStyle : "", + imageUrl: mediaField(catForm.imageUrl), + }), + onSuccess: () => { + setCatModalOpen(false); + invalidateMenu(); + }, + }); + + // ── Modal openers ────────────────────────────────────────────────────────── + const openAddItem = () => { + setEditingItem(null); + setItemForm({ + ...defaultItemForm, + categoryId: + selectedCategoryId !== "all" ? selectedCategoryId : (categories[0]?.id ?? ""), }); - setEditCatImageUrl(cat.imageUrl ?? ""); + setItemModalOpen(true); }; - 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 ?? ""); + const openEditItem = (item: MenuItem) => { + setEditingItem(item); + setItemForm({ + categoryId: item.categoryId, + name: item.name, + nameEn: item.nameEn ?? "", + price: String(item.price), + discount: String(item.discountPercent), + imageUrl: item.imageUrl ?? "", + videoUrl: item.videoUrl ?? "", + model3dUrl: item.model3dUrl ?? "", + }); + setItemModalOpen(true); }; + const openAddCategory = () => { + setEditingCategory(null); + setCatForm(defaultCatForm); + setCatModalOpen(true); + }; + + const openEditCategory = (cat: MenuCategory) => { + setEditingCategory(cat); + setCatForm({ + name: cat.name, + icon: cat.icon ?? "", + iconPreset: { + iconPresetId: cat.iconPresetId ?? null, + iconStyle: + (cat.iconStyle as CategoryIconSelection["iconStyle"]) ?? + DEFAULT_CATEGORY_ICON_STYLE, + }, + imageUrl: cat.imageUrl ?? "", + }); + setCatModalOpen(true); + }; + + // ── Form submit handlers ─────────────────────────────────────────────────── + const handleItemSave = () => { + if (editingItem) { + updateItemMutation.mutate(editingItem.id); + } else { + addItemMutation.mutate(); + } + }; + + const handleCategorySave = () => { + if (editingCategory) { + updateCategoryMutation.mutate(editingCategory.id); + } else { + addCategoryMutation.mutate(); + } + }; + + const itemMutationBusy = + addItemMutation.isPending || updateItemMutation.isPending; + const catMutationBusy = + addCategoryMutation.isPending || updateCategoryMutation.isPending; + + const itemFormValid = + itemForm.name.trim() && + itemForm.categoryId && + itemForm.price && + !isNaN(parseFloat(itemForm.price)); + if (!cafeId) return null; + // ── Tab bar ──────────────────────────────────────────────────────────────── return ( -
+
- - -

- {t("categories")} -

- {t("addCategory")} -
- -
- - setCatName(e.target.value)} /> - - -
- setCatImageUrl(url ?? "")} + + {/* Tab switcher */} +
+ + +
+ + {/* Branch tab */} + {activeTab === "branch" ? ( + branchId ? ( + -
- {categories.map((c) => { - const isEditingCat = editingCategoryId === c.id; - return ( -
+ {t("selectBranchForOverrides")} +

+ ) + ) : ( + /* ── Catalog tab ─────────────────────────────────────────────────── */ +
+ + {/* ── Category Sidebar (desktop) ─────────────────────────────── */} + + + {/* ── Items panel ────────────────────────────────────────────── */} +
+ + {/* Mobile category tabs */} +
+ + {categories.map((cat) => ( + - -
-
- ) : ( - <> - {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" - /> - - + ))} + +
+ + {/* Search + Add bar */} +
+
+ + setItemSearch(e.target.value)} + placeholder={t("searchItemsPlaceholder")} + className="h-9 ps-9 pe-9" + /> + {itemSearch ? ( + + ) : null} +
+
- - 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; + {/* Items grid */} + {isLoading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+ ) : filteredItems.length === 0 ? ( + /* Empty state */ +
+
🍽️
+

+ {itemSearch ? t("noItemsMatchSearch") : t("noItemsInCategory")} +

+ {!itemSearch ? ( + + ) : null} +
+ ) : ( +
+ {filteredItems.map((item) => { + const kind = inferMenuItemKind( + item.categoryId, + categoryNameById.get(item.categoryId) + ); + const hasDiscount = item.discountPercent > 0; + const salePrice = discountedPrice(item.price, item.discountPercent); - 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 ?? "")} + {/* Image area */} +
+ - setEditModel3dUrl(url ?? "")} - /> - setEditModel3dUrl(url)} - /> -
- - -
-
- ) : ( - <> - -
- {hasDiscount ? ( - <> - - {formatCurrency(item.price)} - - - {formatCurrency(salePrice)} - - - ) : ( - - {formatCurrency(item.price)} - - )} -
-
- + + {/* Hover overlay — edit button */} +
- - )} - - - ); - })} + + {/* Discount badge */} + {hasDiscount ? ( + + {formatNumber(item.discountPercent, numberLocale)}% {t("discountBadge")} + + ) : null} + + {/* Media badges */} +
+ {item.videoUrl ? ( + + + ) : null} + {item.model3dUrl ? ( + + + 3D + + ) : null} +
+ + {/* Out of stock overlay */} + {!item.isAvailable ? ( +
+ + {t("outOfStock")} + +
+ ) : null} +
+ + {/* Body */} +
+
+ +
+ {hasDiscount ? ( + <> + + {formatCurrency(item.price, numberLocale)} + + + {formatCurrency(salePrice, numberLocale)} + + + ) : ( + + {formatCurrency(item.price, numberLocale)} + + )} +
+
+ + {/* Availability toggle */} + + toggleItemMutation.mutate({ id: item.id, isAvailable: v }) + } + disabled={toggleItemMutation.isPending} + label={ + item.isAvailable ? t("available") : t("unavailable") + } + /> +
+
+ ); + })} +
+ )}
- )} -
+
+ )} + + {/* ── Item Add / Edit Modal ─────────────────────────────────────────── */} + setItemModalOpen(false)} + title={editingItem ? t("editItem") : t("newItem")} + maxWidth="max-w-lg" + > +
+ {/* Category selector */} + + + + +
+ + setItemForm((f) => ({ ...f, name: e.target.value }))} + autoFocus + /> + + + + setItemForm((f) => ({ ...f, nameEn: e.target.value })) + } + dir="ltr" + className="text-start" + placeholder="e.g. Espresso" + /> + +
+ +
+ + setItemForm((f) => ({ ...f, price: e.target.value }))} + inputMode="numeric" + dir="ltr" + className="text-end" + /> + + + + setItemForm((f) => ({ ...f, discount: e.target.value })) + } + inputMode="numeric" + dir="ltr" + className="text-end" + /> + +
+ + {/* Media */} + + + setItemForm((f) => ({ ...f, imageUrl: url ?? "" })) + } + onVideoChange={(url) => + setItemForm((f) => ({ ...f, videoUrl: url ?? "" })) + } + /> + + + {/* 3D model */} + + + setItemForm((f) => ({ ...f, model3dUrl: url ?? "" })) + } + /> + {editingItem ? ( + + setItemForm((f) => ({ ...f, model3dUrl: url })) + } + /> + ) : null} + + + {/* Actions */} +
+ + +
+
+
+ + {/* ── Category Add / Edit Modal ─────────────────────────────────────── */} + setCatModalOpen(false)} + title={editingCategory ? t("editCategoryTitle") : t("newCategory")} + maxWidth="max-w-md" + > +
+ + setCatForm((f) => ({ ...f, name: e.target.value }))} + autoFocus + /> + + + setCatForm((f) => ({ ...f, icon }))} + onPresetChange={(iconPreset) => setCatForm((f) => ({ ...f, iconPreset }))} + onImageChange={(url) => + setCatForm((f) => ({ ...f, imageUrl: url ?? "" })) + } + /> + +
+ + +
+
+
); }