import React, { useState, useRef, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { format } from 'date-fns'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Calendar } from '@/components/ui/calendar'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } from '@/components/ui/form'; import { SearchableDropdown } from './shared/SearchableDropdown'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Loader2, Save, X, Calendar as CalendarIcon, User } from 'lucide-react'; import { AssetAssignment, AssetAssignmentCreateRequest, AssetAssignmentUpdateRequest, AssetAssignmentFormSchema, } from '@/types/asset-management'; import { Employee } from '@/types'; import { useAssets } from '@/hooks/asset-management/useAssets'; // Custom DatePicker component that works in modals interface CustomDatePickerProps { value: Date | null; onChange: (date: Date | null) => void; placeholder?: string; disabled?: (date: Date) => boolean; } const CustomDatePicker: React.FC = ({ value, onChange, placeholder = "Pick a date", disabled }) => { const [isOpen, setIsOpen] = useState(false); const datePickerRef = useRef(null); // Close date picker when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (datePickerRef.current && !datePickerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; } }, [isOpen]); return (
{isOpen && (

Select Date

{ onChange(date || null); setIsOpen(false); }} disabled={disabled} initialFocus className="rounded border" />
)}
); }; interface AssignmentFormProps { assignment?: AssetAssignment; preselectedAssetId?: number; preselectedEmployeeId?: number; employees?: Employee[]; onSubmit: (data: AssetAssignmentCreateRequest | AssetAssignmentUpdateRequest) => Promise; onCancel: () => void; isSubmitting?: boolean; title?: string; } type AssignmentFormValues = { assetID: number; employeeID: number; employeeName: string; assetCode: string; assignedDate: Date; returnedDate: Date | null; remarks: string; }; const AssignmentForm: React.FC = ({ assignment, preselectedAssetId, preselectedEmployeeId, employees = [], onSubmit, onCancel, isSubmitting = false, title }) => { const isEditing = !!assignment; const formTitle = title || (isEditing ? 'Edit Assignment' : 'Create New Assignment'); const { data: assets = [], isLoading: isLoadingAssets } = useAssets(); const employeeFieldRef = useRef(null); const form = useForm({ resolver: zodResolver(AssetAssignmentFormSchema), defaultValues: { assetID: assignment?.assetID || preselectedAssetId || 0, employeeID: assignment?.employeeID || preselectedEmployeeId || 0, employeeName: assignment?.employeeName || '', assetCode: assignment?.assetCode || '', assignedDate: assignment ? new Date(assignment.assignedDate) : new Date(), returnedDate: assignment?.returnedDate ? new Date(assignment.returnedDate) : null, remarks: assignment?.remarks || '', }, }); // Get preselected asset details const preselectedAsset = preselectedAssetId ? assets.find(a => a.assetID === preselectedAssetId) : null; // Auto-populate asset field when preselectedAssetId is provided useEffect(() => { if (preselectedAssetId && assets.length > 0 && !assignment) { const asset = assets.find(a => a.assetID === preselectedAssetId); if (asset) { form.setValue('assetID', asset.assetID); form.setValue('assetCode', asset.assetCode); } } }, [preselectedAssetId, assets, assignment, form]); // Auto-focus employee selection field when asset is pre-selected useEffect(() => { if (preselectedAssetId && !assignment && employeeFieldRef.current) { // Small delay to ensure the form is fully rendered const timer = setTimeout(() => { employeeFieldRef.current?.focus(); }, 100); return () => clearTimeout(timer); } }, [preselectedAssetId, assignment]); const handleSubmit = async (values: AssignmentFormValues) => { try { // Validate dates before submission if (values.returnedDate && values.returnedDate <= values.assignedDate) { form.setError('returnedDate', { type: 'manual', message: 'Returned date must be after the assigned date.' }); return; } // Convert dates to ISO strings const assignedDateISO = values.assignedDate.toISOString(); const returnedDateISO = values.returnedDate ? values.returnedDate.toISOString() : null; if (isEditing && assignment) { const updateData: AssetAssignmentUpdateRequest = { assignmentID: assignment.assignmentID, assetID: values.assetID, employeeID: values.employeeID, employeeName: values.employeeName, assetCode: values.assetCode, assignedDate: assignedDateISO, returnedDate: returnedDateISO, remarks: values.remarks, }; await onSubmit(updateData); } else { const createData: AssetAssignmentCreateRequest = { assetID: values.assetID, employeeID: values.employeeID, employeeName: values.employeeName, assetCode: values.assetCode, assignedDate: assignedDateISO, returnedDate: returnedDateISO, remarks: values.remarks, }; await onSubmit(createData); } } catch (error) { console.error('Form submission error:', error); // Set form errors if validation failed if (error instanceof Error) { if (error.message.includes('Asset with ID') && error.message.includes('does not exist')) { form.setError('assetID', { type: 'manual', message: 'The selected asset does not exist. Please select a valid asset.' }); } else if (error.message.includes('Employee with ID') && error.message.includes('does not exist')) { form.setError('employeeID', { type: 'manual', message: 'The selected employee does not exist. Please select a valid employee.' }); } else if (error.message.includes('already assigned')) { form.setError('assetID', { type: 'manual', message: 'This asset is already assigned to another employee. Please select a different asset or return the current assignment first.' }); } else if (error.message.includes('date')) { form.setError('assignedDate', { type: 'manual', message: 'Invalid date format. Please select a valid date.' }); } else { // Set a general form error form.setError('root', { type: 'manual', message: error.message }); } } } }; const getEmployeeDisplayName = (employee: Employee) => { return `${employee.firstName} ${employee.lastName}`; }; return ( {formTitle}
{/* Display form-level errors */} {form.formState.errors.root && (

{form.formState.errors.root.message}

)} {/* Display pre-selected asset information prominently */} {preselectedAsset && !assignment && (

Assigning Asset

Code: {preselectedAsset.assetCode}

Type: {preselectedAsset.assetType}

Model: {preselectedAsset.brandModel}

)}
{/* Asset Selection */} ( Asset * {preselectedAssetId && !assignment ? 'Asset is pre-selected for this assignment.' : 'Select the asset to assign to the employee.'} ({ value: asset.assetID.toString(), label: `${asset.assetCode} - ${asset.assetType} - ${asset.brandModel}` }))} value={field.value?.toString() || ''} onChange={(value) => { const assetId = parseInt(value); field.onChange(assetId); // Auto-populate asset code when asset is selected const selectedAsset = assets.find(asset => asset.assetID === assetId); if (selectedAsset) { form.setValue('assetCode', selectedAsset.assetCode); } }} placeholder="Select an asset" searchPlaceholder="Search assets by code, type, or model..." disabled={isLoadingAssets || (!!preselectedAssetId && !assignment)} error={!!form.formState.errors.assetID} aria-label="Asset selection" aria-describedby={form.formState.errors.assetID ? 'assetID-error' : undefined} emptyMessage="No assets found matching your search" /> )} /> {/* Employee Selection */} ( Employee * Select the employee to assign the asset to.
{ if (el && preselectedAssetId && !assignment) { const trigger = el.querySelector('[role="combobox"]') as HTMLElement; if (trigger) { (employeeFieldRef as any).current = trigger; } } }}> ({ value: employee.id.toString(), label: `#${employee.empCode} - ${getEmployeeDisplayName(employee)} - ${employee.email}` }))} value={field.value?.toString() || ''} onChange={(value) => { const employeeId = parseInt(value); field.onChange(employeeId); // Auto-populate employee name when employee is selected const selectedEmployee = employees.find(employee => employee.id === employeeId); if (selectedEmployee) { form.setValue('employeeName', getEmployeeDisplayName(selectedEmployee)); } }} placeholder="Select an employee" searchPlaceholder="Search employees by name, email, or ID..." error={!!form.formState.errors.employeeID} aria-label="Employee selection" aria-describedby={form.formState.errors.employeeID ? 'employeeID-error' : undefined} emptyMessage="No employees found matching your search" />
)} /> {/* Employee Name (Auto-populated) */} ( Employee Name * This field is automatically populated when you select an employee. )} /> {/* Asset Code (Auto-populated) */} ( Asset Code * This field is automatically populated when you select an asset. )} /> {/* Assigned Date */} ( Assigned Date * date < new Date("1900-01-01")} /> The date when the asset was assigned to the employee. )} /> {/* Returned Date */} ( Returned Date { const assignedDate = form.getValues('assignedDate'); return ( date < new Date("1900-01-01") || (assignedDate && date < assignedDate) ); }} /> Leave empty if the asset is still assigned. The returned date must be after the assigned date. )} />
{/* Remarks */} ( Remarks *