975 lines
34 KiB
TypeScript
975 lines
34 KiB
TypeScript
"use client";
|
|
import React, { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { PlusIcon, MenuIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
|
import {
|
|
MasterMenu,
|
|
getMasterMenus,
|
|
getMasterMenuById,
|
|
createMasterMenu,
|
|
updateMasterMenu,
|
|
deleteMasterMenu,
|
|
} from "@/service/menu-modules";
|
|
import {
|
|
MenuAction,
|
|
getMenuActionsByMenuId,
|
|
createMenuAction,
|
|
updateMenuAction,
|
|
deleteMenuAction,
|
|
createMenuActionsBatch,
|
|
} from "@/service/menu-actions";
|
|
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
|
import Swal from "sweetalert2";
|
|
import { FormField } from "@/components/form/common/FormField";
|
|
import { getCookiesDecrypt } from "@/lib/utils";
|
|
|
|
export default function MenuManagementPage() {
|
|
const router = useRouter();
|
|
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [editingMenu, setEditingMenu] = useState<MasterMenu | null>(null);
|
|
const [selectedMenuForActions, setSelectedMenuForActions] = useState<MasterMenu | null>(null);
|
|
const [menuActions, setMenuActions] = useState<MenuAction[]>([]);
|
|
const [isActionsDialogOpen, setIsActionsDialogOpen] = useState(false);
|
|
const [isActionFormOpen, setIsActionFormOpen] = useState(false);
|
|
const [editingAction, setEditingAction] = useState<MenuAction | null>(null);
|
|
const [actionFormData, setActionFormData] = useState({
|
|
actionCode: "",
|
|
actionName: "",
|
|
description: "",
|
|
pathUrl: "",
|
|
httpMethod: "none",
|
|
position: 0,
|
|
});
|
|
const [formData, setFormData] = useState({
|
|
name: "",
|
|
description: "",
|
|
group: "",
|
|
statusId: 1,
|
|
parentMenuId: undefined as number | undefined,
|
|
icon: "",
|
|
});
|
|
|
|
useEffect(() => {
|
|
// Check if user has roleId = 1
|
|
const roleId = getCookiesDecrypt("urie");
|
|
if (Number(roleId) !== 1) {
|
|
Swal.fire({
|
|
title: "Access Denied",
|
|
text: "You don't have permission to access this page",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
}).then(() => {
|
|
router.push("/admin/dashboard");
|
|
});
|
|
return;
|
|
}
|
|
|
|
loadData();
|
|
}, [router]);
|
|
|
|
const loadData = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const menusRes = await getMasterMenus({ limit: 100 });
|
|
|
|
if (menusRes?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: menusRes?.message || "Failed to load menus",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
setMenus([]);
|
|
} else {
|
|
// Transform snake_case to camelCase for consistency
|
|
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
|
|
...menu,
|
|
moduleId: menu.module_id || menu.moduleId,
|
|
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
|
|
statusId: menu.status_id || menu.statusId,
|
|
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
|
|
}));
|
|
setMenus(menusData);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading data:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "An unexpected error occurred while loading menus",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
setMenus([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleOpenDialog = async (menu?: MasterMenu) => {
|
|
if (menu) {
|
|
// Fetch fresh data from API to ensure all fields are loaded correctly
|
|
try {
|
|
const res = await getMasterMenuById(menu.id);
|
|
if (!res?.error && res?.data?.data) {
|
|
const menuData = res.data.data as any;
|
|
setEditingMenu(menu);
|
|
setFormData({
|
|
name: menuData.name || menu.name || "",
|
|
description: menuData.description || menu.description || "",
|
|
group: menuData.group || menu.group || "",
|
|
statusId: menuData.status_id || menuData.statusId || menu.statusId || 1,
|
|
parentMenuId: menuData.parent_menu_id !== undefined && menuData.parent_menu_id !== null
|
|
? menuData.parent_menu_id
|
|
: (menuData.parentMenuId !== undefined && menuData.parentMenuId !== null
|
|
? menuData.parentMenuId
|
|
: (menu.parentMenuId || undefined)),
|
|
icon: menuData.icon || menu.icon || "",
|
|
});
|
|
} else {
|
|
// Fallback to menu object if API call fails
|
|
setEditingMenu(menu);
|
|
setFormData({
|
|
name: menu.name || "",
|
|
description: menu.description || "",
|
|
group: menu.group || "",
|
|
statusId: (menu as any).status_id || menu.statusId || 1,
|
|
parentMenuId: (menu as any).parent_menu_id !== undefined && (menu as any).parent_menu_id !== null
|
|
? (menu as any).parent_menu_id
|
|
: (menu.parentMenuId || undefined),
|
|
icon: menu.icon || "",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading menu details:", error);
|
|
// Fallback to menu object if API call fails
|
|
setEditingMenu(menu);
|
|
setFormData({
|
|
name: menu.name || "",
|
|
description: menu.description || "",
|
|
group: menu.group || "",
|
|
statusId: (menu as any).status_id || menu.statusId || 1,
|
|
parentMenuId: (menu as any).parent_menu_id !== undefined && (menu as any).parent_menu_id !== null
|
|
? (menu as any).parent_menu_id
|
|
: (menu.parentMenuId || undefined),
|
|
icon: menu.icon || "",
|
|
});
|
|
}
|
|
} else {
|
|
setEditingMenu(null);
|
|
setFormData({
|
|
name: "",
|
|
description: "",
|
|
group: "",
|
|
statusId: 1,
|
|
parentMenuId: undefined,
|
|
icon: "",
|
|
});
|
|
}
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
// Prepare payload without moduleId
|
|
const payload = {
|
|
name: formData.name,
|
|
description: formData.description,
|
|
group: formData.group,
|
|
statusId: formData.statusId,
|
|
parentMenuId: formData.parentMenuId,
|
|
icon: formData.icon,
|
|
};
|
|
|
|
if (editingMenu) {
|
|
const res = await updateMasterMenu(editingMenu.id, payload);
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to update menu",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
title: "Success",
|
|
text: "Menu updated successfully",
|
|
icon: "success",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
await loadData();
|
|
setIsDialogOpen(false);
|
|
}
|
|
} else {
|
|
const res = await createMasterMenu(payload);
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to create menu",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
title: "Success",
|
|
text: "Menu created successfully",
|
|
icon: "success",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
await loadData();
|
|
setIsDialogOpen(false);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error saving menu:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "An unexpected error occurred",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleManageActions = async (menu: MasterMenu) => {
|
|
try {
|
|
setSelectedMenuForActions(menu);
|
|
setIsActionsDialogOpen(true);
|
|
await loadMenuActions(menu.id);
|
|
} catch (error) {
|
|
console.error("Error opening actions dialog:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "Failed to open actions management",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const loadMenuActions = async (menuId: number) => {
|
|
try {
|
|
const res = await getMenuActionsByMenuId(menuId);
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to load menu actions",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
setMenuActions([]);
|
|
} else {
|
|
setMenuActions(res?.data?.data || []);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading menu actions:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "An unexpected error occurred while loading menu actions",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
setMenuActions([]);
|
|
}
|
|
};
|
|
|
|
const handleOpenActionForm = (action?: MenuAction) => {
|
|
if (action) {
|
|
setEditingAction(action);
|
|
setActionFormData({
|
|
actionCode: action.actionCode,
|
|
actionName: action.actionName,
|
|
description: action.description || "",
|
|
pathUrl: action.pathUrl || "",
|
|
httpMethod: action.httpMethod || "none",
|
|
position: action.position || 0,
|
|
});
|
|
} else {
|
|
setEditingAction(null);
|
|
setActionFormData({
|
|
actionCode: "",
|
|
actionName: "",
|
|
description: "",
|
|
pathUrl: "",
|
|
httpMethod: "none",
|
|
position: menuActions.length + 1,
|
|
});
|
|
}
|
|
setIsActionFormOpen(true);
|
|
};
|
|
|
|
const handleSaveAction = async () => {
|
|
if (!selectedMenuForActions) return;
|
|
|
|
try {
|
|
// Prepare payload, converting "none" back to undefined for httpMethod
|
|
const payload = {
|
|
menuId: selectedMenuForActions.id,
|
|
actionCode: actionFormData.actionCode,
|
|
actionName: actionFormData.actionName,
|
|
description: actionFormData.description || undefined,
|
|
pathUrl: actionFormData.pathUrl || undefined,
|
|
httpMethod: actionFormData.httpMethod === "none" ? undefined : actionFormData.httpMethod,
|
|
position: actionFormData.position,
|
|
};
|
|
|
|
if (editingAction) {
|
|
const res = await updateMenuAction(editingAction.id, payload);
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to update action",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
title: "Success",
|
|
text: "Action updated successfully",
|
|
icon: "success",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
await loadMenuActions(selectedMenuForActions.id);
|
|
setIsActionFormOpen(false);
|
|
}
|
|
} else {
|
|
const res = await createMenuAction(payload);
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to create action",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
title: "Success",
|
|
text: "Action created successfully",
|
|
icon: "success",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
await loadMenuActions(selectedMenuForActions.id);
|
|
setIsActionFormOpen(false);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error saving action:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "An unexpected error occurred",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDeleteAction = async (action: MenuAction) => {
|
|
const result = await Swal.fire({
|
|
title: "Delete Action?",
|
|
text: `Are you sure you want to delete "${action.actionName}"?`,
|
|
icon: "warning",
|
|
showCancelButton: true,
|
|
confirmButtonText: "Yes, delete it",
|
|
cancelButtonText: "Cancel",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
|
|
if (result.isConfirmed) {
|
|
try {
|
|
const res = await deleteMenuAction(action.id);
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to delete action",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
title: "Deleted!",
|
|
text: "Action has been deleted.",
|
|
icon: "success",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
if (selectedMenuForActions) {
|
|
await loadMenuActions(selectedMenuForActions.id);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting action:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "An unexpected error occurred",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleQuickAddActions = async () => {
|
|
if (!selectedMenuForActions) return;
|
|
|
|
const standardActions = ["view", "create", "edit", "delete", "approve", "export"];
|
|
const existingActionCodes = menuActions.map(a => a.actionCode);
|
|
const newActions = standardActions.filter(code => !existingActionCodes.includes(code));
|
|
|
|
if (newActions.length === 0) {
|
|
Swal.fire({
|
|
title: "Info",
|
|
text: "All standard actions already exist",
|
|
icon: "info",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await createMenuActionsBatch({
|
|
menuId: selectedMenuForActions.id,
|
|
actionCodes: newActions,
|
|
});
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to create actions",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
title: "Success",
|
|
text: `${newActions.length} actions created successfully`,
|
|
icon: "success",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
await loadMenuActions(selectedMenuForActions.id);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error creating actions:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "An unexpected error occurred",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (menu: MasterMenu) => {
|
|
const result = await Swal.fire({
|
|
title: "Delete Menu?",
|
|
text: `Are you sure you want to delete "${menu.name}"? This action cannot be undone.`,
|
|
icon: "warning",
|
|
showCancelButton: true,
|
|
confirmButtonText: "Yes, delete it",
|
|
cancelButtonText: "Cancel",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
|
|
if (result.isConfirmed) {
|
|
try {
|
|
const res = await deleteMasterMenu(menu.id);
|
|
if (res?.error) {
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: res?.message || "Failed to delete menu",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
title: "Deleted!",
|
|
text: "Menu has been deleted.",
|
|
icon: "success",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
await loadData();
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting menu:", error);
|
|
Swal.fire({
|
|
title: "Error",
|
|
text: "An unexpected error occurred",
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
customClass: {
|
|
popup: 'swal-z-index-9999'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const roleId = getCookiesDecrypt("urie");
|
|
if (Number(roleId) !== 1) {
|
|
return null; // Will redirect in useEffect
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<SiteBreadcrumb />
|
|
<div className="container mx-auto p-6 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">Menu Management</h1>
|
|
<p className="text-gray-600 mt-2">
|
|
Manage system menus and their configurations
|
|
</p>
|
|
</div>
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
|
<PlusIcon className="h-4 w-4" />
|
|
Create Menu
|
|
</Button>
|
|
</DialogTrigger>
|
|
{/* @ts-ignore */}
|
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{editingMenu ? `Edit Menu: ${editingMenu.name}` : "Create New Menu"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<FormField
|
|
label="Menu Name"
|
|
name="name"
|
|
type="text"
|
|
placeholder="e.g., Dashboard, Content Management"
|
|
value={formData.name}
|
|
onChange={(value) => setFormData({ ...formData, name: value })}
|
|
required
|
|
/>
|
|
<FormField
|
|
label="Description"
|
|
name="description"
|
|
type="text"
|
|
placeholder="Brief description of the menu"
|
|
value={formData.description}
|
|
onChange={(value) => setFormData({ ...formData, description: value })}
|
|
required
|
|
/>
|
|
<FormField
|
|
label="Group"
|
|
name="group"
|
|
type="text"
|
|
placeholder="e.g., Main, Settings, Content"
|
|
value={formData.group}
|
|
onChange={(value) => setFormData({ ...formData, group: value })}
|
|
required
|
|
/>
|
|
<FormField
|
|
label="Parent Menu"
|
|
name="parentMenuId"
|
|
type="select"
|
|
placeholder="Select parent menu (optional)"
|
|
value={formData.parentMenuId || 0}
|
|
onChange={(value) => setFormData({ ...formData, parentMenuId: Number(value) === 0 ? undefined : Number(value) })}
|
|
options={[
|
|
{ value: 0, label: "No Parent (Root Menu)" },
|
|
...menus
|
|
.filter((m) => !m.parentMenuId || m.id !== editingMenu?.id)
|
|
.map((menu) => ({
|
|
value: menu.id,
|
|
label: `${menu.name} - ${menu.group}`,
|
|
})),
|
|
]}
|
|
/>
|
|
<FormField
|
|
label="Icon"
|
|
name="icon"
|
|
type="text"
|
|
placeholder="e.g., material-symbols:dashboard, heroicons:bars-3"
|
|
value={formData.icon}
|
|
onChange={(value) => setFormData({ ...formData, icon: value })}
|
|
helpText="Icon identifier (e.g., from Iconify)"
|
|
/>
|
|
<FormField
|
|
label="Status ID"
|
|
name="statusId"
|
|
type="number"
|
|
placeholder="1"
|
|
value={formData.statusId}
|
|
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
|
|
required
|
|
/>
|
|
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsDialogOpen(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSave}>
|
|
{editingMenu ? "Update" : "Create"} Menu
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="text-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Loading menus...</p>
|
|
</div>
|
|
) : menus.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{menus.map((menu) => {
|
|
const parentMenu = menus.find((m) => m.id === menu.parentMenuId);
|
|
return (
|
|
<Card key={menu.id} className="hover:shadow-lg transition-shadow">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span className="truncate">{menu.name}</span>
|
|
{menu.isActive ? (
|
|
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
|
Active
|
|
</span>
|
|
) : (
|
|
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
|
Inactive
|
|
</span>
|
|
)}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-2 mb-4">
|
|
<div className="text-sm text-gray-600">{menu.description}</div>
|
|
<div className="text-xs text-gray-500">
|
|
<span className="font-medium">Group:</span> {menu.group}
|
|
</div>
|
|
{parentMenu && (
|
|
<div className="text-xs text-gray-500">
|
|
<span className="font-medium">Parent:</span> {parentMenu.name}
|
|
</div>
|
|
)}
|
|
{menu.icon && (
|
|
<div className="text-xs text-gray-500">
|
|
<span className="font-medium">Icon:</span> {menu.icon}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1"
|
|
onClick={() => handleOpenDialog(menu)}
|
|
>
|
|
<EditIcon className="h-4 w-4 mr-2" />
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1"
|
|
onClick={() => handleManageActions(menu)}
|
|
>
|
|
<MenuIcon className="h-4 w-4 mr-2" />
|
|
Actions
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
onClick={() => handleDelete(menu)}
|
|
>
|
|
<DeleteIcon className="h-4 w-4 mr-2" />
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<MenuIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No Menus Found</h3>
|
|
<p className="text-gray-500 mb-4">
|
|
Create your first menu to define system navigation
|
|
</p>
|
|
<Button onClick={() => handleOpenDialog()}>
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
|
Create Menu
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Actions Management Dialog */}
|
|
<Dialog open={isActionsDialogOpen} onOpenChange={setIsActionsDialogOpen}>
|
|
{/* @ts-ignore */}
|
|
<DialogContent size="lg" className="!max-w-[60vw] !w-[60vw] !min-w-[60vw] max-h-[95vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
Manage Actions: {selectedMenuForActions?.name}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm text-gray-600">
|
|
Manage actions available for this menu
|
|
</p>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleQuickAddActions}
|
|
>
|
|
Quick Add Standard Actions
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleOpenActionForm()}
|
|
>
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
|
Add Action
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{menuActions.length > 0 ? (
|
|
<div className="border rounded-lg">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Code</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Path URL</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Position</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{menuActions.map((action) => (
|
|
<tr key={action.id}>
|
|
<td className="px-4 py-3 text-sm font-medium text-gray-900">{action.actionCode}</td>
|
|
<td className="px-4 py-3 text-sm text-gray-600">{action.actionName}</td>
|
|
<td className="px-4 py-3 text-sm text-gray-600">{action.pathUrl || "-"}</td>
|
|
<td className="px-4 py-3 text-sm text-gray-600">{action.httpMethod || "-"}</td>
|
|
<td className="px-4 py-3 text-sm text-gray-600">{action.position || "-"}</td>
|
|
<td className="px-4 py-3 text-sm">
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleOpenActionForm(action)}
|
|
>
|
|
<EditIcon className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
onClick={() => handleDeleteAction(action)}
|
|
>
|
|
<DeleteIcon className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<p className="text-gray-500 mb-4">No actions found for this menu</p>
|
|
<Button onClick={() => handleOpenActionForm()}>
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
|
Add First Action
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Action Form Dialog */}
|
|
<Dialog open={isActionFormOpen} onOpenChange={setIsActionFormOpen}>
|
|
{/* @ts-ignore */}
|
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{editingAction ? `Edit Action: ${editingAction.actionName}` : "Create New Action"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<FormField
|
|
label="Action Code"
|
|
name="actionCode"
|
|
type="text"
|
|
placeholder="e.g., view, create, edit, delete"
|
|
value={actionFormData.actionCode}
|
|
onChange={(value) => setActionFormData({ ...actionFormData, actionCode: value })}
|
|
required
|
|
helpText="Unique identifier for the action"
|
|
/>
|
|
<FormField
|
|
label="Action Name"
|
|
name="actionName"
|
|
type="text"
|
|
placeholder="e.g., View Content, Create Content"
|
|
value={actionFormData.actionName}
|
|
onChange={(value) => setActionFormData({ ...actionFormData, actionName: value })}
|
|
required
|
|
/>
|
|
<FormField
|
|
label="Description"
|
|
name="description"
|
|
type="textarea"
|
|
placeholder="Brief description of the action"
|
|
value={actionFormData.description}
|
|
onChange={(value) => setActionFormData({ ...actionFormData, description: value })}
|
|
/>
|
|
<FormField
|
|
label="Path URL"
|
|
name="pathUrl"
|
|
type="text"
|
|
placeholder="e.g., /admin/articles, /api/articles"
|
|
value={actionFormData.pathUrl}
|
|
onChange={(value) => setActionFormData({ ...actionFormData, pathUrl: value })}
|
|
helpText="Optional: URL path for routing"
|
|
/>
|
|
<FormField
|
|
label="HTTP Method"
|
|
name="httpMethod"
|
|
type="select"
|
|
placeholder="Select HTTP method"
|
|
value={actionFormData.httpMethod || "none"}
|
|
onChange={(value) => setActionFormData({ ...actionFormData, httpMethod: value })}
|
|
options={[
|
|
{ value: "none", label: "Not specified" },
|
|
{ value: "GET", label: "GET" },
|
|
{ value: "POST", label: "POST" },
|
|
{ value: "PUT", label: "PUT" },
|
|
{ value: "PATCH", label: "PATCH" },
|
|
{ value: "DELETE", label: "DELETE" },
|
|
]}
|
|
/>
|
|
<FormField
|
|
label="Position"
|
|
name="position"
|
|
type="number"
|
|
placeholder="1"
|
|
value={actionFormData.position}
|
|
onChange={(value) => setActionFormData({ ...actionFormData, position: Number(value) || 0 })}
|
|
helpText="Order of action in the menu"
|
|
/>
|
|
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsActionFormOpen(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSaveAction}>
|
|
{editingAction ? "Update" : "Create"} Action
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|