2025-10-02 05:04:42 +00:00
|
|
|
"use client";
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
2026-01-22 02:20:17 +00:00
|
|
|
import {
|
|
|
|
|
Collapsible,
|
|
|
|
|
CollapsibleContent,
|
|
|
|
|
CollapsibleTrigger,
|
|
|
|
|
} from "@/components/ui/collapsible";
|
|
|
|
|
import {
|
|
|
|
|
ChevronDownIcon,
|
|
|
|
|
ChevronUpIcon,
|
|
|
|
|
SaveIcon,
|
|
|
|
|
EyeIcon,
|
|
|
|
|
RotateCcwIcon,
|
|
|
|
|
UsersIcon,
|
|
|
|
|
HierarchyIcon,
|
|
|
|
|
PlusIcon,
|
|
|
|
|
TrashIcon,
|
|
|
|
|
} from "@/components/icons";
|
2025-10-02 05:04:42 +00:00
|
|
|
import { FormField } from "./common/FormField";
|
|
|
|
|
import { DynamicArray } from "./common/DynamicArray";
|
|
|
|
|
import {
|
|
|
|
|
UserLevelsCreateRequest,
|
|
|
|
|
UserLevel,
|
|
|
|
|
Province,
|
|
|
|
|
createUserLevel,
|
2026-01-18 19:57:16 +00:00
|
|
|
updateUserLevel,
|
2025-10-02 05:04:42 +00:00
|
|
|
getUserLevels,
|
|
|
|
|
getProvinces,
|
|
|
|
|
} from "@/service/approval-workflows";
|
2026-01-22 02:20:17 +00:00
|
|
|
import { MasterMenu, getMasterMenus } from "@/service/menu-modules";
|
2026-01-16 03:25:30 +00:00
|
|
|
import {
|
2026-01-18 19:57:16 +00:00
|
|
|
getUserLevelMenuAccessesByUserLevelId,
|
|
|
|
|
createUserLevelMenuAccessesBatch,
|
|
|
|
|
deleteUserLevelMenuAccess,
|
|
|
|
|
UserLevelMenuAccess,
|
|
|
|
|
} from "@/service/user-level-menu-accesses";
|
|
|
|
|
import {
|
|
|
|
|
getUserLevelMenuActionAccessesByUserLevelIdAndMenuId,
|
|
|
|
|
createUserLevelMenuActionAccessesBatch,
|
|
|
|
|
deleteUserLevelMenuActionAccess,
|
|
|
|
|
UserLevelMenuActionAccess,
|
|
|
|
|
} from "@/service/user-level-menu-action-accesses";
|
2026-01-22 02:20:17 +00:00
|
|
|
import { getMenuActionsByMenuId, MenuAction } from "@/service/menu-actions";
|
2025-10-02 05:04:42 +00:00
|
|
|
import Swal from "sweetalert2";
|
|
|
|
|
|
|
|
|
|
interface UserLevelsFormProps {
|
|
|
|
|
initialData?: UserLevelsCreateRequest;
|
|
|
|
|
onSave?: (data: UserLevelsCreateRequest) => void;
|
|
|
|
|
onCancel?: () => void;
|
|
|
|
|
isLoading?: boolean;
|
|
|
|
|
mode?: "single" | "bulk";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|
|
|
|
initialData,
|
|
|
|
|
onSave,
|
|
|
|
|
onCancel,
|
|
|
|
|
isLoading = false,
|
|
|
|
|
mode = "single",
|
|
|
|
|
}) => {
|
|
|
|
|
// Form state
|
|
|
|
|
const [formData, setFormData] = useState<UserLevelsCreateRequest>({
|
|
|
|
|
name: "",
|
|
|
|
|
aliasName: "",
|
|
|
|
|
levelNumber: 1,
|
|
|
|
|
parentLevelId: undefined,
|
|
|
|
|
provinceId: undefined,
|
|
|
|
|
group: "",
|
|
|
|
|
isApprovalActive: true,
|
|
|
|
|
isActive: true,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>(
|
|
|
|
|
[],
|
|
|
|
|
);
|
2025-10-02 05:04:42 +00:00
|
|
|
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
|
|
|
|
const [provinces, setProvinces] = useState<Province[]>([]);
|
2026-01-18 19:57:16 +00:00
|
|
|
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
|
|
|
|
const [selectedMenuIds, setSelectedMenuIds] = useState<number[]>([]);
|
2026-01-22 02:20:17 +00:00
|
|
|
const [userLevelMenuAccesses, setUserLevelMenuAccesses] = useState<
|
|
|
|
|
UserLevelMenuAccess[]
|
|
|
|
|
>([]);
|
|
|
|
|
const [menuActionsMap, setMenuActionsMap] = useState<
|
|
|
|
|
Record<number, MenuAction[]>
|
|
|
|
|
>({});
|
|
|
|
|
const [selectedActionAccesses, setSelectedActionAccesses] = useState<
|
|
|
|
|
Record<number, string[]>
|
|
|
|
|
>({});
|
2025-10-02 05:04:42 +00:00
|
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
|
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
|
|
|
|
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
2026-01-22 02:20:17 +00:00
|
|
|
const [activeTab, setActiveTab] = useState(
|
|
|
|
|
mode === "single" ? "basic" : "bulk",
|
|
|
|
|
);
|
2025-10-02 05:04:42 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (initialData) {
|
|
|
|
|
setFormData(initialData);
|
|
|
|
|
}
|
|
|
|
|
}, [initialData]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadData = async () => {
|
|
|
|
|
try {
|
2026-01-18 19:57:16 +00:00
|
|
|
const [userLevelsRes, provincesRes, menusRes] = await Promise.all([
|
2025-10-02 05:04:42 +00:00
|
|
|
getUserLevels(),
|
|
|
|
|
getProvinces(),
|
2026-01-18 19:57:16 +00:00
|
|
|
getMasterMenus({ limit: 100 }),
|
2025-10-02 05:04:42 +00:00
|
|
|
]);
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
if (!userLevelsRes?.error)
|
|
|
|
|
setUserLevels(userLevelsRes?.data?.data || []);
|
2025-10-02 06:41:49 +00:00
|
|
|
if (!provincesRes?.error) setProvinces(provincesRes?.data?.data || []);
|
2026-01-18 19:57:16 +00:00
|
|
|
if (!menusRes?.error) {
|
|
|
|
|
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
|
|
|
|
|
...menu,
|
|
|
|
|
moduleId: menu.module_id || menu.moduleId,
|
2026-01-22 02:20:17 +00:00
|
|
|
parentMenuId:
|
|
|
|
|
menu.parent_menu_id !== undefined
|
|
|
|
|
? menu.parent_menu_id
|
|
|
|
|
: menu.parentMenuId,
|
2026-01-18 19:57:16 +00:00
|
|
|
statusId: menu.status_id || menu.statusId,
|
2026-01-22 02:20:17 +00:00
|
|
|
isActive:
|
|
|
|
|
menu.is_active !== undefined ? menu.is_active : menu.isActive,
|
2026-01-18 19:57:16 +00:00
|
|
|
}));
|
|
|
|
|
setMenus(menusData);
|
2026-01-22 02:20:17 +00:00
|
|
|
|
2026-01-18 19:57:16 +00:00
|
|
|
// Load actions for each menu
|
|
|
|
|
const actionsMap: Record<number, MenuAction[]> = {};
|
|
|
|
|
for (const menu of menusData) {
|
|
|
|
|
try {
|
|
|
|
|
const actionsRes = await getMenuActionsByMenuId(menu.id);
|
|
|
|
|
if (!actionsRes?.error) {
|
|
|
|
|
actionsMap[menu.id] = actionsRes?.data?.data || [];
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-01-22 02:20:17 +00:00
|
|
|
console.error(
|
|
|
|
|
`Error loading actions for menu ${menu.id}:`,
|
|
|
|
|
error,
|
|
|
|
|
);
|
2026-01-18 19:57:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setMenuActionsMap(actionsMap);
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error loading form data:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoadingData(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadData();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-16 03:25:30 +00:00
|
|
|
useEffect(() => {
|
2026-01-18 19:57:16 +00:00
|
|
|
const loadAccesses = async () => {
|
2026-01-16 03:25:30 +00:00
|
|
|
if (initialData && (initialData as any).id) {
|
2026-01-18 19:57:16 +00:00
|
|
|
const userLevelId = (initialData as any).id;
|
2026-01-16 03:25:30 +00:00
|
|
|
try {
|
2026-01-18 19:57:16 +00:00
|
|
|
// Load menu accesses
|
2026-01-22 02:20:17 +00:00
|
|
|
const menuRes =
|
|
|
|
|
await getUserLevelMenuAccessesByUserLevelId(userLevelId);
|
2026-01-18 19:57:16 +00:00
|
|
|
if (!menuRes?.error) {
|
|
|
|
|
const menuAccesses = menuRes?.data?.data || [];
|
|
|
|
|
setUserLevelMenuAccesses(menuAccesses);
|
2026-01-22 02:20:17 +00:00
|
|
|
setSelectedMenuIds(
|
|
|
|
|
menuAccesses
|
|
|
|
|
.filter((a: UserLevelMenuAccess) => a.canAccess)
|
|
|
|
|
.map((a: UserLevelMenuAccess) => a.menuId),
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-18 19:57:16 +00:00
|
|
|
// Load action accesses for each menu
|
|
|
|
|
const actionAccesses: Record<number, string[]> = {};
|
2026-01-22 02:20:17 +00:00
|
|
|
for (const menuAccess of menuAccesses.filter(
|
|
|
|
|
(a: UserLevelMenuAccess) => a.canAccess,
|
|
|
|
|
)) {
|
2026-01-18 19:57:16 +00:00
|
|
|
try {
|
2026-01-22 02:20:17 +00:00
|
|
|
const actionRes =
|
|
|
|
|
await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(
|
|
|
|
|
userLevelId,
|
|
|
|
|
menuAccess.menuId,
|
|
|
|
|
);
|
2026-01-18 19:57:16 +00:00
|
|
|
if (!actionRes?.error) {
|
|
|
|
|
const actions = actionRes?.data?.data || [];
|
|
|
|
|
actionAccesses[menuAccess.menuId] = actions
|
|
|
|
|
.filter((a: UserLevelMenuActionAccess) => a.canAccess)
|
|
|
|
|
.map((a: UserLevelMenuActionAccess) => a.actionCode);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-01-22 02:20:17 +00:00
|
|
|
console.error(
|
|
|
|
|
`Error loading action accesses for menu ${menuAccess.menuId}:`,
|
|
|
|
|
error,
|
|
|
|
|
);
|
2026-01-18 19:57:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setSelectedActionAccesses(actionAccesses);
|
2026-01-16 03:25:30 +00:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-01-18 19:57:16 +00:00
|
|
|
console.error("Error loading accesses:", error);
|
2026-01-16 03:25:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 19:57:16 +00:00
|
|
|
loadAccesses();
|
2026-01-16 03:25:30 +00:00
|
|
|
}, [initialData]);
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
const validateForm = (
|
|
|
|
|
data: UserLevelsCreateRequest,
|
|
|
|
|
): Record<string, string> => {
|
2025-10-02 05:04:42 +00:00
|
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
if (!data.name.trim()) {
|
|
|
|
|
newErrors.name = "Level name is required";
|
|
|
|
|
} else if (data.name.trim().length < 3) {
|
|
|
|
|
newErrors.name = "Level name must be at least 3 characters";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!data.aliasName.trim()) {
|
|
|
|
|
newErrors.aliasName = "Alias name is required";
|
|
|
|
|
} else if (data.aliasName.trim().length < 3) {
|
|
|
|
|
newErrors.aliasName = "Alias name must be at least 3 characters";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!data.levelNumber || data.levelNumber <= 0) {
|
|
|
|
|
newErrors.levelNumber = "Level number must be a positive integer";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for duplicate level numbers
|
2025-10-02 06:41:49 +00:00
|
|
|
// const existingLevel = userLevels.length > 0 ? userLevels.find(level => level.levelNumber === data.levelNumber) : null;
|
|
|
|
|
// if (existingLevel && (!initialData || existingLevel.id !== (initialData as any).id)) {
|
|
|
|
|
// newErrors.levelNumber = "Level number already exists";
|
|
|
|
|
// }
|
2025-10-02 05:04:42 +00:00
|
|
|
|
|
|
|
|
return newErrors;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const validateBulkForm = (): boolean => {
|
|
|
|
|
let isValid = true;
|
|
|
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
bulkFormData.forEach((data, index) => {
|
|
|
|
|
const itemErrors = validateForm(data);
|
2026-01-22 02:20:17 +00:00
|
|
|
Object.keys(itemErrors).forEach((key) => {
|
2025-10-02 05:04:42 +00:00
|
|
|
newErrors[`${index}.${key}`] = itemErrors[key];
|
|
|
|
|
});
|
|
|
|
|
if (Object.keys(itemErrors).length > 0) {
|
|
|
|
|
isValid = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setErrors(newErrors);
|
|
|
|
|
return isValid;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
const handleFieldChange = (
|
|
|
|
|
field: keyof UserLevelsCreateRequest,
|
|
|
|
|
value: any,
|
|
|
|
|
) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
2025-10-02 05:04:42 +00:00
|
|
|
if (errors[field]) {
|
2026-01-22 02:20:17 +00:00
|
|
|
setErrors((prev) => ({ ...prev, [field]: "" }));
|
2025-10-02 05:04:42 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
const handleBulkFieldChange = (
|
|
|
|
|
index: number,
|
|
|
|
|
field: keyof UserLevelsCreateRequest,
|
|
|
|
|
value: any,
|
|
|
|
|
) => {
|
2025-10-02 05:04:42 +00:00
|
|
|
setBulkFormData((prev: UserLevelsCreateRequest[]) => {
|
|
|
|
|
const newData = [...prev];
|
|
|
|
|
newData[index] = { ...newData[index], [field]: value };
|
|
|
|
|
return newData;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-03 00:53:19 +00:00
|
|
|
const handleTabChange = (value: string) => {
|
|
|
|
|
setActiveTab(value);
|
|
|
|
|
if (value === "bulk") {
|
|
|
|
|
} else {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addBulkItem = () => {
|
|
|
|
|
const newItem: UserLevelsCreateRequest = {
|
|
|
|
|
name: "",
|
|
|
|
|
aliasName: "",
|
|
|
|
|
levelNumber: 1,
|
|
|
|
|
parentLevelId: undefined,
|
|
|
|
|
provinceId: undefined,
|
|
|
|
|
group: "",
|
|
|
|
|
isApprovalActive: true,
|
|
|
|
|
isActive: true,
|
|
|
|
|
};
|
2026-01-22 02:20:17 +00:00
|
|
|
setBulkFormData((prev) => [...prev, newItem]);
|
2025-10-02 05:04:42 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
const renderBulkItemForm = (
|
|
|
|
|
item: UserLevelsCreateRequest,
|
|
|
|
|
index: number,
|
|
|
|
|
onUpdate: (item: UserLevelsCreateRequest) => void,
|
|
|
|
|
onDelete: () => void,
|
|
|
|
|
) => {
|
2025-10-02 05:04:42 +00:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
|
<FormField
|
|
|
|
|
label="Level Name"
|
|
|
|
|
name={`name-${index}`}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., Senior Editor, Manager, Publisher"
|
|
|
|
|
value={item.name}
|
|
|
|
|
onChange={(value) => onUpdate({ ...item, name: value })}
|
|
|
|
|
error={errors[`${index}.name`]}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Alias Name"
|
|
|
|
|
name={`aliasName-${index}`}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., SENIOR_EDITOR, MANAGER, PUBLISHER"
|
|
|
|
|
value={item.aliasName}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
onUpdate({ ...item, aliasName: value.toUpperCase() })
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
error={errors[`${index}.aliasName`]}
|
|
|
|
|
required
|
|
|
|
|
helpText="Short identifier for system use"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Level Number"
|
|
|
|
|
name={`levelNumber-${index}`}
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="e.g., 1, 2, 3"
|
|
|
|
|
value={item.levelNumber}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
onUpdate({ ...item, levelNumber: value ? Number(value) : 1 })
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
error={errors[`${index}.levelNumber`]}
|
|
|
|
|
required
|
|
|
|
|
min={1}
|
|
|
|
|
helpText="Higher number = higher authority"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<FormField
|
|
|
|
|
label="Parent Level"
|
|
|
|
|
name={`parentLevelId-${index}`}
|
|
|
|
|
type="select"
|
2026-01-22 02:20:17 +00:00
|
|
|
placeholder={
|
|
|
|
|
userLevels.length > 0
|
|
|
|
|
? "Select parent level"
|
|
|
|
|
: "No parent levels available"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
value={item.parentLevelId}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
onUpdate({
|
|
|
|
|
...item,
|
|
|
|
|
parentLevelId: value !== undefined ? Number(value) : undefined,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-10-02 06:41:49 +00:00
|
|
|
options={[
|
|
|
|
|
{ value: 0, label: "No Parent (Root Level)" },
|
2026-01-22 02:20:17 +00:00
|
|
|
...(userLevels.length > 0
|
|
|
|
|
? userLevels.map((level) => ({
|
|
|
|
|
value: level.id,
|
|
|
|
|
label: `${level.name} (Level ${level.levelNumber})`,
|
|
|
|
|
}))
|
|
|
|
|
: []),
|
2025-10-02 06:41:49 +00:00
|
|
|
]}
|
2026-01-22 02:20:17 +00:00
|
|
|
helpText={
|
|
|
|
|
userLevels.length === 0
|
|
|
|
|
? "No parent levels found. This will be a root level."
|
|
|
|
|
: "Select parent level for hierarchy"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
disabled={userLevels.length === 0}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Province"
|
|
|
|
|
name={`provinceId-${index}`}
|
|
|
|
|
type="select"
|
2026-01-22 02:20:17 +00:00
|
|
|
placeholder={
|
|
|
|
|
provinces.length > 0
|
|
|
|
|
? "Select province"
|
|
|
|
|
: "No provinces available"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
value={item.provinceId}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
onUpdate({
|
|
|
|
|
...item,
|
|
|
|
|
provinceId: value ? Number(value) : undefined,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
options={
|
|
|
|
|
provinces.length > 0
|
|
|
|
|
? provinces.map((province) => ({
|
|
|
|
|
value: province.id,
|
|
|
|
|
label: province.prov_name,
|
|
|
|
|
}))
|
|
|
|
|
: []
|
|
|
|
|
}
|
|
|
|
|
helpText={
|
|
|
|
|
provinces.length === 0
|
|
|
|
|
? "No provinces found. Please ensure provinces are available in the system."
|
|
|
|
|
: "Geographic scope for this level"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
disabled={provinces.length === 0}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
|
<FormField
|
|
|
|
|
label="Group"
|
|
|
|
|
name={`group-${index}`}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., Editorial, Management, Technical"
|
|
|
|
|
value={item.group || ""}
|
|
|
|
|
onChange={(value) => onUpdate({ ...item, group: value })}
|
|
|
|
|
helpText="Group classification for organization"
|
|
|
|
|
/>
|
|
|
|
|
|
2026-02-13 11:42:59 +00:00
|
|
|
{/* <FormField
|
2025-10-02 05:04:42 +00:00
|
|
|
label="Is Approval Active"
|
|
|
|
|
name={`isApprovalActive-${index}`}
|
|
|
|
|
type="checkbox"
|
|
|
|
|
value={item.isApprovalActive}
|
|
|
|
|
onChange={(value) => onUpdate({ ...item, isApprovalActive: value })}
|
|
|
|
|
helpText="Users with this level can participate in approval process"
|
2026-02-13 11:42:59 +00:00
|
|
|
/> */}
|
2025-10-02 05:04:42 +00:00
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Is Active"
|
|
|
|
|
name={`isActive-${index}`}
|
|
|
|
|
type="checkbox"
|
|
|
|
|
value={item.isActive}
|
|
|
|
|
onChange={(value) => onUpdate({ ...item, isActive: value })}
|
|
|
|
|
helpText="Level is available for assignment"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
2025-10-02 06:41:49 +00:00
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
if (isSubmitting) return; // 🔒 HARD LOCK
|
|
|
|
|
|
|
|
|
|
setIsSubmitting(true);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const isBulkMode = activeTab === "bulk";
|
|
|
|
|
const isEditing = Boolean((initialData as any)?.id);
|
|
|
|
|
const userLevelId = (initialData as any)?.id;
|
|
|
|
|
|
|
|
|
|
/* ===============================
|
|
|
|
|
* BULK MODE
|
|
|
|
|
* =============================== */
|
|
|
|
|
if (isBulkMode) {
|
|
|
|
|
if (!validateBulkForm()) {
|
|
|
|
|
Swal.fire({
|
|
|
|
|
title: "Validation Error",
|
|
|
|
|
text: "Please fix the errors before submitting",
|
|
|
|
|
icon: "error",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let successCount = 0;
|
|
|
|
|
let failedCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const item of bulkFormData) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await createUserLevel(item);
|
|
|
|
|
if (res?.error) {
|
|
|
|
|
failedCount++;
|
|
|
|
|
} else {
|
|
|
|
|
successCount++;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
failedCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Swal.fire({
|
|
|
|
|
title: failedCount === 0 ? "Success" : "Partial Success",
|
|
|
|
|
text:
|
|
|
|
|
failedCount === 0
|
|
|
|
|
? `${successCount} user levels created successfully`
|
|
|
|
|
: `${successCount} success, ${failedCount} failed`,
|
|
|
|
|
icon: failedCount === 0 ? "success" : "warning",
|
|
|
|
|
}).then(() => {
|
|
|
|
|
window.location.reload();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return; // ⛔ STOP HERE (PENTING)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ===============================
|
|
|
|
|
* SINGLE MODE
|
|
|
|
|
* =============================== */
|
|
|
|
|
|
|
|
|
|
const validationErrors = validateForm(formData);
|
2025-10-02 05:04:42 +00:00
|
|
|
if (Object.keys(validationErrors).length > 0) {
|
|
|
|
|
setErrors(validationErrors);
|
|
|
|
|
Swal.fire({
|
|
|
|
|
title: "Validation Error",
|
|
|
|
|
text: "Please fix the errors before submitting",
|
|
|
|
|
icon: "error",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-22 02:20:17 +00:00
|
|
|
|
|
|
|
|
let response;
|
|
|
|
|
if (isEditing) {
|
|
|
|
|
response = await updateUserLevel(userLevelId, formData);
|
|
|
|
|
} else {
|
|
|
|
|
response = await createUserLevel(formData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response?.error) {
|
2025-10-02 05:04:42 +00:00
|
|
|
Swal.fire({
|
2026-01-22 02:20:17 +00:00
|
|
|
title: "Error",
|
|
|
|
|
text: response?.message || "Failed to save user level",
|
2025-10-02 05:04:42 +00:00
|
|
|
icon: "error",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
const createdUserLevelId = response?.data?.data?.id || userLevelId;
|
2026-01-16 03:25:30 +00:00
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
/* ===============================
|
|
|
|
|
* MENU ACCESS
|
|
|
|
|
* =============================== */
|
2026-01-16 03:25:30 +00:00
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
if (createdUserLevelId) {
|
|
|
|
|
if (isEditing) {
|
|
|
|
|
for (const access of userLevelMenuAccesses) {
|
|
|
|
|
await deleteUserLevelMenuAccess(access.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 03:25:30 +00:00
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
if (selectedMenuIds.length > 0) {
|
|
|
|
|
await createUserLevelMenuAccessesBatch({
|
|
|
|
|
userLevelId: createdUserLevelId,
|
|
|
|
|
menuIds: selectedMenuIds,
|
|
|
|
|
});
|
2026-01-18 19:57:16 +00:00
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
for (const menuId of selectedMenuIds) {
|
|
|
|
|
const actionCodes = selectedActionAccesses[menuId] || [];
|
2026-01-18 19:57:16 +00:00
|
|
|
|
|
|
|
|
if (isEditing) {
|
2026-01-22 02:20:17 +00:00
|
|
|
const existing =
|
|
|
|
|
await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(
|
|
|
|
|
createdUserLevelId,
|
|
|
|
|
menuId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (const a of existing?.data?.data || []) {
|
|
|
|
|
await deleteUserLevelMenuActionAccess(a.id);
|
2026-01-16 03:25:30 +00:00
|
|
|
}
|
2026-01-22 02:20:17 +00:00
|
|
|
}
|
2026-01-16 03:25:30 +00:00
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
if (actionCodes.length > 0) {
|
|
|
|
|
await createUserLevelMenuActionAccessesBatch({
|
2026-01-16 03:25:30 +00:00
|
|
|
userLevelId: createdUserLevelId,
|
2026-01-22 02:20:17 +00:00
|
|
|
menuId,
|
|
|
|
|
actionCodes,
|
2026-01-16 03:25:30 +00:00
|
|
|
});
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-22 02:20:17 +00:00
|
|
|
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Success",
|
|
|
|
|
// text: isEditing
|
|
|
|
|
// ? "User level updated successfully"
|
|
|
|
|
// : "User level created successfully",
|
|
|
|
|
// icon: "success",
|
|
|
|
|
// }).then(() => {
|
|
|
|
|
// window.location.reload();
|
|
|
|
|
// });
|
|
|
|
|
Swal.fire({
|
|
|
|
|
icon: "success",
|
|
|
|
|
title: "Sukses",
|
|
|
|
|
text: isEditing
|
|
|
|
|
? "User level updated successfully"
|
|
|
|
|
: "User level created successfully",
|
|
|
|
|
timer: 1500,
|
|
|
|
|
showConfirmButton: false,
|
|
|
|
|
allowOutsideClick: false,
|
|
|
|
|
}).then(() => {
|
|
|
|
|
window.location.reload();
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
2025-10-02 05:04:42 +00:00
|
|
|
Swal.fire({
|
|
|
|
|
title: "Error",
|
2026-01-22 02:20:17 +00:00
|
|
|
text: "Unexpected error occurred",
|
2025-10-02 05:04:42 +00:00
|
|
|
icon: "error",
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
// const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
// e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// // Determine current mode based on active tab
|
|
|
|
|
// const currentMode = activeTab === "bulk" ? "bulk" : "single";
|
|
|
|
|
|
|
|
|
|
// if (currentMode === "single") {
|
|
|
|
|
// const validationErrors = validateForm(formData);
|
|
|
|
|
// console.log("Form mode: ", currentMode);
|
|
|
|
|
// console.log("Error single ", validationErrors);
|
|
|
|
|
|
|
|
|
|
// if (Object.keys(validationErrors).length > 0) {
|
|
|
|
|
// setErrors(validationErrors);
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Validation Error",
|
|
|
|
|
// text: "Please fix the errors before submitting",
|
|
|
|
|
// icon: "error",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
// } else {
|
|
|
|
|
// if (!validateBulkForm()) {
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Validation Error",
|
|
|
|
|
// text: "Please fix the errors before submitting",
|
|
|
|
|
// icon: "error",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// setIsSubmitting(true);
|
|
|
|
|
|
|
|
|
|
// try {
|
|
|
|
|
// if (onSave) {
|
|
|
|
|
// if (currentMode === "single") {
|
|
|
|
|
// // Check if editing or creating
|
|
|
|
|
// const isEditing = !!(initialData as any)?.id;
|
|
|
|
|
// const userLevelId = (initialData as any)?.id;
|
|
|
|
|
|
|
|
|
|
// // Save user level first
|
|
|
|
|
// let userLevelResponse;
|
|
|
|
|
// if (isEditing) {
|
|
|
|
|
// userLevelResponse = await updateUserLevel(userLevelId, formData);
|
|
|
|
|
// } else {
|
|
|
|
|
// userLevelResponse = await createUserLevel(formData);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// if (userLevelResponse?.error) {
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Error",
|
|
|
|
|
// text: userLevelResponse?.message || `Failed to ${isEditing ? 'update' : 'create'} user level`,
|
|
|
|
|
// icon: "error",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// setIsSubmitting(false);
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Get the user level ID
|
|
|
|
|
// const createdUserLevelId = userLevelResponse?.data?.data?.id || userLevelId;
|
|
|
|
|
|
|
|
|
|
// // Save menu accesses
|
|
|
|
|
// if (createdUserLevelId && selectedMenuIds.length > 0) {
|
|
|
|
|
// // Delete existing menu accesses if editing
|
|
|
|
|
// if ((initialData as any)?.id) {
|
|
|
|
|
// for (const access of userLevelMenuAccesses) {
|
|
|
|
|
// await deleteUserLevelMenuAccess(access.id);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Create new menu accesses in batch
|
|
|
|
|
// const menuAccessResponse = await createUserLevelMenuAccessesBatch({
|
|
|
|
|
// userLevelId: createdUserLevelId,
|
|
|
|
|
// menuIds: selectedMenuIds,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// if (menuAccessResponse?.error) {
|
|
|
|
|
// console.error("Error saving menu accesses:", menuAccessResponse?.message);
|
|
|
|
|
// } else {
|
|
|
|
|
// // Save action accesses for each menu
|
|
|
|
|
// for (const menuId of selectedMenuIds) {
|
|
|
|
|
// const actionCodes = selectedActionAccesses[menuId] || [];
|
|
|
|
|
|
|
|
|
|
// // Delete existing action accesses for this menu if editing
|
|
|
|
|
// if ((initialData as any)?.id) {
|
|
|
|
|
// try {
|
|
|
|
|
// const existingActionsRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(createdUserLevelId, menuId);
|
|
|
|
|
// if (!existingActionsRes?.error) {
|
|
|
|
|
// const existingActions = existingActionsRes?.data?.data || [];
|
|
|
|
|
// for (const action of existingActions) {
|
|
|
|
|
// await deleteUserLevelMenuActionAccess(action.id);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// } catch (error) {
|
|
|
|
|
// console.error(`Error deleting existing action accesses for menu ${menuId}:`, error);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Create new action accesses in batch
|
|
|
|
|
// if (actionCodes.length > 0) {
|
|
|
|
|
// const actionAccessResponse = await createUserLevelMenuActionAccessesBatch({
|
|
|
|
|
// userLevelId: createdUserLevelId,
|
|
|
|
|
// menuId: menuId,
|
|
|
|
|
// actionCodes: actionCodes,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// if (actionAccessResponse?.error) {
|
|
|
|
|
// console.error(`Error saving action accesses for menu ${menuId}:`, actionAccessResponse?.message);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// onSave(formData);
|
|
|
|
|
// } else {
|
|
|
|
|
// // For bulk mode, save each item individually
|
|
|
|
|
// let hasErrors = false;
|
|
|
|
|
// let successCount = 0;
|
|
|
|
|
|
|
|
|
|
// for (const item of bulkFormData) {
|
|
|
|
|
// const response = await createUserLevel(item);
|
|
|
|
|
|
|
|
|
|
// if (response?.error) {
|
|
|
|
|
// hasErrors = true;
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Error",
|
|
|
|
|
// text: `Failed to create user level "${item.name}": ${response?.message || "Unknown error"}`,
|
|
|
|
|
// icon: "error",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// } else {
|
|
|
|
|
// successCount++;
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Success",
|
|
|
|
|
// text: `User level "${item.name}" created successfully`,
|
|
|
|
|
// icon: "success",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Refresh page if at least one item was created successfully
|
|
|
|
|
// if (successCount > 0) {
|
|
|
|
|
// setTimeout(() => {
|
|
|
|
|
// window.location.reload();
|
|
|
|
|
// }, 1000); // Small delay to let user see the success message
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// } else {
|
|
|
|
|
// if (currentMode === "single") {
|
|
|
|
|
// // Check if editing or creating
|
|
|
|
|
// const isEditing = !!(initialData as any)?.id;
|
|
|
|
|
// const userLevelId = (initialData as any)?.id;
|
|
|
|
|
|
|
|
|
|
// let response;
|
|
|
|
|
// if (isEditing) {
|
|
|
|
|
// response = await updateUserLevel(userLevelId, formData);
|
|
|
|
|
// } else {
|
|
|
|
|
// response = await createUserLevel(formData);
|
|
|
|
|
// }
|
|
|
|
|
// console.log(`${isEditing ? 'Update' : 'Create'} Response: `, response);
|
|
|
|
|
|
|
|
|
|
// if (response?.error) {
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Error",
|
|
|
|
|
// text: response?.message || `Failed to ${isEditing ? 'update' : 'create'} user level`,
|
|
|
|
|
// icon: "error",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// } else {
|
|
|
|
|
// // Get the user level ID
|
|
|
|
|
// const createdUserLevelId = response?.data?.data?.id || userLevelId;
|
|
|
|
|
|
|
|
|
|
// // Save menu accesses
|
|
|
|
|
// if (createdUserLevelId && selectedMenuIds.length > 0) {
|
|
|
|
|
// // Delete existing menu accesses if editing
|
|
|
|
|
// if ((initialData as any)?.id) {
|
|
|
|
|
// for (const access of userLevelMenuAccesses) {
|
|
|
|
|
// await deleteUserLevelMenuAccess(access.id);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Create new menu accesses in batch
|
|
|
|
|
// const menuAccessResponse = await createUserLevelMenuAccessesBatch({
|
|
|
|
|
// userLevelId: createdUserLevelId,
|
|
|
|
|
// menuIds: selectedMenuIds,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// if (menuAccessResponse?.error) {
|
|
|
|
|
// console.error("Error saving menu accesses:", menuAccessResponse?.message);
|
|
|
|
|
// } else {
|
|
|
|
|
// // Save action accesses for each menu
|
|
|
|
|
// for (const menuId of selectedMenuIds) {
|
|
|
|
|
// const actionCodes = selectedActionAccesses[menuId] || [];
|
|
|
|
|
|
|
|
|
|
// // Delete existing action accesses for this menu if editing
|
|
|
|
|
// if ((initialData as any)?.id) {
|
|
|
|
|
// try {
|
|
|
|
|
// const existingActionsRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(createdUserLevelId, menuId);
|
|
|
|
|
// if (!existingActionsRes?.error) {
|
|
|
|
|
// const existingActions = existingActionsRes?.data?.data || [];
|
|
|
|
|
// for (const action of existingActions) {
|
|
|
|
|
// await deleteUserLevelMenuActionAccess(action.id);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// } catch (error) {
|
|
|
|
|
// console.error(`Error deleting existing action accesses for menu ${menuId}:`, error);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Create new action accesses in batch
|
|
|
|
|
// if (actionCodes.length > 0) {
|
|
|
|
|
// const actionAccessResponse = await createUserLevelMenuActionAccessesBatch({
|
|
|
|
|
// userLevelId: createdUserLevelId,
|
|
|
|
|
// menuId: menuId,
|
|
|
|
|
// actionCodes: actionCodes,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// if (actionAccessResponse?.error) {
|
|
|
|
|
// console.error(`Error saving action accesses for menu ${menuId}:`, actionAccessResponse?.message);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Success",
|
|
|
|
|
// text: isEditing ? "User level updated successfully" : "User level created successfully",
|
|
|
|
|
// icon: "success",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// }).then(() => {
|
|
|
|
|
// // Refresh page after successful save
|
|
|
|
|
// window.location.reload();
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
// } else {
|
|
|
|
|
// // Bulk creation
|
|
|
|
|
// const promises = bulkFormData.map(item => createUserLevel(item));
|
|
|
|
|
// const responses = await Promise.all(promises);
|
|
|
|
|
|
|
|
|
|
// console.log("Create Responses: ", responses);
|
|
|
|
|
|
|
|
|
|
// const failedCount = responses.filter((r: any) => r.error).length;
|
|
|
|
|
// const successCount = responses.length - failedCount;
|
|
|
|
|
|
|
|
|
|
// if (failedCount === 0) {
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Success",
|
|
|
|
|
// text: `All ${successCount} user levels created successfully`,
|
|
|
|
|
// icon: "success",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// } else {
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Partial Success",
|
|
|
|
|
// text: `${successCount} user levels created successfully, ${failedCount} failed`,
|
|
|
|
|
// icon: "warning",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// } catch (error) {
|
|
|
|
|
// console.error("Error submitting form:", error);
|
|
|
|
|
// Swal.fire({
|
|
|
|
|
// title: "Error",
|
|
|
|
|
// text: "An unexpected error occurred",
|
|
|
|
|
// icon: "error",
|
|
|
|
|
// confirmButtonText: "OK",
|
|
|
|
|
// customClass: {
|
|
|
|
|
// popup: 'swal-z-index-9999'
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// } finally {
|
|
|
|
|
// setIsSubmitting(false);
|
|
|
|
|
// }
|
|
|
|
|
// };
|
|
|
|
|
|
2025-10-02 05:04:42 +00:00
|
|
|
const handleReset = () => {
|
|
|
|
|
Swal.fire({
|
|
|
|
|
title: "Reset Form",
|
|
|
|
|
text: "Are you sure you want to reset all form data?",
|
|
|
|
|
icon: "warning",
|
|
|
|
|
showCancelButton: true,
|
|
|
|
|
confirmButtonText: "Yes, reset",
|
2025-10-03 00:53:19 +00:00
|
|
|
cancelButtonText: "Cancel",
|
|
|
|
|
customClass: {
|
2026-01-22 02:20:17 +00:00
|
|
|
popup: "swal-z-index-9999",
|
|
|
|
|
},
|
2025-10-02 05:04:42 +00:00
|
|
|
}).then((result) => {
|
|
|
|
|
if (result.isConfirmed) {
|
|
|
|
|
if (mode === "single") {
|
|
|
|
|
setFormData({
|
|
|
|
|
name: "",
|
|
|
|
|
aliasName: "",
|
|
|
|
|
levelNumber: 1,
|
|
|
|
|
parentLevelId: undefined,
|
|
|
|
|
provinceId: undefined,
|
|
|
|
|
group: "",
|
|
|
|
|
isApprovalActive: true,
|
|
|
|
|
isActive: true,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setBulkFormData([]);
|
|
|
|
|
}
|
|
|
|
|
setErrors({});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderHierarchyTree = () => {
|
|
|
|
|
const buildTree = (levels: UserLevel[], parentId?: number): UserLevel[] => {
|
|
|
|
|
return levels
|
2026-01-22 02:20:17 +00:00
|
|
|
.filter((level) => level.parentLevelId === parentId)
|
|
|
|
|
.map((level) => ({
|
2025-10-02 05:04:42 +00:00
|
|
|
...level,
|
2026-01-22 02:20:17 +00:00
|
|
|
children: buildTree(levels, level.id),
|
2025-10-02 05:04:42 +00:00
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const tree = buildTree(userLevels);
|
|
|
|
|
|
2026-01-22 02:20:17 +00:00
|
|
|
const renderNode = (
|
|
|
|
|
node: UserLevel & { children?: UserLevel[] },
|
|
|
|
|
depth = 0,
|
|
|
|
|
) => (
|
2025-10-02 05:04:42 +00:00
|
|
|
<div key={node.id} className="ml-4">
|
|
|
|
|
<div className="flex items-center gap-2 py-1">
|
|
|
|
|
<div className="w-4 h-4 border-l border-b border-gray-300"></div>
|
|
|
|
|
<span className="text-sm">
|
|
|
|
|
{node.name} (Level {node.levelNumber})
|
2026-01-22 02:20:17 +00:00
|
|
|
{node.group && (
|
|
|
|
|
<span className="text-gray-500 ml-2">- {node.group}</span>
|
|
|
|
|
)}
|
2025-10-02 05:04:42 +00:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{node.children?.map((child: any) => renderNode(child, depth + 1))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-22 02:20:17 +00:00
|
|
|
<div className="space-y-1">{tree.map((node) => renderNode(node))}</div>
|
2025-10-02 05:04:42 +00:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
|
|
|
{isLoadingData && (
|
|
|
|
|
<div className="flex items-center justify-center py-8">
|
|
|
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
|
|
|
<span className="ml-2 text-gray-600">Loading form data...</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-22 02:20:17 +00:00
|
|
|
|
|
|
|
|
<Tabs
|
|
|
|
|
value={activeTab}
|
|
|
|
|
onValueChange={handleTabChange}
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
2026-01-16 03:25:30 +00:00
|
|
|
<TabsList className="grid w-full grid-cols-4">
|
2026-01-22 02:20:17 +00:00
|
|
|
<TabsTrigger value="basic" disabled={isLoadingData}>
|
|
|
|
|
Basic Information
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger
|
|
|
|
|
value="menus"
|
|
|
|
|
disabled={isLoadingData || mode === "bulk"}
|
|
|
|
|
>
|
|
|
|
|
Menu Access
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger
|
|
|
|
|
value="actions"
|
|
|
|
|
disabled={isLoadingData || mode === "bulk"}
|
|
|
|
|
>
|
|
|
|
|
Action Access
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="bulk" disabled={isLoadingData}>
|
|
|
|
|
Bulk Operations
|
|
|
|
|
</TabsTrigger>
|
2025-10-02 05:04:42 +00:00
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
{/* Basic Information Tab */}
|
|
|
|
|
<TabsContent value="basic" className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>User Level Basic Information</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
|
<FormField
|
|
|
|
|
label="Level Name"
|
|
|
|
|
name="name"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., Senior Editor, Manager, Publisher"
|
|
|
|
|
value={formData.name}
|
|
|
|
|
onChange={(value) => handleFieldChange("name", value)}
|
|
|
|
|
error={errors.name}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Alias Name"
|
|
|
|
|
name="aliasName"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., SENIOR_EDITOR, MANAGER, PUBLISHER"
|
|
|
|
|
value={formData.aliasName}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
handleFieldChange("aliasName", value.toUpperCase())
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
error={errors.aliasName}
|
|
|
|
|
required
|
|
|
|
|
helpText="Short identifier for system use"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Level Number"
|
|
|
|
|
name="levelNumber"
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="e.g., 1, 2, 3"
|
|
|
|
|
value={formData.levelNumber}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
handleFieldChange("levelNumber", value ? Number(value) : 1)
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
error={errors.levelNumber}
|
|
|
|
|
required
|
|
|
|
|
min={1}
|
|
|
|
|
helpText="Higher number = higher authority"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<FormField
|
|
|
|
|
label="Parent Level"
|
|
|
|
|
name="parentLevelId"
|
|
|
|
|
type="select"
|
2026-01-22 02:20:17 +00:00
|
|
|
placeholder={
|
|
|
|
|
userLevels.length > 0
|
|
|
|
|
? "Select parent level"
|
|
|
|
|
: "No parent levels available"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
value={formData.parentLevelId}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
handleFieldChange(
|
|
|
|
|
"parentLevelId",
|
|
|
|
|
value !== undefined ? Number(value) : undefined,
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-10-02 06:41:49 +00:00
|
|
|
options={[
|
|
|
|
|
{ value: 0, label: "No Parent (Root Level)" },
|
2026-01-22 02:20:17 +00:00
|
|
|
...(userLevels.length > 0
|
|
|
|
|
? userLevels.map((level) => ({
|
|
|
|
|
value: level.id,
|
|
|
|
|
label: `${level.name} (Level ${level.levelNumber})`,
|
|
|
|
|
}))
|
|
|
|
|
: []),
|
2025-10-02 06:41:49 +00:00
|
|
|
]}
|
2026-01-22 02:20:17 +00:00
|
|
|
helpText={
|
|
|
|
|
userLevels.length === 0
|
|
|
|
|
? "No parent levels found. This will be a root level."
|
|
|
|
|
: "Select parent level for hierarchy"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
disabled={userLevels.length === 0}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Province"
|
|
|
|
|
name="provinceId"
|
|
|
|
|
type="select"
|
2026-01-22 02:20:17 +00:00
|
|
|
placeholder={
|
|
|
|
|
provinces.length > 0
|
|
|
|
|
? "Select province"
|
|
|
|
|
: "No provinces available"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
value={formData.provinceId}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
handleFieldChange(
|
|
|
|
|
"provinceId",
|
|
|
|
|
value ? Number(value) : undefined,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
options={
|
|
|
|
|
provinces.length > 0
|
|
|
|
|
? provinces.map((province) => ({
|
|
|
|
|
value: province.id,
|
|
|
|
|
label: province.prov_name,
|
|
|
|
|
}))
|
|
|
|
|
: []
|
|
|
|
|
}
|
|
|
|
|
helpText={
|
|
|
|
|
provinces.length === 0
|
|
|
|
|
? "No provinces found. Please ensure provinces are available in the system."
|
|
|
|
|
: "Geographic scope for this level"
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
disabled={provinces.length === 0}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Group"
|
|
|
|
|
name="group"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., Editorial, Management, Technical"
|
|
|
|
|
value={formData.group || ""}
|
|
|
|
|
onChange={(value) => handleFieldChange("group", value)}
|
|
|
|
|
helpText="Group classification for organization"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
2026-02-13 11:42:59 +00:00
|
|
|
{/* <FormField
|
2025-10-02 05:04:42 +00:00
|
|
|
label="Is Approval Active"
|
|
|
|
|
name="isApprovalActive"
|
|
|
|
|
type="checkbox"
|
|
|
|
|
value={formData.isApprovalActive}
|
2026-01-22 02:20:17 +00:00
|
|
|
onChange={(value) =>
|
|
|
|
|
handleFieldChange("isApprovalActive", value)
|
|
|
|
|
}
|
2025-10-02 05:04:42 +00:00
|
|
|
helpText="Users with this level can participate in approval process"
|
2026-02-13 11:42:59 +00:00
|
|
|
/> */}
|
2025-10-02 05:04:42 +00:00
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="Is Active"
|
|
|
|
|
name="isActive"
|
|
|
|
|
type="checkbox"
|
|
|
|
|
value={formData.isActive}
|
|
|
|
|
onChange={(value) => handleFieldChange("isActive", value)}
|
|
|
|
|
helpText="Level is available for assignment"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* Hierarchy Tab */}
|
2025-10-02 06:41:49 +00:00
|
|
|
{/* <TabsContent value="hierarchy" className="space-y-6">
|
2025-10-02 05:04:42 +00:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<HierarchyIcon className="h-5 w-5" />
|
|
|
|
|
Level Hierarchy Visualization
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<Collapsible>
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
<Button variant="ghost" className="flex items-center gap-2">
|
|
|
|
|
{expandedHierarchy ? "Hide" : "Show"} Hierarchy Tree
|
|
|
|
|
{expandedHierarchy ? <ChevronUpIcon className="h-4 w-4" /> : <ChevronDownIcon className="h-4 w-4" />}
|
|
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent className="mt-4">
|
|
|
|
|
{userLevels.length > 0 ? (
|
|
|
|
|
<div className="border rounded-lg p-4 bg-gray-50">
|
|
|
|
|
{renderHierarchyTree()}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-gray-500 text-center py-8">No user levels found</p>
|
|
|
|
|
)}
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2025-10-02 06:41:49 +00:00
|
|
|
</TabsContent> */}
|
2025-10-02 05:04:42 +00:00
|
|
|
|
2026-01-18 19:57:16 +00:00
|
|
|
{/* Menu Access Tab */}
|
|
|
|
|
<TabsContent value="menus" className="space-y-6">
|
2026-01-16 03:25:30 +00:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
2026-01-18 19:57:16 +00:00
|
|
|
<CardTitle>Menu Access Configuration</CardTitle>
|
2026-01-16 03:25:30 +00:00
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<p className="text-sm text-gray-600">
|
2026-01-22 02:20:17 +00:00
|
|
|
Select which menus this user level can access. Users with this
|
|
|
|
|
level will only see selected menus in the navigation.
|
2026-01-16 03:25:30 +00:00
|
|
|
</p>
|
2026-01-18 19:57:16 +00:00
|
|
|
{menus.length > 0 ? (
|
2026-01-16 03:25:30 +00:00
|
|
|
<div className="space-y-2 max-h-96 overflow-y-auto border rounded-lg p-4">
|
2026-01-18 19:57:16 +00:00
|
|
|
{menus.map((menu) => (
|
2026-01-16 03:25:30 +00:00
|
|
|
<label
|
2026-01-18 19:57:16 +00:00
|
|
|
key={menu.id}
|
2026-02-02 04:31:13 +00:00
|
|
|
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 dark:hover:bg-black cursor-pointer"
|
2026-01-16 03:25:30 +00:00
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
2026-01-18 19:57:16 +00:00
|
|
|
checked={selectedMenuIds.includes(menu.id)}
|
2026-01-16 03:25:30 +00:00
|
|
|
onChange={() => {
|
2026-01-18 19:57:16 +00:00
|
|
|
setSelectedMenuIds((prev) => {
|
|
|
|
|
const newMenuIds = prev.includes(menu.id)
|
|
|
|
|
? prev.filter((id) => id !== menu.id)
|
|
|
|
|
: [...prev, menu.id];
|
2026-01-22 02:20:17 +00:00
|
|
|
|
2026-01-18 19:57:16 +00:00
|
|
|
// If menu is deselected, remove its action accesses
|
2026-01-22 02:20:17 +00:00
|
|
|
if (
|
|
|
|
|
prev.includes(menu.id) &&
|
|
|
|
|
!newMenuIds.includes(menu.id)
|
|
|
|
|
) {
|
2026-01-18 19:57:16 +00:00
|
|
|
setSelectedActionAccesses((prevActions) => {
|
|
|
|
|
const newActions = { ...prevActions };
|
|
|
|
|
delete newActions[menu.id];
|
|
|
|
|
return newActions;
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-22 02:20:17 +00:00
|
|
|
|
2026-01-18 19:57:16 +00:00
|
|
|
return newMenuIds;
|
|
|
|
|
});
|
2026-01-16 03:25:30 +00:00
|
|
|
}}
|
|
|
|
|
className="mt-1"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex-1">
|
2026-01-18 19:57:16 +00:00
|
|
|
<div className="font-medium">{menu.name}</div>
|
2026-01-22 02:20:17 +00:00
|
|
|
<div className="text-sm text-gray-500">
|
|
|
|
|
{menu.description}
|
|
|
|
|
</div>
|
2026-01-16 03:25:30 +00:00
|
|
|
<div className="text-xs text-gray-400 mt-1">
|
2026-01-18 19:57:16 +00:00
|
|
|
Group: {menu.group} • Module ID: {menu.moduleId}
|
2026-01-16 03:25:30 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-center py-8 text-gray-500">
|
2026-01-18 19:57:16 +00:00
|
|
|
No menus available
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* Action Access Tab */}
|
|
|
|
|
<TabsContent value="actions" className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Action Access Configuration</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<p className="text-sm text-gray-600">
|
2026-01-22 02:20:17 +00:00
|
|
|
Configure which actions users with this level can perform in
|
|
|
|
|
each menu. First select menus in the "Menu Access" tab.
|
2026-01-18 19:57:16 +00:00
|
|
|
</p>
|
|
|
|
|
{selectedMenuIds.length > 0 ? (
|
|
|
|
|
<div className="space-y-4 max-h-96 overflow-y-auto">
|
|
|
|
|
{selectedMenuIds.map((menuId) => {
|
|
|
|
|
const menu = menus.find((m) => m.id === menuId);
|
|
|
|
|
const actions = menuActionsMap[menuId] || [];
|
2026-01-22 02:20:17 +00:00
|
|
|
const selectedActions =
|
|
|
|
|
selectedActionAccesses[menuId] || [];
|
2026-01-18 19:57:16 +00:00
|
|
|
|
|
|
|
|
if (!menu) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-22 02:20:17 +00:00
|
|
|
<Card
|
|
|
|
|
key={menuId}
|
|
|
|
|
className="border-l-4 border-l-blue-500"
|
|
|
|
|
>
|
2026-01-18 19:57:16 +00:00
|
|
|
<CardHeader className="pb-3">
|
2026-01-22 02:20:17 +00:00
|
|
|
<CardTitle className="text-base">
|
|
|
|
|
{menu.name}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
{menu.description}
|
|
|
|
|
</p>
|
2026-01-18 19:57:16 +00:00
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{actions.length > 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{actions.map((action) => (
|
|
|
|
|
<label
|
|
|
|
|
key={action.id}
|
2026-02-02 04:31:13 +00:00
|
|
|
className="flex items-start gap-3 p-2 border rounded-lg hover:bg-gray-50 dark:hover:bg-black cursor-pointer"
|
2026-01-18 19:57:16 +00:00
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
2026-01-22 02:20:17 +00:00
|
|
|
checked={selectedActions.includes(
|
|
|
|
|
action.actionCode,
|
|
|
|
|
)}
|
2026-01-18 19:57:16 +00:00
|
|
|
onChange={() => {
|
|
|
|
|
setSelectedActionAccesses((prev) => {
|
|
|
|
|
const current = prev[menuId] || [];
|
2026-01-22 02:20:17 +00:00
|
|
|
const newActions = current.includes(
|
|
|
|
|
action.actionCode,
|
|
|
|
|
)
|
|
|
|
|
? current.filter(
|
|
|
|
|
(code) =>
|
|
|
|
|
code !== action.actionCode,
|
|
|
|
|
)
|
2026-01-18 19:57:16 +00:00
|
|
|
: [...current, action.actionCode];
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
[menuId]: newActions,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
className="mt-1"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex-1">
|
2026-01-22 02:20:17 +00:00
|
|
|
<div className="font-medium text-sm">
|
|
|
|
|
{action.actionName}
|
|
|
|
|
</div>
|
2026-01-18 19:57:16 +00:00
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
Code: {action.actionCode}
|
2026-01-22 02:20:17 +00:00
|
|
|
{action.pathUrl &&
|
|
|
|
|
` • Path: ${action.pathUrl}`}
|
2026-01-18 19:57:16 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-center py-4 text-gray-500 text-sm">
|
|
|
|
|
No actions available for this menu
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-center py-8 text-gray-500">
|
|
|
|
|
<p className="mb-2">No menus selected</p>
|
2026-01-22 02:20:17 +00:00
|
|
|
<p className="text-sm">
|
|
|
|
|
Please select menus in the "Menu Access" tab first
|
|
|
|
|
</p>
|
2026-01-16 03:25:30 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
2025-10-02 05:04:42 +00:00
|
|
|
{/* Bulk Operations Tab */}
|
|
|
|
|
<TabsContent value="bulk" className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<UsersIcon className="h-5 w-5" />
|
|
|
|
|
Bulk User Level Creation
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
2025-10-03 00:53:19 +00:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h3 className="text-lg font-medium">User Levels</h3>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={addBulkItem}
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<PlusIcon className="h-4 w-4" />
|
|
|
|
|
Add User Level
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{bulkFormData.length === 0 ? (
|
|
|
|
|
<div className="text-center py-8 text-gray-500">
|
|
|
|
|
<UsersIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
2026-01-22 02:20:17 +00:00
|
|
|
<p>
|
|
|
|
|
No user levels added yet. Add multiple levels to create
|
|
|
|
|
them in bulk.
|
|
|
|
|
</p>
|
2025-10-03 00:53:19 +00:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{bulkFormData.map((item, index) => (
|
2026-01-22 02:20:17 +00:00
|
|
|
<Card
|
|
|
|
|
key={index}
|
|
|
|
|
className="border-l-4 border-l-blue-500"
|
|
|
|
|
>
|
2025-10-03 00:53:19 +00:00
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<CardTitle className="text-base">
|
|
|
|
|
User Level #{index + 1}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
2026-01-22 02:20:17 +00:00
|
|
|
const newItems = bulkFormData.filter(
|
|
|
|
|
(_, i) => i !== index,
|
|
|
|
|
);
|
2025-10-03 00:53:19 +00:00
|
|
|
setBulkFormData(newItems);
|
|
|
|
|
}}
|
|
|
|
|
className="text-red-600 hover:text-red-700"
|
|
|
|
|
>
|
|
|
|
|
<TrashIcon className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{renderBulkItemForm(
|
|
|
|
|
item,
|
|
|
|
|
index,
|
|
|
|
|
(updatedItem) => {
|
|
|
|
|
const newItems = [...bulkFormData];
|
|
|
|
|
newItems[index] = updatedItem;
|
|
|
|
|
setBulkFormData(newItems);
|
|
|
|
|
},
|
|
|
|
|
() => {
|
2026-01-22 02:20:17 +00:00
|
|
|
const newItems = bulkFormData.filter(
|
|
|
|
|
(_, i) => i !== index,
|
|
|
|
|
);
|
2025-10-03 00:53:19 +00:00
|
|
|
setBulkFormData(newItems);
|
2026-01-22 02:20:17 +00:00
|
|
|
},
|
2025-10-03 00:53:19 +00:00
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-02 05:04:42 +00:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
{/* Form Actions */}
|
|
|
|
|
<div className="flex items-center justify-between pt-6 border-t">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
>
|
|
|
|
|
<RotateCcwIcon className="h-4 w-4 mr-2" />
|
|
|
|
|
Reset
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{onCancel && (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={onCancel}
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-01-22 02:20:17 +00:00
|
|
|
|
2026-01-23 11:12:56 +00:00
|
|
|
<Button variant="outline" type="submit" disabled={isSubmitting}>
|
2026-01-22 02:20:17 +00:00
|
|
|
{isSubmitting ? "Saving..." : "Save User Level"}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* <Button
|
2025-10-02 05:04:42 +00:00
|
|
|
type="submit"
|
|
|
|
|
disabled={isSubmitting || isLoading || isLoadingData}
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<SaveIcon className="h-4 w-4" />
|
2026-01-22 02:20:17 +00:00
|
|
|
{isSubmitting
|
|
|
|
|
? "Saving..."
|
|
|
|
|
: isLoadingData
|
|
|
|
|
? "Loading..."
|
|
|
|
|
: `Save ${activeTab === "bulk" ? "User Levels" : "User Level"}`}
|
|
|
|
|
</Button> */}
|
2025-10-02 05:04:42 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
);
|
|
|
|
|
};
|