Refactor composite submission logic

This commit is contained in:
gpt-engineer-app[bot]
2025-11-02 20:29:08 +00:00
parent 6a9df807fa
commit bb951e637f
6 changed files with 290 additions and 23 deletions

View File

@@ -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<typeof entitySchemas.ride>;
interface RideFormProps {
onSubmit: (data: RideFormData) => Promise<void>;
onSubmit: (data: RideFormData & {
_tempNewPark?: TempParkData;
_tempNewManufacturer?: TempCompanyData;
_tempNewDesigner?: TempCompanyData;
_tempNewRideModel?: TempRideModelData;
}) => Promise<void>;
onCancel?: () => void;
initialData?: Partial<RideFormData & {
id?: string;
banner_image_url?: string;
card_image_url?: string;
_tempNewPark?: TempParkData;
_tempNewManufacturer?: TempCompanyData;
_tempNewDesigner?: TempCompanyData;
_tempNewRideModel?: TempRideModelData;
}>;
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<TempParkData | null>(initialData?._tempNewPark || null);
const [selectedManufacturerId, setSelectedManufacturerId] = useState<string>(
initialData?.manufacturer_id || ''
);
const [selectedManufacturerName, setSelectedManufacturerName] = useState<string>('');
const [tempNewManufacturer, setTempNewManufacturer] = useState<TempCompanyData | null>(null);
const [tempNewRideModel, setTempNewRideModel] = useState<TempRideModelData | null>(null);
const [tempNewManufacturer, setTempNewManufacturer] = useState<TempCompanyData | null>(initialData?._tempNewManufacturer || null);
const [tempNewDesigner, setTempNewDesigner] = useState<TempCompanyData | null>(initialData?._tempNewDesigner || null);
const [tempNewRideModel, setTempNewRideModel] = useState<TempRideModelData | null>(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 }:
</div>
</form>
{/* Park Modal - Add before Manufacturer Modal */}
<Dialog open={isParkModalOpen} onOpenChange={setIsParkModalOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Park</DialogTitle>
</DialogHeader>
<ParkForm
onSubmit={async (data) => {
setTempNewPark(data as TempParkData);
setIsParkModalOpen(false);
setValue('park_id', undefined);
}}
onCancel={() => setIsParkModalOpen(false)}
/>
</DialogContent>
</Dialog>
{/* Designer Modal */}
<Dialog open={isDesignerModalOpen} onOpenChange={setIsDesignerModalOpen}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Designer</DialogTitle>
</DialogHeader>
<ManufacturerForm
initialData={tempNewDesigner}
onSubmit={(data) => {
setTempNewDesigner(data);
setIsDesignerModalOpen(false);
setValue('designer_id', undefined);
}}
onCancel={() => setIsDesignerModalOpen(false)}
/>
</DialogContent>
</Dialog>
{/* Manufacturer Modal */}
<Dialog open={isManufacturerModalOpen} onOpenChange={setIsManufacturerModalOpen}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">

View File

@@ -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 ? (
<div className="space-y-2">
<Badge variant="secondary" className="gap-2">
<Building2 className="h-3 w-3" />
New: {tempNewPark.name}
<button type="button" onClick={onRemove} className="ml-1 hover:text-destructive">×</button>
</Badge>
<Button type="button" variant="outline" size="sm" onClick={onEdit}>Edit New Park</Button>
</div>
) : (
<Button type="button" variant="outline" size="sm" onClick={onCreateNew}>
<Plus className="h-4 w-4 mr-2" />Create New Park
</Button>
);
}
export function RideDesignerSelector({ tempNewDesigner, onCreateNew, onEdit, onRemove }: DesignerSelectorProps) {
return tempNewDesigner ? (
<div className="space-y-2">
<Badge variant="secondary" className="gap-2">
<Building2 className="h-3 w-3" />
New: {tempNewDesigner.name}
<button type="button" onClick={onRemove} className="ml-1 hover:text-destructive">×</button>
</Badge>
<Button type="button" variant="outline" size="sm" onClick={onEdit}>Edit New Designer</Button>
</div>
) : (
<Button type="button" variant="outline" size="sm" onClick={onCreateNew}>
<Plus className="h-4 w-4 mr-2" />Create New Designer
</Button>
);
}

View File

@@ -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
);

View File

@@ -921,6 +921,17 @@ function resolveDependencies(data: any, dependencyMap: Map<string, string>, 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<string, string>, 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<string, string>, 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])) {

View File

@@ -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;

View File

@@ -790,6 +790,41 @@ function resolveDependencies(data: any, dependencyMap: Map<string, string>, 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) {