kontenhumas-fe/components/form/UserLevelsForm.tsx

1510 lines
52 KiB
TypeScript

"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";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
ChevronDownIcon,
ChevronUpIcon,
SaveIcon,
EyeIcon,
RotateCcwIcon,
UsersIcon,
HierarchyIcon,
PlusIcon,
TrashIcon,
} from "@/components/icons";
import { FormField } from "./common/FormField";
import { DynamicArray } from "./common/DynamicArray";
import {
UserLevelsCreateRequest,
UserLevel,
Province,
createUserLevel,
updateUserLevel,
getUserLevels,
getProvinces,
} from "@/service/approval-workflows";
import { MasterMenu, getMasterMenus } from "@/service/menu-modules";
import {
getUserLevelMenuAccessesByUserLevelId,
createUserLevelMenuAccessesBatch,
deleteUserLevelMenuAccess,
UserLevelMenuAccess,
} from "@/service/user-level-menu-accesses";
import {
getUserLevelMenuActionAccessesByUserLevelIdAndMenuId,
createUserLevelMenuActionAccessesBatch,
deleteUserLevelMenuActionAccess,
UserLevelMenuActionAccess,
} from "@/service/user-level-menu-action-accesses";
import { getMenuActionsByMenuId, MenuAction } from "@/service/menu-actions";
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,
});
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>(
[],
);
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
const [provinces, setProvinces] = useState<Province[]>([]);
const [menus, setMenus] = useState<MasterMenu[]>([]);
const [selectedMenuIds, setSelectedMenuIds] = useState<number[]>([]);
const [userLevelMenuAccesses, setUserLevelMenuAccesses] = useState<
UserLevelMenuAccess[]
>([]);
const [menuActionsMap, setMenuActionsMap] = useState<
Record<number, MenuAction[]>
>({});
const [selectedActionAccesses, setSelectedActionAccesses] = useState<
Record<number, string[]>
>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true);
const [activeTab, setActiveTab] = useState(
mode === "single" ? "basic" : "bulk",
);
useEffect(() => {
if (initialData) {
setFormData(initialData);
}
}, [initialData]);
useEffect(() => {
const loadData = async () => {
try {
const [userLevelsRes, provincesRes, menusRes] = await Promise.all([
getUserLevels(),
getProvinces(),
getMasterMenus({ limit: 100 }),
]);
if (!userLevelsRes?.error)
setUserLevels(userLevelsRes?.data?.data || []);
if (!provincesRes?.error) setProvinces(provincesRes?.data?.data || []);
if (!menusRes?.error) {
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);
// 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) {
console.error(
`Error loading actions for menu ${menu.id}:`,
error,
);
}
}
setMenuActionsMap(actionsMap);
}
} catch (error) {
console.error("Error loading form data:", error);
} finally {
setIsLoadingData(false);
}
};
loadData();
}, []);
useEffect(() => {
const loadAccesses = async () => {
if (initialData && (initialData as any).id) {
const userLevelId = (initialData as any).id;
try {
// Load menu accesses
const menuRes =
await getUserLevelMenuAccessesByUserLevelId(userLevelId);
if (!menuRes?.error) {
const menuAccesses = menuRes?.data?.data || [];
setUserLevelMenuAccesses(menuAccesses);
setSelectedMenuIds(
menuAccesses
.filter((a: UserLevelMenuAccess) => a.canAccess)
.map((a: UserLevelMenuAccess) => a.menuId),
);
// Load action accesses for each menu
const actionAccesses: Record<number, string[]> = {};
for (const menuAccess of menuAccesses.filter(
(a: UserLevelMenuAccess) => a.canAccess,
)) {
try {
const actionRes =
await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(
userLevelId,
menuAccess.menuId,
);
if (!actionRes?.error) {
const actions = actionRes?.data?.data || [];
actionAccesses[menuAccess.menuId] = actions
.filter((a: UserLevelMenuActionAccess) => a.canAccess)
.map((a: UserLevelMenuActionAccess) => a.actionCode);
}
} catch (error) {
console.error(
`Error loading action accesses for menu ${menuAccess.menuId}:`,
error,
);
}
}
setSelectedActionAccesses(actionAccesses);
}
} catch (error) {
console.error("Error loading accesses:", error);
}
}
};
loadAccesses();
}, [initialData]);
const validateForm = (
data: UserLevelsCreateRequest,
): Record<string, string> => {
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
// 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";
// }
return newErrors;
};
const validateBulkForm = (): boolean => {
let isValid = true;
const newErrors: Record<string, string> = {};
bulkFormData.forEach((data, index) => {
const itemErrors = validateForm(data);
Object.keys(itemErrors).forEach((key) => {
newErrors[`${index}.${key}`] = itemErrors[key];
});
if (Object.keys(itemErrors).length > 0) {
isValid = false;
}
});
setErrors(newErrors);
return isValid;
};
const handleFieldChange = (
field: keyof UserLevelsCreateRequest,
value: any,
) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
}
};
const handleBulkFieldChange = (
index: number,
field: keyof UserLevelsCreateRequest,
value: any,
) => {
setBulkFormData((prev: UserLevelsCreateRequest[]) => {
const newData = [...prev];
newData[index] = { ...newData[index], [field]: value };
return newData;
});
};
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,
};
setBulkFormData((prev) => [...prev, newItem]);
};
const renderBulkItemForm = (
item: UserLevelsCreateRequest,
index: number,
onUpdate: (item: UserLevelsCreateRequest) => void,
onDelete: () => void,
) => {
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}
onChange={(value) =>
onUpdate({ ...item, aliasName: value.toUpperCase() })
}
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}
onChange={(value) =>
onUpdate({ ...item, levelNumber: value ? Number(value) : 1 })
}
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"
placeholder={
userLevels.length > 0
? "Select parent level"
: "No parent levels available"
}
value={item.parentLevelId}
onChange={(value) =>
onUpdate({
...item,
parentLevelId: value !== undefined ? Number(value) : undefined,
})
}
options={[
{ value: 0, label: "No Parent (Root Level)" },
...(userLevels.length > 0
? userLevels.map((level) => ({
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
}))
: []),
]}
helpText={
userLevels.length === 0
? "No parent levels found. This will be a root level."
: "Select parent level for hierarchy"
}
disabled={userLevels.length === 0}
/>
<FormField
label="Province"
name={`provinceId-${index}`}
type="select"
placeholder={
provinces.length > 0
? "Select province"
: "No provinces available"
}
value={item.provinceId}
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"
}
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"
/>
{/* <FormField
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"
/> */}
<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();
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);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
Swal.fire({
title: "Validation Error",
text: "Please fix the errors before submitting",
icon: "error",
});
return;
}
let response;
if (isEditing) {
response = await updateUserLevel(userLevelId, formData);
} else {
response = await createUserLevel(formData);
}
if (response?.error) {
Swal.fire({
title: "Error",
text: response?.message || "Failed to save user level",
icon: "error",
});
return;
}
const createdUserLevelId = response?.data?.data?.id || userLevelId;
/* ===============================
* MENU ACCESS
* =============================== */
if (createdUserLevelId) {
if (isEditing) {
for (const access of userLevelMenuAccesses) {
await deleteUserLevelMenuAccess(access.id);
}
}
if (selectedMenuIds.length > 0) {
await createUserLevelMenuAccessesBatch({
userLevelId: createdUserLevelId,
menuIds: selectedMenuIds,
});
for (const menuId of selectedMenuIds) {
const actionCodes = selectedActionAccesses[menuId] || [];
if (isEditing) {
const existing =
await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(
createdUserLevelId,
menuId,
);
for (const a of existing?.data?.data || []) {
await deleteUserLevelMenuActionAccess(a.id);
}
}
if (actionCodes.length > 0) {
await createUserLevelMenuActionAccessesBatch({
userLevelId: createdUserLevelId,
menuId,
actionCodes,
});
}
}
}
}
// 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);
Swal.fire({
title: "Error",
text: "Unexpected error occurred",
icon: "error",
});
} finally {
setIsSubmitting(false);
}
};
// 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);
// }
// };
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",
cancelButtonText: "Cancel",
customClass: {
popup: "swal-z-index-9999",
},
}).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
.filter((level) => level.parentLevelId === parentId)
.map((level) => ({
...level,
children: buildTree(levels, level.id),
}));
};
const tree = buildTree(userLevels);
const renderNode = (
node: UserLevel & { children?: UserLevel[] },
depth = 0,
) => (
<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})
{node.group && (
<span className="text-gray-500 ml-2">- {node.group}</span>
)}
</span>
</div>
{node.children?.map((child: any) => renderNode(child, depth + 1))}
</div>
);
return (
<div className="space-y-1">{tree.map((node) => renderNode(node))}</div>
);
};
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>
)}
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="w-full"
>
<TabsList className="grid w-full grid-cols-4">
<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>
</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}
onChange={(value) =>
handleFieldChange("aliasName", value.toUpperCase())
}
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}
onChange={(value) =>
handleFieldChange("levelNumber", value ? Number(value) : 1)
}
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"
placeholder={
userLevels.length > 0
? "Select parent level"
: "No parent levels available"
}
value={formData.parentLevelId}
onChange={(value) =>
handleFieldChange(
"parentLevelId",
value !== undefined ? Number(value) : undefined,
)
}
options={[
{ value: 0, label: "No Parent (Root Level)" },
...(userLevels.length > 0
? userLevels.map((level) => ({
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
}))
: []),
]}
helpText={
userLevels.length === 0
? "No parent levels found. This will be a root level."
: "Select parent level for hierarchy"
}
disabled={userLevels.length === 0}
/>
<FormField
label="Province"
name="provinceId"
type="select"
placeholder={
provinces.length > 0
? "Select province"
: "No provinces available"
}
value={formData.provinceId}
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"
}
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">
{/* <FormField
label="Is Approval Active"
name="isApprovalActive"
type="checkbox"
value={formData.isApprovalActive}
onChange={(value) =>
handleFieldChange("isApprovalActive", value)
}
helpText="Users with this level can participate in approval process"
/> */}
<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 */}
{/* <TabsContent value="hierarchy" className="space-y-6">
<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>
</TabsContent> */}
{/* Menu Access Tab */}
<TabsContent value="menus" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Menu Access Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600">
Select which menus this user level can access. Users with this
level will only see selected menus in the navigation.
</p>
{menus.length > 0 ? (
<div className="space-y-2 max-h-96 overflow-y-auto border rounded-lg p-4">
{menus.map((menu) => (
<label
key={menu.id}
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 dark:hover:bg-black cursor-pointer"
>
<input
type="checkbox"
checked={selectedMenuIds.includes(menu.id)}
onChange={() => {
setSelectedMenuIds((prev) => {
const newMenuIds = prev.includes(menu.id)
? prev.filter((id) => id !== menu.id)
: [...prev, menu.id];
// If menu is deselected, remove its action accesses
if (
prev.includes(menu.id) &&
!newMenuIds.includes(menu.id)
) {
setSelectedActionAccesses((prevActions) => {
const newActions = { ...prevActions };
delete newActions[menu.id];
return newActions;
});
}
return newMenuIds;
});
}}
className="mt-1"
/>
<div className="flex-1">
<div className="font-medium">{menu.name}</div>
<div className="text-sm text-gray-500">
{menu.description}
</div>
<div className="text-xs text-gray-400 mt-1">
Group: {menu.group} Module ID: {menu.moduleId}
</div>
</div>
</label>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
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">
Configure which actions users with this level can perform in
each menu. First select menus in the "Menu Access" tab.
</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] || [];
const selectedActions =
selectedActionAccesses[menuId] || [];
if (!menu) return null;
return (
<Card
key={menuId}
className="border-l-4 border-l-blue-500"
>
<CardHeader className="pb-3">
<CardTitle className="text-base">
{menu.name}
</CardTitle>
<p className="text-sm text-gray-500">
{menu.description}
</p>
</CardHeader>
<CardContent>
{actions.length > 0 ? (
<div className="space-y-2">
{actions.map((action) => (
<label
key={action.id}
className="flex items-start gap-3 p-2 border rounded-lg hover:bg-gray-50 dark:hover:bg-black cursor-pointer"
>
<input
type="checkbox"
checked={selectedActions.includes(
action.actionCode,
)}
onChange={() => {
setSelectedActionAccesses((prev) => {
const current = prev[menuId] || [];
const newActions = current.includes(
action.actionCode,
)
? current.filter(
(code) =>
code !== action.actionCode,
)
: [...current, action.actionCode];
return {
...prev,
[menuId]: newActions,
};
});
}}
className="mt-1"
/>
<div className="flex-1">
<div className="font-medium text-sm">
{action.actionName}
</div>
<div className="text-xs text-gray-500">
Code: {action.actionCode}
{action.pathUrl &&
` • Path: ${action.pathUrl}`}
</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>
<p className="text-sm">
Please select menus in the "Menu Access" tab first
</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* 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>
<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" />
<p>
No user levels added yet. Add multiple levels to create
them in bulk.
</p>
</div>
) : (
<div className="space-y-4">
{bulkFormData.map((item, index) => (
<Card
key={index}
className="border-l-4 border-l-blue-500"
>
<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={() => {
const newItems = bulkFormData.filter(
(_, i) => i !== index,
);
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);
},
() => {
const newItems = bulkFormData.filter(
(_, i) => i !== index,
);
setBulkFormData(newItems);
},
)}
</CardContent>
</Card>
))}
</div>
)}
</div>
</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>
)}
<Button variant="outline" type="submit" disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save User Level"}
</Button>
{/* <Button
type="submit"
disabled={isSubmitting || isLoading || isLoadingData}
className="flex items-center gap-2"
>
<SaveIcon className="h-4 w-4" />
{isSubmitting
? "Saving..."
: isLoadingData
? "Loading..."
: `Save ${activeTab === "bulk" ? "User Levels" : "User Level"}`}
</Button> */}
</div>
</div>
</form>
);
};