kontenhumas-fe/components/form/UserLevelsForm.tsx

788 lines
27 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,
getUserLevels,
getProvinces,
} from "@/service/approval-workflows";
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,
});
// Bulk form state
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]);
// API data
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
const [provinces, setProvinces] = useState<Province[]>([]);
// UI state
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");
// Load initial data
useEffect(() => {
if (initialData) {
setFormData(initialData);
}
}, [initialData]);
// Load API data
useEffect(() => {
const loadData = async () => {
try {
const [userLevelsRes, provincesRes] = await Promise.all([
getUserLevels(),
getProvinces(),
]);
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data?.data || []);
if (!provincesRes?.error) setProvinces(provincesRes?.data?.data || []);
} catch (error) {
console.error("Error loading form data:", error);
} finally {
setIsLoadingData(false);
}
};
loadData();
}, []);
// Validation
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;
};
// Form handlers
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);
// Update mode based on active tab
if (value === "bulk") {
// Mode will be determined by activeTab in handleSubmit
} else {
// Mode will be determined by activeTab in handleSubmit
}
};
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();
// 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") {
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") {
const response = await createUserLevel(formData);
console.log("Create Response: ", response);
if (response?.error) {
Swal.fire({
title: "Error",
text: response?.message || "Failed to create user level",
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
} else {
Swal.fire({
title: "Success",
text: "User level created successfully",
icon: "success",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
}).then(() => {
// Refresh page after successful creation
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-3">
<TabsTrigger value="basic" disabled={isLoadingData}>Basic Information</TabsTrigger>
{/* <TabsTrigger value="hierarchy" disabled={isLoadingData}>Hierarchy</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> */}
{/* 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
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>
);
};