Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from "react"; | |
| import { useNavigate, useParams, useLocation } from "react-router-dom"; | |
| import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | |
| import { createRoot } from "react-dom/client"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Dropdown, DropdownOption } from "@/components/ui/dropdown"; | |
| import { toast } from "@/lib/custom-toast"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { ChevronLeft, Loader2, ArrowLeft, LockIcon, InfoIcon } from "lucide-react"; | |
| import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; | |
| import { employeeService, roleService } from "@/lib/api"; | |
| import { Employee, EmployeeRegistrationRequest, Role } from "@/types"; | |
| import Header from "@/components/layout/header"; | |
| import Sidebar from "@/components/layout/sidebar"; | |
| import { useIsMobile } from "@/hooks/use-mobile"; | |
| import { Skeleton } from "@/components/ui/skeleton"; | |
| import { useAuth } from "@/lib/auth-context"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "@/components/ui/select"; | |
| // Function to show toast with longer duration | |
| const showLongToast = (message: string, type: 'success' | 'error' = 'success') => { | |
| if (type === 'success') { | |
| toast.success(message, { duration: 10000 }); | |
| } else { | |
| toast.error(message, { duration: 10000 }); | |
| } | |
| }; | |
| const EmployeeEdit = () => { | |
| const { id } = useParams<{ id: string }>(); | |
| const navigate = useNavigate(); | |
| const location = useLocation(); | |
| const queryClient = useQueryClient(); | |
| const isMobile = useIsMobile(); | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(!isMobile); | |
| const { userData } = useAuth(); | |
| // Close sidebar on mobile when route changes | |
| useEffect(() => { | |
| if (isMobile) { | |
| setIsSidebarOpen(false); | |
| } | |
| }, [location, isMobile]); | |
| // Update sidebar state when screen size changes | |
| useEffect(() => { | |
| setIsSidebarOpen(!isMobile); | |
| }, [isMobile]); | |
| // Check if user has HR or Admin permissions | |
| const hasHrAdminPermission = ['HR', 'Admin', 'Manager', 'Super Admin', 'HR Manager', 'Sr. HR Manager'].includes(userData?.roleName || ''); | |
| console.log('userData:', userData); | |
| console.log('roleName:', userData?.roleName); | |
| console.log('hasHrAdminPermission:', hasHrAdminPermission); | |
| // Check if editing own profile | |
| const [isOwnProfile, setIsOwnProfile] = useState(false); | |
| // Employee form state | |
| const [formData, setFormData] = useState<EmployeeRegistrationRequest>({ | |
| id: 0, | |
| empCode: "", | |
| firstName: "", | |
| middleName: "", | |
| lastName: "", | |
| email: "", | |
| mobile: "", | |
| type: "", | |
| academics: "", | |
| financialData: "", | |
| description: "", | |
| passwordHash: "", | |
| roleId: 1 | |
| }); | |
| // Store the original restricted field values | |
| const [originalValues, setOriginalValues] = useState({ | |
| email: "", | |
| empCode: "", | |
| type: "", | |
| roleId: 0 | |
| }); | |
| // Fetch employee details | |
| const { data: employee, isLoading, error } = useQuery({ | |
| queryKey: ["employee", id], | |
| queryFn: () => employeeService.getById(Number(id)), | |
| enabled: !!id, | |
| staleTime: 300000 // 5 minutes | |
| }); | |
| // Fetch roles | |
| const { data: roles, isLoading: isLoadingRoles } = useQuery({ | |
| queryKey: ["roles"], | |
| queryFn: roleService.getAll, | |
| }); | |
| // Set form data when employee data is loaded | |
| useEffect(() => { | |
| if (employee) { | |
| setFormData({ | |
| ...employee, | |
| // Use an empty string for password since we don't want to pre-fill it | |
| passwordHash: "" | |
| }); | |
| // Store original values for restricted fields | |
| setOriginalValues({ | |
| email: employee.email, | |
| empCode: employee.empCode, | |
| type: employee.type, | |
| roleId: employee.roleId | |
| }); | |
| } | |
| }, [employee]); | |
| // Check if this is the user's own profile | |
| useEffect(() => { | |
| if (employee && userData) { | |
| setIsOwnProfile(employee.userId === userData.userId); | |
| } | |
| }, [employee, userData]); | |
| // Handle form input changes | |
| const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |
| const { name, value } = e.target; | |
| // For restricted fields, only update if user has permission | |
| if (['email', 'empCode', 'type'].includes(name) && !hasHrAdminPermission) { | |
| return; | |
| } | |
| setFormData(prev => ({ | |
| ...prev, | |
| [name]: value | |
| })); | |
| }; | |
| // Handle select input changes | |
| const handleSelectChange = (name: string, value: string) => { | |
| setFormData(prev => ({ | |
| ...prev, | |
| [name]: value | |
| })); | |
| }; | |
| // Handle role selection | |
| const handleRoleChange = (value: string) => { | |
| // Only update role if user has permission | |
| if (!hasHrAdminPermission) { | |
| return; | |
| } | |
| setFormData(prev => ({ | |
| ...prev, | |
| roleId: parseInt(value) | |
| })); | |
| }; | |
| // Update mutation | |
| const updateMutation = useMutation({ | |
| mutationFn: (data: EmployeeRegistrationRequest) => { | |
| // If user doesn't have HR/Admin permission, restore original values for restricted fields | |
| if (!hasHrAdminPermission) { | |
| return employeeService.update({ | |
| ...data, | |
| email: originalValues.email, | |
| empCode: originalValues.empCode, | |
| type: originalValues.type, | |
| roleId: originalValues.roleId | |
| }); | |
| } | |
| return employeeService.update(data); | |
| }, | |
| onSuccess: (data) => { | |
| // Use response message if available | |
| const successMessage = data?.message || `Employee '${formData.firstName} ${formData.lastName}' has been successfully updated`; | |
| toast.success(successMessage); | |
| queryClient.invalidateQueries({ queryKey: ["employees"] }); | |
| queryClient.invalidateQueries({ queryKey: ["employee", id] }); | |
| // Navigate based on user role and profile ownership | |
| if (hasHrAdminPermission || !isOwnProfile) { | |
| navigate("/employees"); | |
| } else { | |
| // Regular users editing their own profile should go back to tasks | |
| navigate("/tasks"); | |
| } | |
| }, | |
| onError: (error: any) => { | |
| const errorMessage = error?.response?.data?.message || "Failed to update employee"; | |
| toast.error(errorMessage); | |
| } | |
| }); | |
| // Toggle sidebar | |
| const toggleSidebar = () => { | |
| setIsSidebarOpen(!isSidebarOpen); | |
| }; | |
| // Handle form submission | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| updateMutation.mutate(formData); | |
| }; | |
| // Handle navigation back | |
| const handleBack = () => { | |
| if (id) { | |
| navigate(`/employee/${id}`); | |
| } else { | |
| if (hasHrAdminPermission) { | |
| navigate("/employees"); | |
| } else { | |
| navigate("/tasks"); | |
| } | |
| } | |
| }; | |
| if (isLoading || isLoadingRoles) { | |
| return ( | |
| <div className="flex min-h-screen items-center justify-center"> | |
| <Loader2 className="h-8 w-8 animate-spin text-primary" /> | |
| </div> | |
| ); | |
| } | |
| if (!employee) { | |
| return ( | |
| <div className="flex min-h-screen items-center justify-center"> | |
| <p className="text-destructive">Employee not found</p> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="flex min-h-screen flex-col"> | |
| <Header toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} /> | |
| <Sidebar isOpen={isSidebarOpen} setIsOpen={setIsSidebarOpen} /> | |
| <main className={`flex-1 p-4 pt-20 md:p-6 md:pt-24 ${isSidebarOpen ? "md:ml-64" : ""} transition-all duration-300 overflow-hidden`}> | |
| <ScrollArea className="h-[calc(100vh-6rem)]"> | |
| <div className="mx-auto max-w-3xl pb-8"> | |
| <div className="mb-6"> | |
| <div className="flex items-center mb-2"> | |
| <Button | |
| variant="ghost" | |
| onClick={handleBack} | |
| className="mr-2" | |
| size="sm" | |
| > | |
| <ChevronLeft className="h-4 w-4 mr-1" /> | |
| Back to {isOwnProfile && !hasHrAdminPermission ? "Profile" : "Employee"} | |
| </Button> | |
| <h2 className="text-2xl md:text-3xl font-bold tracking-tight">Edit Employee</h2> | |
| </div> | |
| <p className="text-muted-foreground"> | |
| Update employee information | |
| </p> | |
| {!hasHrAdminPermission && ( | |
| <div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-md text-amber-800 text-sm flex items-start gap-2"> | |
| <InfoIcon className="h-5 w-5 shrink-0 mt-0.5" /> | |
| <div> | |
| <p className="font-medium">Restricted Access</p> | |
| <p>Some fields can only be edited by HR or Admin users. These fields are marked with a lock icon.</p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <form onSubmit={handleSubmit}> | |
| <Card> | |
| <CardHeader className="pb-4"> | |
| <CardTitle>Personal Information</CardTitle> | |
| <CardDescription> | |
| Edit the employee's personal details | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-6"> | |
| <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="empCode" className="flex items-center gap-1"> | |
| Employee Code* | |
| {!hasHrAdminPermission && ( | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <LockIcon className="h-3.5 w-3.5 text-amber-500" /> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>Only HR and Admin can edit this field</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| )} | |
| </Label> | |
| <Input | |
| id="empCode" | |
| name="empCode" | |
| value={formData.empCode} | |
| onChange={handleChange} | |
| placeholder="EMP-001" | |
| required | |
| disabled={!hasHrAdminPermission} | |
| className={!hasHrAdminPermission ? "bg-muted cursor-not-allowed" : ""} | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="firstName">First Name*</Label> | |
| <Input | |
| id="firstName" | |
| name="firstName" | |
| value={formData.firstName} | |
| onChange={handleChange} | |
| placeholder="John" | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="middleName">Middle Name</Label> | |
| <Input | |
| id="middleName" | |
| name="middleName" | |
| value={formData.middleName} | |
| onChange={handleChange} | |
| placeholder="David" | |
| /> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="lastName">Last Name*</Label> | |
| <Input | |
| id="lastName" | |
| name="lastName" | |
| value={formData.lastName} | |
| onChange={handleChange} | |
| placeholder="Doe" | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="email" className="flex items-center gap-1"> | |
| Email* | |
| {!hasHrAdminPermission && ( | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <LockIcon className="h-3.5 w-3.5 text-amber-500" /> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>Only HR and Admin can edit this field</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| )} | |
| </Label> | |
| <Input | |
| id="email" | |
| name="email" | |
| type="email" | |
| value={formData.email} | |
| onChange={handleChange} | |
| placeholder="john.doe@example.com" | |
| required | |
| disabled={!hasHrAdminPermission} | |
| className={!hasHrAdminPermission ? "bg-muted cursor-not-allowed" : ""} | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="mobile">Mobile Number*</Label> | |
| <Input | |
| id="mobile" | |
| name="mobile" | |
| value={formData.mobile} | |
| onChange={handleChange} | |
| placeholder="9876543210" | |
| required | |
| /> | |
| </div> | |
| </div> | |
| <Separator className="my-2" /> | |
| <h3 className="text-lg font-medium">Professional Details</h3> | |
| <div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="type" className="flex items-center gap-1"> | |
| Employee Type* | |
| {!hasHrAdminPermission && ( | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <LockIcon className="h-3.5 w-3.5 text-amber-500" /> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>Only HR and Admin can edit this field</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| )} | |
| </Label> | |
| <Input | |
| id="type" | |
| name="type" | |
| value={formData.type} | |
| onChange={handleChange} | |
| placeholder="Software Engineer" | |
| required | |
| disabled={!hasHrAdminPermission} | |
| className={!hasHrAdminPermission ? "bg-muted cursor-not-allowed" : ""} | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="academics">Academics*</Label> | |
| <Input | |
| id="academics" | |
| name="academics" | |
| value={formData.academics} | |
| onChange={handleChange} | |
| placeholder="BSc Computer Science" | |
| required | |
| /> | |
| </div> | |
| </div> | |
| {/* <div className="space-y-2"> | |
| <Label htmlFor="financialData">Financial Data</Label> | |
| <Textarea | |
| id="financialData" | |
| name="financialData" | |
| value={formData.financialData} | |
| onChange={handleChange} | |
| placeholder="Financial information (optional)" | |
| rows={3} | |
| className="resize-none" | |
| /> | |
| </div> */} | |
| <div className="space-y-2"> | |
| <Label htmlFor="description">Description</Label> | |
| <Textarea | |
| id="description" | |
| name="description" | |
| value={formData.description} | |
| onChange={handleChange} | |
| placeholder="Additional information about the employee (optional)" | |
| rows={3} | |
| className="resize-none" | |
| /> | |
| </div> | |
| <Separator className="my-2" /> | |
| <h3 className="text-lg font-medium">Account Details</h3> | |
| <div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="passwordHash"> | |
| Password{!id ? "*" : " (Leave blank to keep current password)"} | |
| </Label> | |
| <Input | |
| id="passwordHash" | |
| name="passwordHash" | |
| type="password" | |
| value={formData.passwordHash} | |
| onChange={handleChange} | |
| placeholder={id ? "Leave blank to keep current password" : "Enter password"} | |
| required={!id} | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="roleId" className="flex items-center gap-1"> | |
| Role <span className="text-destructive">*</span> | |
| {!hasHrAdminPermission && ( | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <LockIcon className="h-3.5 w-3.5 text-amber-500" /> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>Only HR and Admin can edit this field</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| )} | |
| </Label> | |
| <Dropdown | |
| options={roles?.map((role: Role) => ({ | |
| label: role.roleName, | |
| value: role.roleId.toString() | |
| })) || []} | |
| value={formData.roleId.toString()} | |
| onValueChange={handleRoleChange} | |
| placeholder="Select role" | |
| disabled={!hasHrAdminPermission} | |
| className={!hasHrAdminPermission ? "opacity-70 cursor-not-allowed" : ""} | |
| /> | |
| </div> | |
| </div> | |
| </CardContent> | |
| <CardFooter className="flex flex-col sm:flex-row justify-between gap-3 pt-6 border-t"> | |
| <Button | |
| variant="outline" | |
| onClick={handleBack} | |
| className="w-full sm:w-auto" | |
| > | |
| <ChevronLeft className="mr-2 h-4 w-4" /> | |
| Cancel | |
| </Button> | |
| <Button | |
| type="submit" | |
| disabled={updateMutation.isPending} | |
| className="w-full sm:w-auto" | |
| > | |
| {updateMutation.isPending ? ( | |
| <> | |
| <Loader2 className="mr-2 h-4 w-4 animate-spin" /> | |
| Updating... | |
| </> | |
| ) : ( | |
| <> | |
| Save Changes | |
| </> | |
| )} | |
| </Button> | |
| </CardFooter> | |
| </Card> | |
| </form> | |
| </div> | |
| </ScrollArea> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| export default EmployeeEdit; |