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
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:
@@ -763,7 +763,13 @@
|
||||
"addItemSuccess": "تمت إضافة الصنف",
|
||||
"updateItemSuccess": "تم تحديث الصنف",
|
||||
"addCategorySuccess": "تمت إضافة الفئة",
|
||||
"updateCategorySuccess": "تم تحديث الفئة"
|
||||
"updateCategorySuccess": "تم تحديث الفئة",
|
||||
"deleteItemConfirmTitle": "حذف الصنف",
|
||||
"deleteItemConfirmDesc": "هل أنت متأكد من حذف «{name}»؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"deleteItemSuccess": "تم حذف الصنف",
|
||||
"deleteCategoryConfirmTitle": "حذف الفئة",
|
||||
"deleteCategoryConfirmDesc": "هل أنت متأكد من حذف الفئة «{name}»؟",
|
||||
"deleteCategorySuccess": "تم حذف الفئة"
|
||||
},
|
||||
"branchMenu": {
|
||||
"title": "قائمة الفرع",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user