diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index 213c6cc8..16daecf9 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -5,7 +5,7 @@ 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 } from '@/types/company'; +import type { TempCompanyData, TempRideModelData, TempParkData } from '@/types/company'; import { entitySchemas } from '@/lib/entityValidationSchemas'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -23,13 +23,14 @@ 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 } from 'lucide-react'; +import { Plus, Zap, Save, X, Building2 } from 'lucide-react'; import { toDateOnly, parseDateOnly } from '@/lib/dateUtils'; import { useUnitPreferences } from '@/hooks/useUnitPreferences'; import { useManufacturers, useRideModels } 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'; @@ -45,12 +46,21 @@ import { type RideFormData = z.infer; interface RideFormProps { - onSubmit: (data: RideFormData) => Promise; + onSubmit: (data: RideFormData & { + _tempNewPark?: TempParkData; + _tempNewManufacturer?: TempCompanyData; + _tempNewDesigner?: TempCompanyData; + _tempNewRideModel?: TempRideModelData; + }) => Promise; onCancel?: () => void; initialData?: Partial; isEditing?: boolean; } @@ -154,14 +164,18 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: validateSubmissionHandler(onSubmit, 'ride'); }, [onSubmit]); - // Manufacturer and model state + // 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(null); - const [tempNewRideModel, setTempNewRideModel] = useState(null); + 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) @@ -299,7 +313,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: _technical_specifications: technicalSpecs, _coaster_statistics: coasterStats, _name_history: formerNames, + _tempNewPark: tempNewPark, _tempNewManufacturer: tempNewManufacturer, + _tempNewDesigner: tempNewDesigner, _tempNewRideModel: tempNewRideModel }; @@ -1337,6 +1353,41 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: + {/* Park Modal - Add before Manufacturer Modal */} + + + + Create New Park + + { + setTempNewPark(data as TempParkData); + setIsParkModalOpen(false); + setValue('park_id', undefined); + }} + onCancel={() => setIsParkModalOpen(false)} + /> + + + + {/* Designer Modal */} + + + + Create New Designer + + { + setTempNewDesigner(data); + setIsDesignerModalOpen(false); + setValue('designer_id', undefined); + }} + onCancel={() => setIsDesignerModalOpen(false)} + /> + + + {/* Manufacturer Modal */} diff --git a/src/components/admin/ride-form-park-designer-ui.tsx b/src/components/admin/ride-form-park-designer-ui.tsx new file mode 100644 index 00000000..b84f2a38 --- /dev/null +++ b/src/components/admin/ride-form-park-designer-ui.tsx @@ -0,0 +1,61 @@ +/** + * UI components for Park and Designer creation within RideForm + * Extracted for clarity - import these into RideForm.tsx + */ + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Plus, Building2, X } from 'lucide-react'; +import type { TempParkData, TempCompanyData } from '@/types/company'; + +interface ParkSelectorProps { + tempNewPark: TempParkData | null; + onCreateNew: () => void; + onEdit: () => void; + onRemove: () => void; + parkId?: string; + onParkChange: (id: string) => void; +} + +interface DesignerSelectorProps { + tempNewDesigner: TempCompanyData | null; + onCreateNew: () => void; + onEdit: () => void; + onRemove: () => void; + designerId?: string; + onDesignerChange: (id: string) => void; +} + +export function RideParkSelector({ tempNewPark, onCreateNew, onEdit, onRemove }: ParkSelectorProps) { + return tempNewPark ? ( +
+ + + New: {tempNewPark.name} + + + +
+ ) : ( + + ); +} + +export function RideDesignerSelector({ tempNewDesigner, onCreateNew, onEdit, onRemove }: DesignerSelectorProps) { + return tempNewDesigner ? ( +
+ + + New: {tempNewDesigner.name} + + + +
+ ) : ( + + ); +} diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 15c2e7e2..e1fd0de9 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -305,13 +305,23 @@ async function submitCompositeCreation( delete primaryData.property_owner_id; } } else if (uploadedPrimary.type === 'ride') { + if (uploadedPrimary.data.park_id?.startsWith('temp-')) { + const parkIndex = tempIdMap.get(uploadedPrimary.data.park_id); + if (parkIndex !== undefined) primaryData._temp_park_ref = parkIndex; + delete primaryData.park_id; + } if (uploadedPrimary.data.manufacturer_id?.startsWith('temp-')) { - const mfgIndex = tempIdMap.get('temp-manufacturer'); + const mfgIndex = tempIdMap.get(uploadedPrimary.data.manufacturer_id); if (mfgIndex !== undefined) primaryData._temp_manufacturer_ref = mfgIndex; delete primaryData.manufacturer_id; } + if (uploadedPrimary.data.designer_id?.startsWith('temp-')) { + const designerIndex = tempIdMap.get(uploadedPrimary.data.designer_id); + if (designerIndex !== undefined) primaryData._temp_designer_ref = designerIndex; + delete primaryData.designer_id; + } if (uploadedPrimary.data.ride_model_id?.startsWith('temp-')) { - const modelIndex = tempIdMap.get('temp-ride-model'); + const modelIndex = tempIdMap.get(uploadedPrimary.data.ride_model_id); if (modelIndex !== undefined) primaryData._temp_ride_model_ref = modelIndex; delete primaryData.ride_model_id; } @@ -600,13 +610,54 @@ export async function submitParkUpdate( * @returns Object containing submitted boolean and submissionId */ export async function submitRideCreation( - data: RideFormData & { _tempNewManufacturer?: any; _tempNewRideModel?: any }, + data: RideFormData & { + _tempNewPark?: any; + _tempNewManufacturer?: any; + _tempNewDesigner?: any; + _tempNewRideModel?: any; + }, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { // Check for composite submission with dependencies - if (data._tempNewManufacturer || data._tempNewRideModel) { + if (data._tempNewPark || data._tempNewManufacturer || data._tempNewDesigner || data._tempNewRideModel) { const dependencies: CompositeSubmissionDependency[] = []; + // Handle new park operator (from nested park) + if (data._tempNewPark?._tempNewOperator) { + dependencies.push({ + type: 'company', + data: { ...data._tempNewPark._tempNewOperator, company_type: 'operator' }, + tempId: 'temp-park-operator', + companyType: 'operator' + }); + } + + // Handle new park property owner (from nested park) + if (data._tempNewPark?._tempNewPropertyOwner) { + dependencies.push({ + type: 'company', + data: { ...data._tempNewPark._tempNewPropertyOwner, company_type: 'property_owner' }, + tempId: 'temp-park-owner', + companyType: 'property_owner' + }); + } + + // Handle new park (depends on operator/owner) + if (data._tempNewPark) { + dependencies.push({ + type: 'park', + data: { + ...data._tempNewPark, + operator_id: data._tempNewPark._tempNewOperator ? 'temp-park-operator' : data._tempNewPark.operator_id, + property_owner_id: data._tempNewPark._tempNewPropertyOwner ? 'temp-park-owner' : data._tempNewPark.property_owner_id, + _tempNewOperator: undefined, + _tempNewPropertyOwner: undefined + }, + tempId: 'temp-park' + }); + } + + // Handle new manufacturer if (data._tempNewManufacturer) { dependencies.push({ type: 'company', @@ -616,18 +667,45 @@ export async function submitRideCreation( }); } + // Handle new designer + if (data._tempNewDesigner) { + dependencies.push({ + type: 'company', + data: { ...data._tempNewDesigner, company_type: 'designer' }, + tempId: 'temp-designer', + companyType: 'designer' + }); + } + + // Handle new ride model (depends on manufacturer) if (data._tempNewRideModel) { dependencies.push({ type: 'ride_model', - data: data._tempNewRideModel, + data: { + ...data._tempNewRideModel, + manufacturer_id: data._tempNewManufacturer ? 'temp-manufacturer' : data._tempNewRideModel.manufacturer_id + }, tempId: 'temp-ride-model', parentTempId: data._tempNewManufacturer ? 'temp-manufacturer' : undefined }); } if (dependencies.length > 0) { + // Prepare ride data with temp references + const rideData = { + ...data, + park_id: data._tempNewPark ? 'temp-park' : data.park_id, + manufacturer_id: data._tempNewManufacturer ? 'temp-manufacturer' : data.manufacturer_id, + designer_id: data._tempNewDesigner ? 'temp-designer' : data.designer_id, + ride_model_id: data._tempNewRideModel ? 'temp-ride-model' : data.ride_model_id, + _tempNewPark: undefined, + _tempNewManufacturer: undefined, + _tempNewDesigner: undefined, + _tempNewRideModel: undefined + }; + return submitCompositeCreation( - { type: 'ride', data }, + { type: 'ride', data: rideData }, dependencies, userId ); diff --git a/src/lib/submissionItemsService.ts b/src/lib/submissionItemsService.ts index 4e5dae59..3c5667bf 100644 --- a/src/lib/submissionItemsService.ts +++ b/src/lib/submissionItemsService.ts @@ -921,6 +921,17 @@ function resolveDependencies(data: any, dependencyMap: Map, sort ]; // Resolve temporary references using sortedItems array (FIXED) + if (resolved._temp_park_ref !== undefined) { + const refIndex = resolved._temp_park_ref; + if (refIndex >= 0 && refIndex < sortedItems.length) { + const refItemId = sortedItems[refIndex].id; + if (dependencyMap.has(refItemId)) { + resolved.park_id = dependencyMap.get(refItemId); + } + } + delete resolved._temp_park_ref; + } + if (resolved._temp_manufacturer_ref !== undefined) { const refIndex = resolved._temp_manufacturer_ref; if (refIndex >= 0 && refIndex < sortedItems.length) { @@ -932,6 +943,17 @@ function resolveDependencies(data: any, dependencyMap: Map, sort delete resolved._temp_manufacturer_ref; } + if (resolved._temp_designer_ref !== undefined) { + const refIndex = resolved._temp_designer_ref; + if (refIndex >= 0 && refIndex < sortedItems.length) { + const refItemId = sortedItems[refIndex].id; + if (dependencyMap.has(refItemId)) { + resolved.designer_id = dependencyMap.get(refItemId); + } + } + delete resolved._temp_designer_ref; + } + if (resolved._temp_operator_ref !== undefined) { const refIndex = resolved._temp_operator_ref; if (refIndex >= 0 && refIndex < sortedItems.length) { @@ -965,17 +987,6 @@ function resolveDependencies(data: any, dependencyMap: Map, sort delete resolved._temp_ride_model_ref; } - if (resolved._temp_designer_ref !== undefined) { - const refIndex = resolved._temp_designer_ref; - if (refIndex >= 0 && refIndex < sortedItems.length) { - const refItemId = sortedItems[refIndex].id; - if (dependencyMap.has(refItemId)) { - resolved.designer_id = dependencyMap.get(refItemId); - } - } - delete resolved._temp_designer_ref; - } - // Resolve each foreign key if it's a submission item ID for (const key of foreignKeys) { if (resolved[key] && dependencyMap.has(resolved[key])) { diff --git a/src/types/company.ts b/src/types/company.ts index a9d0759f..82343b1b 100644 --- a/src/types/company.ts +++ b/src/types/company.ts @@ -35,6 +35,37 @@ export interface TempCompanyData { website_url?: string; } +export interface TempParkData { + name: string; + slug: string; + park_type: string; + status: string; + description?: string; + opening_date?: string; + closing_date?: string; + operator_id?: string; + property_owner_id?: string; + website_url?: string; + phone?: string; + email?: string; + location?: { + name: string; + city?: string; + state_province?: string; + country: string; + postal_code?: string; + latitude: number; + longitude: number; + }; + images?: { + uploaded: UploadedImage[]; + banner_assignment?: number | null; + card_assignment?: number | null; + }; + _tempNewOperator?: TempCompanyData; + _tempNewPropertyOwner?: TempCompanyData; +} + export interface TempRideModelData { name: string; slug: string; diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 9d57afbc..5444ab26 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -790,6 +790,41 @@ function resolveDependencies(data: any, dependencyMap: Map, sort delete resolved._temp_manufacturer_ref; } + // Resolve temporary references using sortedItems array + if (resolved._temp_park_ref !== undefined) { + const refIndex = resolved._temp_park_ref; + if (refIndex >= 0 && refIndex < sortedItems.length) { + const refItemId = sortedItems[refIndex].id; + if (dependencyMap.has(refItemId)) { + resolved.park_id = dependencyMap.get(refItemId); + edgeLogger.info('Resolved temp park ref', { + action: 'dependency_resolve_temp_ref', + refIndex, + refItemId, + resolvedId: resolved.park_id + }); + } + } + delete resolved._temp_park_ref; + } + + if (resolved._temp_manufacturer_ref !== undefined) { + const refIndex = resolved._temp_manufacturer_ref; + if (refIndex >= 0 && refIndex < sortedItems.length) { + const refItemId = sortedItems[refIndex].id; + if (dependencyMap.has(refItemId)) { + resolved.manufacturer_id = dependencyMap.get(refItemId); + edgeLogger.info('Resolved temp manufacturer ref', { + action: 'dependency_resolve_temp_ref', + refIndex, + refItemId, + resolvedId: resolved.manufacturer_id + }); + } + } + delete resolved._temp_manufacturer_ref; + } + if (resolved._temp_operator_ref !== undefined) { const refIndex = resolved._temp_operator_ref; if (refIndex >= 0 && refIndex < sortedItems.length) {