feat(menu): delete category/item + fix RTL availability toggle
CI/CD / CI · API (dotnet build + test) (push) Successful in 56s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 58s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m18s

- Add DELETE /api/cafes/{cafeId}/menu/items/{id} (DeleteItemAsync soft-delete,
  mirroring the existing category delete) — item delete had no backend route.
- Dashboard menu admin: destructive "delete" action in the item and category
  edit modals, behind a shared confirm dialog (AlertDialog). Deleting the
  selected category falls back to "all items".
- Fix the availability ToggleSwitch in RTL: force dir="ltr" so the knob's
  translate-x stays inside the track instead of escaping on the right
  (same fix as the admin-panel toggles).
- i18n: deleteItem/deleteCategory confirm + success strings (fa/en/ar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 12:24:09 +03:30
parent 72f95aa0db
commit 7122df57b2
6 changed files with 192 additions and 30 deletions
+7 -1
View File
@@ -763,7 +763,13 @@
"addItemSuccess": "تمت إضافة الصنف",
"updateItemSuccess": "تم تحديث الصنف",
"addCategorySuccess": "تمت إضافة الفئة",
"updateCategorySuccess": "تم تحديث الفئة"
"updateCategorySuccess": "تم تحديث الفئة",
"deleteItemConfirmTitle": "حذف الصنف",
"deleteItemConfirmDesc": "هل أنت متأكد من حذف «{name}»؟ لا يمكن التراجع عن هذا الإجراء.",
"deleteItemSuccess": "تم حذف الصنف",
"deleteCategoryConfirmTitle": "حذف الفئة",
"deleteCategoryConfirmDesc": "هل أنت متأكد من حذف الفئة «{name}»؟",
"deleteCategorySuccess": "تم حذف الفئة"
},
"branchMenu": {
"title": "قائمة الفرع",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -806,7 +806,13 @@
"addItemSuccess": "آیتم اضافه شد",
"updateItemSuccess": "آیتم به‌روز شد",
"addCategorySuccess": "دسته اضافه شد",
"updateCategorySuccess": "دسته به‌روز شد"
"updateCategorySuccess": "دسته به‌روز شد",
"deleteItemConfirmTitle": "حذف آیتم",
"deleteItemConfirmDesc": "آیا از حذف «{name}» مطمئن هستید؟ این عمل قابل بازگشت نیست.",
"deleteItemSuccess": "آیتم حذف شد",
"deleteCategoryConfirmTitle": "حذف دسته‌بندی",
"deleteCategoryConfirmDesc": "آیا از حذف دسته «{name}» مطمئن هستید؟",
"deleteCategorySuccess": "دسته حذف شد"
},
"branchMenu": {
"title": "منوی شعبه",
@@ -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<MenuCategory | null>(null);
const [catForm, setCatForm] = useState<CatForm>(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() {
</LabeledField>
{/* Actions */}
<div className="flex justify-end gap-2 border-t border-border pt-4">
<Button
variant="ghost"
onClick={() => setItemModalOpen(false)}
>
{tCommon("cancel")}
</Button>
<Button
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
disabled={!itemFormValid || itemMutationBusy}
onClick={handleItemSave}
>
{itemMutationBusy ? t("saving") : tCommon("save")}
</Button>
<div className="flex items-center justify-between gap-2 border-t border-border pt-4">
{editingItem ? (
<Button
type="button"
variant="ghost"
className="text-red-600 hover:bg-red-50 hover:text-red-700"
onClick={() =>
setConfirmDelete({
kind: "item",
id: editingItem.id,
name: editingItem.name,
})
}
>
<Trash2 className="me-1.5 size-4" />
{tCommon("delete")}
</Button>
) : (
<span />
)}
<div className="flex gap-2">
<Button variant="ghost" onClick={() => setItemModalOpen(false)}>
{tCommon("cancel")}
</Button>
<Button
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
disabled={!itemFormValid || itemMutationBusy}
onClick={handleItemSave}
>
{itemMutationBusy ? t("saving") : tCommon("save")}
</Button>
</div>
</div>
</div>
</Modal>
@@ -939,20 +999,84 @@ export function MenuAdminScreen() {
}
/>
<div className="flex justify-end gap-2 border-t border-border pt-4">
<Button variant="ghost" onClick={() => setCatModalOpen(false)}>
{tCommon("cancel")}
</Button>
<Button
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
disabled={!catForm.name.trim() || catMutationBusy}
onClick={handleCategorySave}
>
{catMutationBusy ? t("saving") : tCommon("save")}
</Button>
<div className="flex items-center justify-between gap-2 border-t border-border pt-4">
{editingCategory ? (
<Button
type="button"
variant="ghost"
className="text-red-600 hover:bg-red-50 hover:text-red-700"
onClick={() =>
setConfirmDelete({
kind: "category",
id: editingCategory.id,
name: editingCategory.name,
})
}
>
<Trash2 className="me-1.5 size-4" />
{tCommon("delete")}
</Button>
) : (
<span />
)}
<div className="flex gap-2">
<Button variant="ghost" onClick={() => setCatModalOpen(false)}>
{tCommon("cancel")}
</Button>
<Button
className="bg-[#0F6E56] hover:bg-[#0c5a46]"
disabled={!catForm.name.trim() || catMutationBusy}
onClick={handleCategorySave}
>
{catMutationBusy ? t("saving") : tCommon("save")}
</Button>
</div>
</div>
</div>
</Modal>
{/* ── Delete confirmation (items + categories) ──────────────────────── */}
<AlertDialog
open={!!confirmDelete}
onOpenChange={(open) => {
if (!open) setConfirmDelete(null);
}}
>
<AlertDialogContent dir={isRtl ? "rtl" : "ltr"}>
<AlertDialogHeader>
<AlertDialogTitle>
{confirmDelete?.kind === "category"
? t("deleteCategoryConfirmTitle")
: t("deleteItemConfirmTitle")}
</AlertDialogTitle>
<AlertDialogDescription>
{confirmDelete?.kind === "category"
? t("deleteCategoryConfirmDesc", { name: confirmDelete?.name ?? "" })
: t("deleteItemConfirmDesc", { name: confirmDelete?.name ?? "" })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 text-white hover:bg-red-700"
disabled={deleteItemMutation.isPending || deleteCategoryMutation.isPending}
onClick={(e) => {
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")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}