![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.quebec/private_html/src/components/ |
import { useState, useEffect, useRef, useMemo } from 'react';
import { useRouter } from 'next/router';
import { motion } from 'framer-motion';
import { useSession } from 'next-auth/react';
import { getCsrfToken } from 'next-auth/react';
import DocumentViewer from './DocumentViewer';
import CaseSelection from './CaseSelection';
import debounce from 'lodash.debounce';
import Draggable from 'react-draggable';
interface FormData {
firstName: string;
lastName: string;
email: string;
phone: string;
birthDate: string;
detaineeInfo: {
name: string;
facility: string;
inmateId: string;
};
relationship: string;
preferredLanguage: 'fr' | 'en';
preferredContactMethod: 'email' | 'phone' | 'mail';
gender: string;
address: {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
};
detaineeIncarcerationDates: string;
detaineeExpectedReleaseDates: string;
previousLegalActions: string;
reasonForJoining: string;
howDidYouHearAboutUs: string;
representation: string;
urgentNeeds: string;
additionalNotes: string;
lawyerName?: string;
lawyerEmail?: string;
lawyerPhone?: string;
lawFirm?: string;
status: string;
// Multi-case system
caseId?: string;
}
interface RegistrationFormProps {
onSuccess?: () => void;
initialData?: any;
isEditing?: boolean;
onSave?: (data: any) => void;
isAdmin?: boolean;
mode?: 'user' | 'admin' | 'edit';
initialValues?: any;
isFrench?: boolean;
initialLocale?: string;
isMobile?: boolean;
preSelectedCaseId?: string;
selectedCase?: any;
}
const RegistrationForm: React.FC<RegistrationFormProps> = ({
onSuccess,
initialData,
isEditing = false,
onSave,
isAdmin = false,
mode = 'user',
initialValues,
isFrench = false,
initialLocale,
isMobile: propIsMobile = false,
preSelectedCaseId,
selectedCase,
}) => {
const router = useRouter();
const { data: session } = useSession();
const [csrfToken, setCsrfToken] = useState<string>('');
const [formData, setFormData] = useState<FormData>(() => {
// Ensure all nested objects are properly initialized
const initialFormData = {
firstName: '',
lastName: '',
email: '',
phone: '',
birthDate: '',
detaineeInfo: {
name: '',
facility: '',
inmateId: '',
},
relationship: '',
preferredLanguage: initialLocale || 'en',
preferredContactMethod: 'email',
gender: '',
address: {
street: '',
city: '',
state: '',
postalCode: '',
country: 'CA', // default to CA
},
detaineeIncarcerationDates: '',
detaineeExpectedReleaseDates: '',
previousLegalActions: '',
reasonForJoining: '',
howDidYouHearAboutUs: '',
representation: '',
urgentNeeds: '',
additionalNotes: '',
lawyerName: '',
lawyerEmail: '',
lawyerPhone: '',
lawFirm: '',
status: 'PENDING',
caseId: preSelectedCaseId || '',
};
// If we have initialData, merge it with our defaults
if (initialData) {
// Format dates for the form
const formattedData = {
...initialData,
birthDate: initialData.birthDate ? new Date(initialData.birthDate).toISOString().split('T')[0] : '',
detaineeIncarcerationDates: initialData.detaineeInfo?.incarcerationDate
? new Date(initialData.detaineeInfo.incarcerationDate).toISOString().split('T')[0]
: '',
detaineeExpectedReleaseDates: initialData.detaineeInfo?.expectedReleaseDate
? new Date(initialData.detaineeInfo.expectedReleaseDate).toISOString().split('T')[0]
: '',
};
// Normalize country code
let countryCode = formattedData.address?.country;
if (countryCode === 'Canada') countryCode = 'CA';
if (countryCode === 'United States') countryCode = 'US';
if (!countryCode) countryCode = 'CA';
return {
...initialFormData,
...formattedData,
// Ensure nested objects are properly merged
address: {
...initialFormData.address,
...(formattedData.address || {}),
country: countryCode,
},
detaineeInfo: {
...initialFormData.detaineeInfo,
...(formattedData.detaineeInfo || {}),
},
};
}
return initialFormData;
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [isMobile, setIsMobile] = useState(propIsMobile);
// Mobile detection (fallback if not provided via props)
useEffect(() => {
const checkMobile = () => {
setIsMobile(propIsMobile || window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, [propIsMobile]);
const [selectedCountry, setSelectedCountry] = useState('CA');
const provincesAndStates = {
CA: [
{ value: 'AB', label: 'Alberta' },
{ value: 'BC', label: 'British Columbia' },
{ value: 'MB', label: 'Manitoba' },
{ value: 'NB', label: 'New Brunswick' },
{ value: 'NL', label: 'Newfoundland and Labrador' },
{ value: 'NS', label: 'Nova Scotia' },
{ value: 'NT', label: 'Northwest Territories' },
{ value: 'NU', label: 'Nunavut' },
{ value: 'ON', label: 'Ontario' },
{ value: 'PE', label: 'Prince Edward Island' },
{ value: 'QC', label: 'Quebec' },
{ value: 'SK', label: 'Saskatchewan' },
{ value: 'YT', label: 'Yukon' }
],
US: [
{ value: 'AL', label: 'Alabama' },
{ value: 'AK', label: 'Alaska' },
{ value: 'AZ', label: 'Arizona' },
{ value: 'AR', label: 'Arkansas' },
{ value: 'CA', label: 'California' },
{ value: 'CO', label: 'Colorado' },
{ value: 'CT', label: 'Connecticut' },
{ value: 'DE', label: 'Delaware' },
{ value: 'FL', label: 'Florida' },
{ value: 'GA', label: 'Georgia' },
{ value: 'HI', label: 'Hawaii' },
{ value: 'ID', label: 'Idaho' },
{ value: 'IL', label: 'Illinois' },
{ value: 'IN', label: 'Indiana' },
{ value: 'IA', label: 'Iowa' },
{ value: 'KS', label: 'Kansas' },
{ value: 'KY', label: 'Kentucky' },
{ value: 'LA', label: 'Louisiana' },
{ value: 'ME', label: 'Maine' },
{ value: 'MD', label: 'Maryland' },
{ value: 'MA', label: 'Massachusetts' },
{ value: 'MI', label: 'Michigan' },
{ value: 'MN', label: 'Minnesota' },
{ value: 'MS', label: 'Mississippi' },
{ value: 'MO', label: 'Missouri' },
{ value: 'MT', label: 'Montana' },
{ value: 'NE', label: 'Nebraska' },
{ value: 'NV', label: 'Nevada' },
{ value: 'NH', label: 'New Hampshire' },
{ value: 'NJ', label: 'New Jersey' },
{ value: 'NM', label: 'New Mexico' },
{ value: 'NY', label: 'New York' },
{ value: 'NC', label: 'North Carolina' },
{ value: 'ND', label: 'North Dakota' },
{ value: 'OH', label: 'Ohio' },
{ value: 'OK', label: 'Oklahoma' },
{ value: 'OR', label: 'Oregon' },
{ value: 'PA', label: 'Pennsylvania' },
{ value: 'RI', label: 'Rhode Island' },
{ value: 'SC', label: 'South Carolina' },
{ value: 'SD', label: 'South Dakota' },
{ value: 'TN', label: 'Tennessee' },
{ value: 'TX', label: 'Texas' },
{ value: 'UT', label: 'Utah' },
{ value: 'VT', label: 'Vermont' },
{ value: 'VA', label: 'Virginia' },
{ value: 'WA', label: 'Washington' },
{ value: 'WV', label: 'West Virginia' },
{ value: 'WI', label: 'Wisconsin' },
{ value: 'WY', label: 'Wyoming' }
]
};
const genderOptions = [
{ value: '', label: 'Select' },
{ value: 'male', label: 'Male' },
{ value: 'female', label: 'Female' },
{ value: 'non_binary', label: 'Non-binary' },
{ value: 'prefer_not_to_say', label: 'Prefer not to say' }
];
const countryOptions = [
{ value: 'CA', label: 'Canada' },
{ value: 'US', label: 'United States' }
];
const facilityOptions = useMemo(() => [
{ value: '', label: 'Sélectionner' },
{ value: 'amos', label: "Établissement de détention d'Amos" },
{ value: 'baie-comeau', label: 'Établissement de détention de Baie-Comeau' },
{ value: 'bordeaux', label: 'Établissement de détention de Montréal (Bordeaux)' },
{ value: 'cowansville', label: 'Établissement de détention de Cowansville' },
{ value: 'drummondville', label: 'Établissement de détention de Drummondville' },
{ value: 'hull', label: 'Établissement de détention de Hull' },
{ value: 'leclerc', label: 'Établissement de détention de Laval (Leclerc)' },
{ value: 'new-carlisle', label: 'Établissement de détention de New Carlisle' },
{ value: 'perce', label: 'Établissement de détention de Percé' },
{ value: 'quebec', label: 'Établissement de détention de Québec' },
{ value: 'rimouski', label: 'Établissement de détention de Rimouski' },
{ value: 'roberval', label: 'Établissement de détention de Roberval' },
{ value: 'saint-jerome', label: 'Établissement de détention de Saint-Jérôme' },
{ value: 'sept-iles', label: 'Établissement de détention de Sept-Îles' },
{ value: 'sherbrooke', label: 'Établissement de détention de Sherbrooke' },
{ value: 'sorel-tracy', label: 'Établissement de détention de Sorel-Tracy' },
{ value: 'trois-rivieres', label: 'Établissement de détention de Trois-Rivières' },
{ value: 'other', label: 'Autre établissement' }
], []);
const relationshipOptions = [
{ value: '', label: 'Select' },
{ value: 'self', label: 'I am the Detainee' },
{ value: 'spouse', label: 'Spouse' },
{ value: 'parent', label: 'Parent' },
{ value: 'child', label: 'Child' },
{ value: 'sibling', label: 'Sibling' },
{ value: 'friend', label: 'Friend' },
{ value: 'lawyer', label: 'Lawyer' },
{ value: 'other', label: 'Other' }
];
const contactMethodOptions = [
{ value: 'email', label: 'Email' },
{ value: 'phone', label: 'Phone' },
{ value: 'mail', label: 'Mail' }
];
const languageOptions = [
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'French' }
];
const representationOptions = [
{ value: '', label: 'Select' },
{ value: 'yes', label: 'Yes, I have a lawyer' },
{ value: 'no', label: 'No, I don\'t have a lawyer' },
{ value: 'looking', label: 'I\'m looking for a lawyer' }
];
const howDidYouHearOptions = [
{ value: '', label: 'Select' },
{ value: 'social_media', label: 'Social Media' },
{ value: 'friend', label: 'Friend or Family' },
{ value: 'lawyer', label: 'Lawyer' },
{ value: 'news', label: 'News' },
{ value: 'search', label: 'Search Engine' },
{ value: 'other', label: 'Other' }
];
const [files, setFiles] = useState<File[]>([]);
const [filePreviews, setFilePreviews] = useState<{ file: File, url: string }[]>([]);
const statusOptions = [
{ value: 'PENDING', label: 'Pending' },
{ value: 'MISSING_DOCUMENTS', label: 'Missing Documents' },
{ value: 'DOCUMENTS_UNDER_REVIEW', label: 'Documents Under Review' },
{ value: 'ADDITIONAL_INFO_NEEDED', label: 'Additional Info Needed' },
{ value: 'VERIFICATION_IN_PROGRESS', label: 'Verification In Progress' },
{ value: 'LAWYER_VERIFICATION', label: 'Lawyer Verification' },
{ value: 'FACILITY_VERIFICATION', label: 'Facility Verification' },
{ value: 'DOCUMENTS_EXPIRED', label: 'Documents Expired' },
{ value: 'DOCUMENTS_INCOMPLETE', label: 'Documents Incomplete' },
{ value: 'INFORMATION_MISMATCH', label: 'Information Mismatch' },
{ value: 'PENDING_PAYMENT', label: 'Pending Payment' },
{ value: 'PAYMENT_RECEIVED', label: 'Payment Received' },
{ value: 'PENDING_LAWYER_APPROVAL', label: 'Pending Lawyer Approval' },
{ value: 'PENDING_FACILITY_APPROVAL', label: 'Pending Facility Approval' },
{ value: 'ON_HOLD', label: 'On Hold' },
{ value: 'ESCALATED', label: 'Escalated' },
{ value: 'FINAL_REVIEW', label: 'Final Review' },
{ value: 'APPROVED', label: 'Approved' },
{ value: 'REJECTED', label: 'Rejected' },
{ value: 'COMPLETED', label: 'Completed' }
];
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const autosaveTimeout = useRef<NodeJS.Timeout | null>(null);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const isEditingRegistration = Boolean(initialValues && initialValues.id);
// Debounced autosave function
const autosave = debounce(async (data) => {
if (!isEditingRegistration) return;
setSaving(true);
setSaved(false);
try {
const endpoint = isAdmin
? `/api/admin/registrations/${initialValues.id}
: `/api/user/registrations/${initialValues.id}
await fetch(endpoint, {
method: isAdmin ? 'PATCH' : 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
setSaved(true);
setLastSaved(new Date());
} catch {
// Optionally show error
} finally {
setSaving(false);
}
}, 1000);
useEffect(() => {
// Create object URLs for each file
const previews = files.map(file => ({
file,
url: URL.createObjectURL(file)
}));
setFilePreviews(previews);
// Cleanup: revoke all object URLs when files change or component unmounts
return () => {
previews.forEach(p => URL.revokeObjectURL(p.url));
};
}, [files]);
useEffect(() => {
// Get CSRF token when component mounts
const getToken = async () => {
const token = await getCsrfToken();
if (token) setCsrfToken(token);
};
getToken();
}, []);
useEffect(() => {
// Only autosave if editing an existing registration (has initialValues.id)
if (!initialValues || !initialValues.id) return;
// Don't autosave if loading or submitting
if (loading) return;
// Debounce autosave
if (autosaveTimeout.current) clearTimeout(autosaveTimeout.current);
autosaveTimeout.current = setTimeout(async () => {
try {
setLoading(true);
// Format the data to match the database schema
const formattedData = {
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
phone: formData.phone,
birthDate: new Date(formData.birthDate).toISOString(),
gender: formData.gender,
relationship: formData.relationship,
preferredLanguage: formData.preferredLanguage,
preferredContactMethod: formData.preferredContactMethod,
message: formData.additionalNotes,
additionalNotes: formData.additionalNotes,
previousLegalActions: formData.previousLegalActions,
reasonForJoining: formData.reasonForJoining,
howDidYouHearAboutUs: formData.howDidYouHearAboutUs,
representation: formData.representation,
urgentNeeds: formData.urgentNeeds,
address: {
street: formData.address.street || '',
city: formData.address.city || '',
state: formData.address.state || '',
postalCode: formData.address.postalCode || '',
country: formData.address.country || '',
},
detaineeInfo: {
name: formData.detaineeInfo.name || '',
facility: formData.detaineeInfo.facility || '',
inmateId: formData.detaineeInfo.inmateId || '',
incarcerationDate: formData.detaineeIncarcerationDates
? new Date(formData.detaineeIncarcerationDates).toISOString()
: undefined,
expectedReleaseDate: formData.detaineeExpectedReleaseDates
? new Date(formData.detaineeExpectedReleaseDates).toISOString()
: null,
},
lawyerName: formData.lawyerName,
lawyerEmail: formData.lawyerEmail,
lawyerPhone: formData.lawyerPhone,
lawFirm: formData.lawFirm,
};
const endpoint = isAdmin
? `/api/admin/registrations/${initialValues.id}
: `/api/user/registrations/${initialValues.id}
await fetch(endpoint, {
method: isAdmin ? 'PATCH' : 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken || '',
},
body: JSON.stringify(formattedData),
});
setLastSaved(new Date());
} catch (err) {
// Optionally handle autosave errors
} finally {
setLoading(false);
}
}, 1000); // 1 second debounce
return () => {
if (autosaveTimeout.current) clearTimeout(autosaveTimeout.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formData]);
useEffect(() => {
if (preSelectedCaseId) {
setFormData(prev => ({
...prev,
caseId: preSelectedCaseId
}));
}
}, [preSelectedCaseId]);
const validateForm = () => {
const missingFields = [];
// Case Selection Validation (only for non-admin users)
if (!isAdmin && !formData.caseId) missingFields.push('Legal Case Selection');
if (!formData.firstName) missingFields.push('First Name');
if (!formData.lastName) missingFields.push('Last Name');
if (!formData.email) missingFields.push('Email');
if (!formData.phone) missingFields.push('Phone');
if (!formData.birthDate) missingFields.push('Birth Date');
if (!formData.gender) missingFields.push('Gender');
if (!formData.address.street) missingFields.push('Street Address');
if (!formData.address.city) missingFields.push('City');
if (!formData.address.state) missingFields.push('State/Province');
if (!formData.address.postalCode) missingFields.push('Postal Code');
if (!formData.address.country) missingFields.push('Country');
if (!formData.detaineeInfo.name) missingFields.push('Detainee Name');
if (!formData.detaineeInfo.facility) missingFields.push('Facility');
if (!formData.detaineeInfo.inmateId) missingFields.push('Inmate ID');
if (!formData.relationship) missingFields.push('Relationship to Detainee');
if (!formData.preferredLanguage) missingFields.push('Preferred Language');
if (!formData.preferredContactMethod) missingFields.push('Preferred Contact Method');
if (!formData.representation) missingFields.push('Legal Representation');
if (!formData.howDidYouHearAboutUs) missingFields.push('How did you hear about us');
if (!formData.detaineeIncarcerationDates) missingFields.push('Incarceration Date');
if (missingFields.length > 0) {
setError('Please fill in all required fields. Missing fields: ' + missingFields.join(', '));
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
if (!validateForm()) {
setLoading(false);
return;
}
try {
// Use the correct endpoint for user vs admin
const endpoint = isAdmin ? '/api/register' : '/api/user/registrations';
// Construct the payload with proper detaineeInfo structure
const payload = {
...formData,
detaineeInfo: {
name: formData.detaineeInfo.name,
facility: formData.detaineeInfo.facility,
inmateId: formData.detaineeInfo.inmateId,
incarcerationDate: formData.detaineeIncarcerationDates
? new Date(formData.detaineeIncarcerationDates).toISOString()
: undefined,
expectedReleaseDate: formData.detaineeExpectedReleaseDates
? new Date(formData.detaineeExpectedReleaseDates).toISOString()
: null,
},
};
// Remove fields not expected by backend
delete (payload as any).detaineeIncarcerationDates;
delete (payload as any).detaineeExpectedReleaseDates;
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) {
// Show error popup/message
setError(data.message || 'Registration error');
alert(data.message || 'Registration error');
setLoading(false);
return;
}
// Show success popup/message
alert('Application submitted successfully!');
if (onSuccess) {
onSuccess();
} else {
router.push('/success');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
alert(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
if (name.includes('.')) {
const [parent, child] = name.split('.');
setFormData(prev => ({
...prev,
[parent]: {
...((prev[parent as keyof FormData] || {}) as object),
[child]: value
}
}));
} else {
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
}));
}
if (isEditingRegistration) autosave(formData);
};
const handleCountryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
setSelectedCountry(value);
setFormData(prev => ({
...prev,
address: {
...prev.address,
country: value,
state: ''
}
}));
};
// Update useEffect to handle initial country value as code
useEffect(() => {
if (initialValues?.address?.country) {
// If the country is already a code, use it; otherwise, map name to code
let countryCode = initialValues.address.country;
if (countryCode === 'Canada') countryCode = 'CA';
if (countryCode === 'United States') countryCode = 'US';
setSelectedCountry(countryCode);
setFormData(prev => ({
...prev,
address: {
...prev.address,
country: countryCode
}
}));
}
}, [initialValues]);
const [openViewers, setOpenViewers] = useState<{ id: number, file: File }[]>([]);
let viewerId = 0;
// File input handler for multiple files
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newFiles = Array.from(e.target.files || []);
setFiles(prev => [...prev, ...newFiles]);
};
// Remove file from list
const handleRemoveFile = (index: number) => {
setFiles(prev => prev.filter((_, i) => i !== index));
};
// Open DocumentViewer modal
const handleViewFile = (file: File) => {
setOpenViewers(prev => [...prev, { id: ++viewerId, file }]);
};
// Close DocumentViewer modal
const handleCloseViewer = (id: number) => {
setOpenViewers(prev => prev.filter(v => v.id !== id));
};
const [openDocument, setOpenDocument] = useState<null | { url: string; type: string; name: string }>(null);
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<div className="bg-white shadow sm:rounded-lg">
<div className="px-4 py-5 sm:p-6">
<form onSubmit={handleSubmit} className="space-y-10">
{/* Last Saved Timestamp */}
{isEditingRegistration && lastSaved && (
<div className="mb-4 text-sm text-gray-500">
Last saved: {lastSaved.toLocaleString()}
</div>
)}
{/* Personal Information */}
<div>
<h2 className="text-xl font-bold mb-4">Personal Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
First Name <span className="text-red-500">*</span>
</label>
<input type="text" name="firstName" value={formData.firstName} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Last Name <span className="text-red-500">*</span>
</label>
<input type="text" name="lastName" value={formData.lastName} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email <span className="text-red-500">*</span>
</label>
<input type="email" name="email" value={formData.email} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Phone <span className="text-red-500">*</span>
</label>
<input type="tel" name="phone" value={formData.phone} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Birth Date <span className="text-red-500">*</span>
</label>
<input type="date" name="birthDate" value={formData.birthDate} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gender <span className="text-red-500">*</span>
</label>
<select name="gender" value={formData.gender} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
{genderOptions.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</div>
</div>
</div>
{/* Address */}
<div>
<h2 className="text-xl font-bold mb-4">Address</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street Address <span className="text-red-500">*</span>
</label>
<input type="text" name="address.street" value={formData.address.street} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City <span className="text-red-500">*</span>
</label>
<input type="text" name="address.city" value={formData.address.city} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country <span className="text-red-500">*</span>
</label>
<select
value={selectedCountry}
onChange={handleCountryChange}
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
>
{countryOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State/Province <span className="text-red-500">*</span>
</label>
<select
name="address.state"
value={formData.address.state}
onChange={handleInputChange}
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
>
<option value="">Select</option>
{(selectedCountry === 'CA' || selectedCountry === 'US') &&
provincesAndStates[selectedCountry].map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))
}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code <span className="text-red-500">*</span>
</label>
<input type="text" name="address.postalCode" value={formData.address.postalCode} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
</div>
</div>
{/* Detainee Information */}
<div>
<h2 className="text-xl font-bold mb-4">Detainee Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Detainee Name <span className="text-red-500">*</span>
</label>
<input type="text" name="detaineeInfo.name" value={formData.detaineeInfo.name} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Facility <span className="text-red-500">*</span>
</label>
<select name="detaineeInfo.facility" value={formData.detaineeInfo.facility} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
{facilityOptions.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
<p className="text-xs text-gray-500 mt-1">Select the facility where the detainee is currently held.</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Inmate ID <span className="text-red-500">*</span>
</label>
<input type="text" name="detaineeInfo.inmateId" value={formData.detaineeInfo.inmateId} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
<p className="text-xs text-gray-500 mt-1">As provided by the facility.</p>
</div>
<div className="md:col-span-2">
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Incarceration Date <span className="text-red-500">*</span>
</label>
<input type="date" name="detaineeIncarcerationDates" value={formData.detaineeIncarcerationDates} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Expected Release Date <span className="text-gray-400">(optional)</span>
</label>
<input type="date" name="detaineeExpectedReleaseDates" value={formData.detaineeExpectedReleaseDates} onChange={handleInputChange} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Relationship to Detainee <span className="text-red-500">*</span>
</label>
<select name="relationship" value={formData.relationship} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
{relationshipOptions.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</div>
</div>
</div>
{/* Contact Preferences */}
<div>
<h2 className="text-xl font-bold mb-4">Contact Preferences</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Preferred Language <span className="text-red-500">*</span>
</label>
<select name="preferredLanguage" value={formData.preferredLanguage} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
{languageOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Preferred Contact Method <span className="text-red-500">*</span>
</label>
<select name="preferredContactMethod" value={formData.preferredContactMethod} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="email">Email</option>
<option value="phone">Phone</option>
<option value="mail">Mail</option>
</select>
</div>
</div>
</div>
{/* Legal Representation */}
<div>
<h2 className="text-xl font-bold mb-4">Legal Representation</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Legal Representation <span className="text-red-500">*</span>
</label>
<select name="representation" value={formData.representation} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="">Select</option>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</div>
</div>
{formData.representation === 'yes' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Lawyer Name <span className="text-gray-400">(optional)</span></label>
<input type="text" name="lawyerName" value={formData.lawyerName} onChange={handleInputChange} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Law Firm <span className="text-gray-400">(optional)</span></label>
<input type="text" name="lawFirm" value={formData.lawFirm} onChange={handleInputChange} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Lawyer Email <span className="text-gray-400">(optional)</span></label>
<input type="email" name="lawyerEmail" value={formData.lawyerEmail} onChange={handleInputChange} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Lawyer Phone <span className="text-gray-400">(optional)</span></label>
<input type="tel" name="lawyerPhone" value={formData.lawyerPhone} onChange={handleInputChange} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
</div>
)}
</div>
{/* Additional Information */}
<div>
<h2 className="text-xl font-bold mb-4">Additional Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Previous Legal Actions <span className="text-gray-400">(optional)</span></label>
<textarea name="previousLegalActions" value={formData.previousLegalActions} onChange={handleInputChange} rows={2} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Reason for Joining <span className="text-gray-400">(optional)</span></label>
<textarea name="reasonForJoining" value={formData.reasonForJoining} onChange={handleInputChange} rows={2} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">How did you hear about us? <span className="text-red-500">*</span></label>
<select name="howDidYouHearAboutUs" value={formData.howDidYouHearAboutUs} onChange={handleInputChange} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
{howDidYouHearOptions.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Urgent Needs <span className="text-gray-400">(optional)</span></label>
<textarea name="urgentNeeds" value={formData.urgentNeeds} onChange={handleInputChange} rows={2} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Additional Notes <span className="text-gray-400">(optional)</span></label>
<textarea name="additionalNotes" value={formData.additionalNotes} onChange={handleInputChange} rows={2} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
</div>
</div>
{/* Admin: Existing Documents */}
{isAdmin && initialData?.documents && initialData.documents.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-bold mb-2">Uploaded Documents</h2>
<ul className="space-y-2">
{initialData.documents.map((doc: any) => (
<li key={doc.id} className="flex items-center gap-2">
<span className="truncate flex-1">{doc.name || doc.title || doc.url}</span>
<button
type="button"
className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
onClick={() => setOpenDocument({ url: doc.url, type: doc.type, name: doc.name || doc.title || doc.url })}
>
View
</button>
</li>
))}
</ul>
</div>
)}
{error && <div className="text-red-600 text-sm mt-2">{error}</div>}
{saving && <div className="text-sm text-blue-500">Saving...</div>}
{saved && !saving && <div className="text-sm text-green-500">Saved</div>}
{!isEditingRegistration && (
<button type="submit" disabled={loading} className="w-full py-3 px-4 bg-indigo-600 text-white rounded-md text-lg font-semibold hover:bg-indigo-700 disabled:opacity-50 mt-8">
{loading ? 'Submitting...' : 'Submit'}
</button>
)}
</form>
</div>
</div>
</div>
{/* DocumentViewer modals */}
{openViewers.map(({ id, file }) => (
<Draggable key={id} handle=".modal-header">
<div className="fixed z-50 top-20 left-20 bg-white shadow-lg rounded-lg border border-gray-300 w-[90vw] max-w-2xl h-[80vh] flex flex-col">
<div className="modal-header flex items-center justify-between bg-indigo-600 text-white px-4 py-2 cursor-move rounded-t-lg">
<span>{file.name}</span>
<div>
<button onClick={() => handleCloseViewer(id)} className="ml-2 px-2 py-1 bg-red-500 rounded text-white">Close</button>
</div>
</div>
<div className="flex-1 overflow-auto">
<DocumentViewer
url={URL.createObjectURL(file)}
type={file.type}
name={file.name}
/>
</div>
</div>
</Draggable>
))}
{/* DocumentViewer modal for existing documents */}
{openDocument && (
<div className="fixed z-50 inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full h-[80vh] flex flex-col">
<div className="flex items-center justify-between px-4 py-2 bg-indigo-600 text-white rounded-t-lg">
<span>{openDocument.name}</span>
<button onClick={() => setOpenDocument(null)} className="ml-2 px-2 py-1 bg-red-500 rounded text-white">Close</button>
</div>
<div className="flex-1 overflow-auto">
<DocumentViewer url={openDocument.url} type={openDocument.type} name={openDocument.name} />
</div>
</div>
</div>
)}
</div>
);
};
export default RegistrationForm;