304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { useNavigate } from "react-router-dom"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Label } from "@/components/ui/label"
|
|
import apiService from "@/services/apiService"
|
|
import { showSuccessToast, showErrorToast, showUpdateItemToast } from "@/utils/toast"
|
|
import { buildFormData } from "@/utils/formDataBuilder"
|
|
|
|
// Import hooks
|
|
import { useMigrantForm } from "@/hooks/useMigrantForm"
|
|
import { usePhotoManagement } from "@/hooks/usePhotoManagement"
|
|
import { useMigrantData } from "@/hooks/useMigrantData"
|
|
|
|
// Import components
|
|
import AddDialog from "./Modal/AddDialog"
|
|
import UpdateDialog from "./Modal/UpdateDialog"
|
|
|
|
// Import step components
|
|
import PersonDetailsStep from "./form-steps/PersonDetailsStep"
|
|
import MigrationInfoStep from "./form-steps/MigrationInfoStep"
|
|
import NaturalizationStep from "./form-steps/NaturalizationStep"
|
|
import ResidenceStep from "./form-steps/ResidenceStep"
|
|
import FamilyStep from "./form-steps/FamilyStep"
|
|
import InternmentStep from "./form-steps/InternmentStep"
|
|
import PhotosStep from "./form-steps/PhotosStep"
|
|
import ReviewStep from "./form-steps/ReviewStep"
|
|
import StepperHeader from "./form-steps/StepperHeader"
|
|
import StepperNavigation from "./form-steps/StepperNavigation"
|
|
|
|
|
|
|
|
const StepperForm = () => {
|
|
const navigate = useNavigate()
|
|
|
|
// Form state management
|
|
const {
|
|
formData,
|
|
setters,
|
|
resetForm,
|
|
populateFormData,
|
|
validation
|
|
} = useMigrantForm()
|
|
|
|
const photoManagement = usePhotoManagement()
|
|
const { loading, isEditMode, initialDataLoaded } = useMigrantData(
|
|
populateFormData,
|
|
photoManagement.populatePhotoData
|
|
)
|
|
|
|
// Local state
|
|
const [currentStep, setCurrentStep] = useState(0)
|
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
|
const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
|
|
const renderFormField = (
|
|
label: string,
|
|
value: string,
|
|
onChange: (value: string) => void,
|
|
type = "text",
|
|
placeholder?: string,
|
|
required?: boolean,
|
|
fieldPath?: string // Add this to identify the field for validation
|
|
) => {
|
|
const hasError = fieldPath ? validation.hasFieldError(fieldPath) : false
|
|
const errorMessage = fieldPath ? validation.getFieldError(fieldPath) : null
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor={label.toLowerCase().replace(/\s+/g, "-")} className="text-sm font-medium text-gray-300">
|
|
{label}
|
|
{required && <span className="text-red-400 ml-1">*</span>}
|
|
</Label>
|
|
{type === "textarea" ? (
|
|
<Textarea
|
|
id={label.toLowerCase().replace(/\s+/g, "-")}
|
|
placeholder={placeholder || label}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className={`min-h-[100px] resize-none bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 focus:border-[#9B2335] focus:ring-[#9B2335] ${
|
|
hasError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
|
|
}`}
|
|
/>
|
|
) : (
|
|
<Input
|
|
id={label.toLowerCase().replace(/\s+/g, "-")}
|
|
type={type}
|
|
placeholder={placeholder || label}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className={`bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 focus:border-[#9B2335] focus:ring-[#9B2335] ${
|
|
hasError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
|
|
}`}
|
|
/>
|
|
)}
|
|
{errorMessage && (
|
|
<p className="text-sm text-red-400 mt-1">{errorMessage}</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const renderStepContent = () => {
|
|
switch (currentStep) {
|
|
case 0:
|
|
return <PersonDetailsStep person={formData.person} setPerson={setters.setPerson} renderFormField={renderFormField} />
|
|
case 1:
|
|
return <MigrationInfoStep migration={formData.migration} setMigration={setters.setMigration} renderFormField={renderFormField} />
|
|
case 2:
|
|
return <NaturalizationStep naturalization={formData.naturalization} setNaturalization={setters.setNaturalization} renderFormField={renderFormField} />
|
|
case 3:
|
|
return <ResidenceStep residence={formData.residence} setResidence={setters.setResidence} renderFormField={renderFormField} />
|
|
case 4:
|
|
return <FamilyStep family={formData.family} setFamily={setters.setFamily} renderFormField={renderFormField} />
|
|
case 5:
|
|
return <InternmentStep internment={formData.internment} setInternment={setters.setInternment} renderFormField={renderFormField} />
|
|
case 6:
|
|
return (
|
|
<PhotosStep
|
|
photos={photoManagement.photos}
|
|
photoPreviews={photoManagement.photoPreviews}
|
|
captions={photoManagement.captions}
|
|
existingPhotos={photoManagement.existingPhotos}
|
|
mainPhotoIndex={photoManagement.mainPhotoIndex}
|
|
API_BASE_URL={apiService.baseURL}
|
|
handlePhotoChange={photoManagement.handlePhotoChange}
|
|
removeExistingPhoto={photoManagement.removeExistingPhoto}
|
|
removeNewPhoto={photoManagement.removeNewPhoto}
|
|
setCaptions={photoManagement.setCaptions}
|
|
setMainPhotoIndex={photoManagement.setMainPhotoIndex}
|
|
/>
|
|
)
|
|
case 7:
|
|
return (
|
|
<ReviewStep
|
|
person={formData.person}
|
|
migration={formData.migration}
|
|
naturalization={formData.naturalization}
|
|
residence={formData.residence}
|
|
family={formData.family}
|
|
internment={formData.internment}
|
|
photos={photoManagement.photos}
|
|
photoPreviews={photoManagement.photoPreviews}
|
|
captions={photoManagement.captions}
|
|
existingPhotos={photoManagement.existingPhotos}
|
|
mainPhotoIndex={photoManagement.mainPhotoIndex ?? -1}
|
|
API_BASE_URL={apiService.baseURL}
|
|
/>
|
|
)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const submitForm = async () => {
|
|
try {
|
|
const formDataToSubmit = buildFormData({
|
|
...formData,
|
|
photos: photoManagement.photos,
|
|
captions: photoManagement.captions,
|
|
mainPhotoIndex: photoManagement.mainPhotoIndex,
|
|
existingPhotos: photoManagement.existingPhotos,
|
|
removedPhotoIds: photoManagement.removedPhotoIds,
|
|
isEditMode,
|
|
})
|
|
|
|
if (isEditMode) {
|
|
const id = window.location.pathname.split('/').pop()
|
|
return await apiService.updateMigrant(parseInt(id!), formDataToSubmit)
|
|
} else {
|
|
return await apiService.createMigrant(formDataToSubmit)
|
|
}
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const handleNext = () => {
|
|
// Validate current section before moving to next step
|
|
const sectionNames = ['person', 'migration', 'naturalization', 'residence', 'family', 'internment']
|
|
if (currentStep < sectionNames.length) {
|
|
const currentSection = sectionNames[currentStep] as 'person' | 'migration' | 'naturalization' | 'residence' | 'family' | 'internment'
|
|
const isValid = validation.validateSection(currentSection)
|
|
|
|
if (isValid || currentStep > 0) { // Allow moving forward from non-required sections
|
|
setCurrentStep(prev => prev + 1)
|
|
}
|
|
} else {
|
|
setCurrentStep(prev => prev + 1)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
if (!isSubmitting) {
|
|
// Validate entire form before submitting
|
|
const isValid = validation.validateForm()
|
|
|
|
if (!isValid) {
|
|
showErrorToast("Please fix the errors in the form before submitting.")
|
|
return
|
|
}
|
|
|
|
if (isEditMode) {
|
|
setIsUpdateDialogOpen(true)
|
|
} else {
|
|
setIsAddDialogOpen(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleConfirmSubmit = async () => {
|
|
if (!isSubmitting) {
|
|
try {
|
|
setIsSubmitting(true)
|
|
await submitForm()
|
|
|
|
if (isEditMode) {
|
|
showUpdateItemToast(`Migrant ${formData.person.surname}, ${formData.person.christian_name}`, () => {
|
|
navigate(`/admin/migrants`)
|
|
})
|
|
} else {
|
|
showSuccessToast(() => {
|
|
navigate(`/admin/migrants`)
|
|
})
|
|
resetForm()
|
|
photoManagement.resetPhotos()
|
|
}
|
|
} catch (error) {
|
|
showErrorToast("There was a problem saving the migrant data. Please try again.")
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
setIsAddDialogOpen(false)
|
|
setIsUpdateDialogOpen(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (loading && isEditMode && !initialDataLoaded) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
|
<Card className="w-full max-w-md bg-gray-900 border-gray-800">
|
|
<CardContent className="p-6 text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#9B2335] mx-auto mb-4"></div>
|
|
<p className="text-gray-400">Loading form data...</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto mt-8 mb-24 p-4">
|
|
<Card className="shadow-2xl border border-gray-800 bg-gray-900/50 overflow-hidden backdrop-blur-sm">
|
|
<StepperHeader currentStep={currentStep} isEditMode={isEditMode} />
|
|
|
|
<CardContent className="p-8 bg-gray-900">
|
|
{renderStepContent()}
|
|
|
|
{/* Display validation errors summary if any */}
|
|
{Object.keys(validation.validationErrors).length > 0 && (
|
|
<div className="mt-6 p-4 bg-red-900/20 border border-red-500/30 rounded-lg">
|
|
<h4 className="text-red-400 font-medium mb-2">Please fix the following errors:</h4>
|
|
<ul className="text-sm text-red-300 space-y-1">
|
|
{Object.entries(validation.validationErrors).map(([field, error]) => (
|
|
<li key={field}>• {error}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
|
|
<StepperNavigation
|
|
currentStep={currentStep}
|
|
totalSteps={8}
|
|
isSubmitting={isSubmitting}
|
|
isEditMode={isEditMode}
|
|
onPrevious={() => setCurrentStep(prev => prev - 1)}
|
|
onNext={handleNext}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
</Card>
|
|
|
|
<AddDialog
|
|
open={isAddDialogOpen}
|
|
onOpenChange={setIsAddDialogOpen}
|
|
onConfirm={handleConfirmSubmit}
|
|
isSubmitting={isSubmitting}
|
|
/>
|
|
|
|
<UpdateDialog
|
|
open={isUpdateDialogOpen}
|
|
onOpenChange={setIsUpdateDialogOpen}
|
|
onConfirm={handleConfirmSubmit}
|
|
isSubmitting={isSubmitting}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default StepperForm |