diff --git a/src/Meezi.API/Controllers/MenuController.cs b/src/Meezi.API/Controllers/MenuController.cs index 089073d..d399ed2 100644 --- a/src/Meezi.API/Controllers/MenuController.cs +++ b/src/Meezi.API/Controllers/MenuController.cs @@ -163,6 +163,15 @@ public class MenuController : CafeApiControllerBase return Ok(new ApiResponse(true, data)); } + [HttpDelete("items/{id}")] + public async Task DeleteItem(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var deleted = await _menuService.DeleteItemAsync(cafeId, id, cancellationToken); + if (!deleted) return NotFoundError(); + return Ok(new ApiResponse(true, new { id })); + } + [HttpGet("ai-3d/usage")] public async Task GetAi3dUsage(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) { diff --git a/src/Meezi.API/Services/MenuService.cs b/src/Meezi.API/Services/MenuService.cs index 452d084..abfd048 100644 --- a/src/Meezi.API/Services/MenuService.cs +++ b/src/Meezi.API/Services/MenuService.cs @@ -16,6 +16,7 @@ public interface IMenuService Task CreateItemAsync(string cafeId, CreateMenuItemRequest request, CancellationToken cancellationToken = default); Task UpdateItemAsync(string cafeId, string id, UpdateMenuItemRequest request, CancellationToken cancellationToken = default); Task SetAvailabilityAsync(string cafeId, string id, bool isAvailable, CancellationToken cancellationToken = default); + Task DeleteItemAsync(string cafeId, string id, CancellationToken cancellationToken = default); } public class MenuService : IMenuService @@ -192,6 +193,16 @@ public class MenuService : IMenuService return ToItemDto(entity); } + public async Task DeleteItemAsync(string cafeId, string id, CancellationToken cancellationToken = default) + { + var entity = await _db.MenuItems.FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken); + if (entity is null) return false; + + entity.DeletedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return true; + } + private static string? NormalizeOptionalText(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); diff --git a/web/dashboard/messages/ar.json b/web/dashboard/messages/ar.json index b206a9a..9ef52de 100644 --- a/web/dashboard/messages/ar.json +++ b/web/dashboard/messages/ar.json @@ -763,7 +763,13 @@ "addItemSuccess": "تمت إضافة الصنف", "updateItemSuccess": "تم تحديث الصنف", "addCategorySuccess": "تمت إضافة الفئة", - "updateCategorySuccess": "تم تحديث الفئة" + "updateCategorySuccess": "تم تحديث الفئة", + "deleteItemConfirmTitle": "حذف الصنف", + "deleteItemConfirmDesc": "هل أنت متأكد من حذف «{name}»؟ لا يمكن التراجع عن هذا الإجراء.", + "deleteItemSuccess": "تم حذف الصنف", + "deleteCategoryConfirmTitle": "حذف الفئة", + "deleteCategoryConfirmDesc": "هل أنت متأكد من حذف الفئة «{name}»؟", + "deleteCategorySuccess": "تم حذف الفئة" }, "branchMenu": { "title": "قائمة الفرع", diff --git a/web/dashboard/messages/en.json b/web/dashboard/messages/en.json index f4e79bd..6109a57 100644 --- a/web/dashboard/messages/en.json +++ b/web/dashboard/messages/en.json @@ -806,7 +806,13 @@ "addItemSuccess": "Item added", "updateItemSuccess": "Item updated", "addCategorySuccess": "Category added", - "updateCategorySuccess": "Category updated" + "updateCategorySuccess": "Category updated", + "deleteItemConfirmTitle": "Delete item", + "deleteItemConfirmDesc": "Are you sure you want to delete “{name}”? This can't be undone.", + "deleteItemSuccess": "Item deleted", + "deleteCategoryConfirmTitle": "Delete category", + "deleteCategoryConfirmDesc": "Are you sure you want to delete the “{name}” category?", + "deleteCategorySuccess": "Category deleted" }, "branchMenu": { "title": "Branch Menu", diff --git a/web/dashboard/messages/fa.json b/web/dashboard/messages/fa.json index 2d3d7a3..c443344 100644 --- a/web/dashboard/messages/fa.json +++ b/web/dashboard/messages/fa.json @@ -806,7 +806,13 @@ "addItemSuccess": "آیتم اضافه شد", "updateItemSuccess": "آیتم به‌روز شد", "addCategorySuccess": "دسته اضافه شد", - "updateCategorySuccess": "دسته به‌روز شد" + "updateCategorySuccess": "دسته به‌روز شد", + "deleteItemConfirmTitle": "حذف آیتم", + "deleteItemConfirmDesc": "آیا از حذف «{name}» مطمئن هستید؟ این عمل قابل بازگشت نیست.", + "deleteItemSuccess": "آیتم حذف شد", + "deleteCategoryConfirmTitle": "حذف دسته‌بندی", + "deleteCategoryConfirmDesc": "آیا از حذف دسته «{name}» مطمئن هستید؟", + "deleteCategorySuccess": "دسته حذف شد" }, "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 f3afb1e..c571c66 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, Plus, Search, Video, X } from "lucide-react"; +import { Box, Pencil, Plus, Search, Trash2, Video, X } from "lucide-react"; import { DemoDataBanner } from "@/components/demo/demo-data-banner"; import { Menu3dUpload } from "@/components/media/menu-3d-upload"; import { MenuAi3dGenerate } from "@/components/media/menu-ai-3d-generate"; @@ -12,7 +12,17 @@ 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 { apiDelete, apiGet, apiPatch, apiPost } from "@/lib/api/client"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { notify } from "@/lib/notify"; import { useApiError } from "@/lib/use-api-error"; import { useAuthStore } from "@/lib/stores/auth.store"; @@ -127,6 +137,9 @@ function ToggleSwitch({ aria-checked={checked} aria-label={label} type="button" + // Force LTR so the knob's translate-x stays inside the track; in RTL the + // flex start sits on the right and translate-x-4 would push it out. + dir="ltr" onClick={() => !disabled && onChange(!checked)} className={cn( "relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-200", @@ -209,6 +222,11 @@ export function MenuAdminScreen() { const [editingCategory, setEditingCategory] = useState(null); const [catForm, setCatForm] = useState(defaultCatForm); + // Delete confirmation (shared dialog for items + categories) + const [confirmDelete, setConfirmDelete] = useState< + { kind: "item" | "category"; id: string; name: string } | null + >(null); + // ── Data queries ─────────────────────────────────────────────────────────── const { data: categories = [] } = useQuery({ queryKey: ["menu-categories", cafeId], @@ -299,6 +317,30 @@ export function MenuAdminScreen() { onError: showError, }); + const deleteItemMutation = useMutation({ + mutationFn: (id: string) => apiDelete(`/api/cafes/${cafeId}/menu/items/${id}`), + onSuccess: () => { + setConfirmDelete(null); + setItemModalOpen(false); + notify.success(t("deleteItemSuccess")); + invalidateMenu(); + }, + onError: showError, + }); + + const deleteCategoryMutation = useMutation({ + mutationFn: (id: string) => apiDelete(`/api/cafes/${cafeId}/menu/categories/${id}`), + onSuccess: (_data, id) => { + setConfirmDelete(null); + setCatModalOpen(false); + // If the deleted category was selected, fall back to "all items". + setSelectedCategoryId((prev) => (prev === id ? "all" : prev)); + notify.success(t("deleteCategorySuccess")); + invalidateMenu(); + }, + onError: showError, + }); + const addCategoryMutation = useMutation({ mutationFn: () => apiPost(`/api/cafes/${cafeId}/menu/categories`, { @@ -891,20 +933,38 @@ export function MenuAdminScreen() { {/* Actions */} -
- - +
+ {editingItem ? ( + + ) : ( + + )} +
+ + +
@@ -939,20 +999,84 @@ export function MenuAdminScreen() { } /> -
- - +
+ {editingCategory ? ( + + ) : ( + + )} +
+ + +
+ + {/* ── Delete confirmation (items + categories) ──────────────────────── */} + { + if (!open) setConfirmDelete(null); + }} + > + + + + {confirmDelete?.kind === "category" + ? t("deleteCategoryConfirmTitle") + : t("deleteItemConfirmTitle")} + + + {confirmDelete?.kind === "category" + ? t("deleteCategoryConfirmDesc", { name: confirmDelete?.name ?? "" }) + : t("deleteItemConfirmDesc", { name: confirmDelete?.name ?? "" })} + + + + {tCommon("cancel")} + { + e.preventDefault(); // keep dialog open until the mutation resolves + if (!confirmDelete) return; + if (confirmDelete.kind === "category") { + deleteCategoryMutation.mutate(confirmDelete.id); + } else { + deleteItemMutation.mutate(confirmDelete.id); + } + }} + > + {deleteItemMutation.isPending || deleteCategoryMutation.isPending + ? t("saving") + : tCommon("delete")} + + + + ); }