import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { validateSubmissionHandler } from '@/lib/entityFormValidation'; import { getErrorMessage } from '@/lib/errorHandler'; import type { RideTechnicalSpec, RideCoasterStat, RideNameHistory } from '@/types/database'; import type { TempCompanyData, TempRideModelData, TempParkData } from '@/types/company'; import { entitySchemas, validateRequiredFields } from '@/lib/entityValidationSchemas'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DatePicker } from '@/components/ui/date-picker'; import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-date-input'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Combobox } from '@/components/ui/combobox'; import { SlugField } from '@/components/ui/slug-field'; import { Checkbox } from '@/components/ui/checkbox'; import { toast } from '@/hooks/use-toast'; import { handleError } from '@/lib/errorHandler'; import { Plus, Zap, Save, X, Building2, AlertCircle } from 'lucide-react'; import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils'; import { useUnitPreferences } from '@/hooks/useUnitPreferences'; import { useManufacturers, useRideModels, useParks } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; import { ManufacturerForm } from './ManufacturerForm'; import { RideModelForm } from './RideModelForm'; import { ParkForm } from './ParkForm'; import { TechnicalSpecsEditor, validateTechnicalSpecs } from './editors/TechnicalSpecsEditor'; import { CoasterStatsEditor, validateCoasterStats } from './editors/CoasterStatsEditor'; import { FormerNamesEditor } from './editors/FormerNamesEditor'; import { convertValueToMetric, convertValueFromMetric, getDisplayUnit, getSpeedUnit, getDistanceUnit, getHeightUnit } from '@/lib/units'; type RideFormData = z.infer; interface RideFormProps { onSubmit: (data: RideFormData & { _tempNewPark?: TempParkData; _tempNewManufacturer?: TempCompanyData; _tempNewDesigner?: TempCompanyData; _tempNewRideModel?: TempRideModelData; }) => Promise; onCancel?: () => void; initialData?: Partial; isEditing?: boolean; } const categories = [ 'roller_coaster', 'flat_ride', 'water_ride', 'dark_ride', 'kiddie_ride', 'transportation' ]; const statusOptions = [ 'Operating', 'Closed Temporarily', 'Closed Permanently', 'Under Construction', 'Relocated', 'Stored', 'Demolished' ]; const coasterTypes = [ 'steel', 'wood', 'hybrid' ]; const seatingTypes = [ 'sit_down', 'stand_up', 'flying', 'inverted', 'floorless', 'suspended', 'wing', 'dive', 'spinning' ]; const intensityLevels = [ 'family', 'thrill', 'extreme' ]; const TRACK_MATERIALS = [ { value: 'steel', label: 'Steel' }, { value: 'wood', label: 'Wood' }, ]; const SUPPORT_MATERIALS = [ { value: 'steel', label: 'Steel' }, { value: 'wood', label: 'Wood' }, ]; const PROPULSION_METHODS = [ { value: 'chain_lift', label: 'Chain Lift' }, { value: 'cable_lift', label: 'Cable Lift' }, { value: 'friction_wheel_lift', label: 'Friction Wheel Lift' }, { value: 'lsm_launch', label: 'LSM Launch' }, { value: 'lim_launch', label: 'LIM Launch' }, { value: 'hydraulic_launch', label: 'Hydraulic Launch' }, { value: 'compressed_air_launch', label: 'Compressed Air Launch' }, { value: 'flywheel_launch', label: 'Flywheel Launch' }, { value: 'gravity', label: 'Gravity' }, { value: 'tire_drive', label: 'Tire Drive' }, { value: 'water_propulsion', label: 'Water Propulsion' }, { value: 'other', label: 'Other' }, ]; // Status value mappings between display (form) and database values const STATUS_DISPLAY_TO_DB: Record = { 'Operating': 'operating', 'Closed Temporarily': 'closed_temporarily', 'Closed Permanently': 'closed_permanently', 'Under Construction': 'under_construction', 'Relocated': 'relocated', 'Stored': 'stored', 'Demolished': 'demolished' }; const STATUS_DB_TO_DISPLAY: Record = { 'operating': 'Operating', 'closed_permanently': 'Closed Permanently', 'closed_temporarily': 'Closed Temporarily', 'under_construction': 'Under Construction', 'relocated': 'Relocated', 'stored': 'Stored', 'demolished': 'Demolished' }; export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: RideFormProps) { const { isModerator } = useUserRole(); const { preferences } = useUnitPreferences(); const measurementSystem = preferences.measurement_system; const [isSubmitting, setIsSubmitting] = useState(false); // Validate that onSubmit uses submission helpers (dev mode only) useEffect(() => { validateSubmissionHandler(onSubmit, 'ride'); }, [onSubmit]); // Temp entity states const [tempNewPark, setTempNewPark] = useState(initialData?._tempNewPark || null); const [selectedManufacturerId, setSelectedManufacturerId] = useState( initialData?.manufacturer_id || '' ); const [selectedManufacturerName, setSelectedManufacturerName] = useState(''); const [tempNewManufacturer, setTempNewManufacturer] = useState(initialData?._tempNewManufacturer || null); const [tempNewDesigner, setTempNewDesigner] = useState(initialData?._tempNewDesigner || null); const [tempNewRideModel, setTempNewRideModel] = useState(initialData?._tempNewRideModel || null); const [isParkModalOpen, setIsParkModalOpen] = useState(false); const [isManufacturerModalOpen, setIsManufacturerModalOpen] = useState(false); const [isDesignerModalOpen, setIsDesignerModalOpen] = useState(false); const [isModelModalOpen, setIsModelModalOpen] = useState(false); // Advanced editor state - using simplified interface for editors (DB fields added on submit) const [technicalSpecs, setTechnicalSpecs] = useState<{ spec_name: string; spec_value: string; spec_type: 'string' | 'number' | 'boolean' | 'date'; category?: string; unit?: string; display_order: number; }[]>([]); const [coasterStats, setCoasterStats] = useState<{ stat_name: string; stat_value: number; unit?: string; category?: string; description?: string; display_order: number; }[]>([]); const [formerNames, setFormerNames] = useState<{ former_name: string; date_changed?: Date | null; reason?: string; from_year?: number; to_year?: number; order_index: number; }[]>([]); // Fetch data const { manufacturers, loading: manufacturersLoading } = useManufacturers(); const { rideModels, loading: modelsLoading } = useRideModels(selectedManufacturerId); const { parks, loading: parksLoading } = useParks(); const { register, handleSubmit, setValue, watch, trigger, formState: { errors } } = useForm({ resolver: zodResolver(entitySchemas.ride), defaultValues: { name: initialData?.name || '', slug: initialData?.slug || '', description: initialData?.description || '', category: initialData?.category || '', ride_sub_type: initialData?.ride_sub_type || '', status: initialData?.status || 'operating' as const, // Store DB value directly opening_date: initialData?.opening_date || undefined, opening_date_precision: initialData?.opening_date_precision || 'day', closing_date: initialData?.closing_date || undefined, closing_date_precision: initialData?.closing_date_precision || 'day', // Convert metric values to user's preferred unit for display height_requirement: initialData?.height_requirement ? convertValueFromMetric(initialData.height_requirement, getDisplayUnit('cm', measurementSystem), 'cm') : undefined, age_requirement: initialData?.age_requirement || undefined, capacity_per_hour: initialData?.capacity_per_hour || undefined, duration_seconds: initialData?.duration_seconds || undefined, max_speed_kmh: initialData?.max_speed_kmh ? convertValueFromMetric(initialData.max_speed_kmh, getDisplayUnit('km/h', measurementSystem), 'km/h') : undefined, max_height_meters: initialData?.max_height_meters ? convertValueFromMetric(initialData.max_height_meters, getDisplayUnit('m', measurementSystem), 'm') : undefined, length_meters: initialData?.length_meters ? convertValueFromMetric(initialData.length_meters, getDisplayUnit('m', measurementSystem), 'm') : undefined, inversions: initialData?.inversions || undefined, coaster_type: initialData?.coaster_type || undefined, seating_type: initialData?.seating_type || undefined, intensity_level: initialData?.intensity_level || undefined, drop_height_meters: initialData?.drop_height_meters ? convertValueFromMetric(initialData.drop_height_meters, getDisplayUnit('m', measurementSystem), 'm') : undefined, max_g_force: initialData?.max_g_force || undefined, manufacturer_id: initialData?.manufacturer_id || undefined, ride_model_id: initialData?.ride_model_id || undefined, source_url: initialData?.source_url || '', submission_notes: initialData?.submission_notes || '', images: { uploaded: [] }, park_id: initialData?.park_id || undefined } }); const selectedCategory = watch('category'); const isParkPreselected = !!initialData?.park_id; // Coming from park detail page const handleFormSubmit = async (data: RideFormData) => { setIsSubmitting(true); try { // Pre-submission validation for required fields const { valid, errors: validationErrors } = validateRequiredFields('ride', data); if (!valid) { validationErrors.forEach(error => { toast({ variant: 'destructive', title: 'Missing Required Fields', description: error }); }); setIsSubmitting(false); return; } // CRITICAL: Block new photo uploads on edits if (isEditing && data.images?.uploaded) { const hasNewPhotos = data.images.uploaded.some(img => img.isLocal); if (hasNewPhotos) { toast({ variant: 'destructive', title: 'Validation Error', description: 'New photos cannot be added during edits. Please remove new photos or use the photo gallery.' }); return; } } // Validate coaster stats if (coasterStats && coasterStats.length > 0) { const statsValidation = validateCoasterStats(coasterStats); if (!statsValidation.valid) { toast({ title: 'Invalid coaster statistics', description: statsValidation.errors.join(', '), variant: 'destructive' }); return; } } // Validate technical specs if (technicalSpecs && technicalSpecs.length > 0) { const specsValidation = validateTechnicalSpecs(technicalSpecs); if (!specsValidation.valid) { toast({ title: 'Invalid technical specifications', description: specsValidation.errors.join(', '), variant: 'destructive' }); return; } } // Convert form values back to metric for storage const metricData = { ...data, // Status is already in DB format height_requirement: data.height_requirement ? convertValueToMetric(data.height_requirement, getDisplayUnit('cm', measurementSystem)) : undefined, max_speed_kmh: data.max_speed_kmh ? convertValueToMetric(data.max_speed_kmh, getDisplayUnit('km/h', measurementSystem)) : undefined, max_height_meters: data.max_height_meters ? convertValueToMetric(data.max_height_meters, getDisplayUnit('m', measurementSystem)) : undefined, length_meters: data.length_meters ? convertValueToMetric(data.length_meters, getDisplayUnit('m', measurementSystem)) : undefined, drop_height_meters: data.drop_height_meters ? convertValueToMetric(data.drop_height_meters, getDisplayUnit('m', measurementSystem)) : undefined, // Pass relational data for proper handling _technical_specifications: technicalSpecs, _coaster_statistics: coasterStats, _name_history: formerNames, _tempNewPark: tempNewPark || undefined, _tempNewManufacturer: tempNewManufacturer || undefined, _tempNewDesigner: tempNewDesigner || undefined, _tempNewRideModel: tempNewRideModel || undefined }; // Pass clean data to parent with extended fields await onSubmit(metricData); toast({ title: isEditing ? "Ride Updated" : "Submission Sent", description: isEditing ? "The ride information has been updated successfully." : tempNewManufacturer ? "Ride, manufacturer, and model submitted for review" : "Ride submitted for review" }); } catch (error: unknown) { handleError(error, { action: isEditing ? 'Update Ride' : 'Create Ride', metadata: { rideName: data.name, hasNewManufacturer: !!tempNewManufacturer, hasNewModel: !!tempNewRideModel } }); // Re-throw so parent can handle modal closing throw error; } finally { setIsSubmitting(false); } }; return ( {isEditing ? 'Edit Ride' : 'Create New Ride'}
{/* Basic Information */}
{errors.name && (

{errors.name.message}

)}
setValue('slug', slug)} isModerator={isModerator()} />
{/* Description */}