diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index abf06fe2..9d3b9a55 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -91,7 +91,9 @@ export interface ParkFormData { park_type: string; status: string; opening_date?: string; + opening_date_precision?: string; closing_date?: string; + closing_date_precision?: string; website_url?: string; phone?: string; email?: string; @@ -131,7 +133,9 @@ export interface RideFormData { designer_id?: string; ride_model_id?: string; opening_date?: string; + opening_date_precision?: string; closing_date?: string; + closing_date_precision?: string; max_speed_kmh?: number; max_height_meters?: number; length_meters?: number; @@ -890,21 +894,72 @@ export async function submitParkUpdate( if (submissionError) throw submissionError; - // Create the submission item with actual park data AND original data + // Extract changed fields + const changedFields = extractChangedFields(data, existingPark as any); + + // Handle location data properly + let tempLocationData: any = null; + if (data.location) { + tempLocationData = { + name: data.location.name, + street_address: data.location.street_address || null, + city: data.location.city || null, + state_province: data.location.state_province || null, + country: data.location.country, + latitude: data.location.latitude, + longitude: data.location.longitude, + timezone: data.location.timezone || null, + postal_code: data.location.postal_code || null, + display_name: data.location.display_name + }; + } + + // ✅ FIXED: Insert into park_submissions table (relational pattern) + const { data: parkSubmission, error: parkSubmissionError } = await supabase + .from('park_submissions') + .insert({ + submission_id: submissionData.id, + name: changedFields.name ?? existingPark.name, + slug: changedFields.slug ?? existingPark.slug, + description: changedFields.description !== undefined ? changedFields.description : existingPark.description, + park_type: changedFields.park_type ?? existingPark.park_type, + status: changedFields.status ?? existingPark.status, + opening_date: changedFields.opening_date !== undefined ? changedFields.opening_date : existingPark.opening_date, + opening_date_precision: changedFields.opening_date_precision !== undefined ? changedFields.opening_date_precision : existingPark.opening_date_precision, + closing_date: changedFields.closing_date !== undefined ? changedFields.closing_date : existingPark.closing_date, + closing_date_precision: changedFields.closing_date_precision !== undefined ? changedFields.closing_date_precision : existingPark.closing_date_precision, + website_url: changedFields.website_url !== undefined ? changedFields.website_url : existingPark.website_url, + phone: changedFields.phone !== undefined ? changedFields.phone : existingPark.phone, + email: changedFields.email !== undefined ? changedFields.email : existingPark.email, + operator_id: changedFields.operator_id !== undefined ? changedFields.operator_id : existingPark.operator_id, + property_owner_id: changedFields.property_owner_id !== undefined ? changedFields.property_owner_id : existingPark.property_owner_id, + location_id: changedFields.location_id !== undefined ? changedFields.location_id : existingPark.location_id, + temp_location_data: tempLocationData, + banner_image_url: changedFields.banner_image_url !== undefined ? changedFields.banner_image_url : existingPark.banner_image_url, + banner_image_id: changedFields.banner_image_id !== undefined ? changedFields.banner_image_id : existingPark.banner_image_id, + card_image_url: changedFields.card_image_url !== undefined ? changedFields.card_image_url : existingPark.card_image_url, + card_image_id: changedFields.card_image_id !== undefined ? changedFields.card_image_id : existingPark.card_image_id, + }) + .select('id') + .single(); + + if (parkSubmissionError) throw parkSubmissionError; + + // ✅ Create submission_items referencing park_submission (no JSON data) const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'park', action_type: 'edit', - item_data: JSON.parse(JSON.stringify({ - ...extractChangedFields(data, existingPark as any), - park_id: parkId, // Always include for relational integrity - images: processedImages - })) as Json, + item_data: { + park_id: parkId, // Only reference IDs + images: processedImages as unknown as Json + }, original_data: JSON.parse(JSON.stringify(existingPark)), status: 'pending' as const, - order_index: 0 + order_index: 0, + park_submission_id: parkSubmission.id }); if (itemError) throw itemError; @@ -1440,7 +1495,52 @@ export async function submitRideUpdate( if (submissionError) throw submissionError; - // Create the submission item with actual ride data AND original data + // Extract changed fields + const changedFields = extractChangedFields(data, existingRide as any); + + // ✅ FIXED: Insert into ride_submissions table (relational pattern) + const { data: rideSubmission, error: rideSubmissionError } = await supabase + .from('ride_submissions') + .insert({ + submission_id: submissionData.id, + name: changedFields.name ?? existingRide.name, + slug: changedFields.slug ?? existingRide.slug, + description: changedFields.description !== undefined ? changedFields.description : existingRide.description, + category: changedFields.category ?? existingRide.category, + status: changedFields.status ?? existingRide.status, + park_id: changedFields.park_id !== undefined ? changedFields.park_id : existingRide.park_id, + manufacturer_id: changedFields.manufacturer_id !== undefined ? changedFields.manufacturer_id : existingRide.manufacturer_id, + designer_id: changedFields.designer_id !== undefined ? changedFields.designer_id : existingRide.designer_id, + ride_model_id: changedFields.ride_model_id !== undefined ? changedFields.ride_model_id : existingRide.ride_model_id, + opening_date: changedFields.opening_date !== undefined ? changedFields.opening_date : existingRide.opening_date, + opening_date_precision: changedFields.opening_date_precision !== undefined ? changedFields.opening_date_precision : existingRide.opening_date_precision, + closing_date: changedFields.closing_date !== undefined ? changedFields.closing_date : existingRide.closing_date, + closing_date_precision: changedFields.closing_date_precision !== undefined ? changedFields.closing_date_precision : existingRide.closing_date_precision, + max_speed_kmh: changedFields.max_speed_kmh !== undefined ? changedFields.max_speed_kmh : existingRide.max_speed_kmh, + max_height_meters: changedFields.max_height_meters !== undefined ? changedFields.max_height_meters : existingRide.max_height_meters, + length_meters: changedFields.length_meters !== undefined ? changedFields.length_meters : existingRide.length_meters, + duration_seconds: changedFields.duration_seconds !== undefined ? changedFields.duration_seconds : existingRide.duration_seconds, + capacity_per_hour: changedFields.capacity_per_hour !== undefined ? changedFields.capacity_per_hour : existingRide.capacity_per_hour, + height_requirement: changedFields.height_requirement !== undefined ? changedFields.height_requirement : existingRide.height_requirement, + age_requirement: changedFields.age_requirement !== undefined ? changedFields.age_requirement : existingRide.age_requirement, + inversions: changedFields.inversions !== undefined ? changedFields.inversions : existingRide.inversions, + drop_height_meters: changedFields.drop_height_meters !== undefined ? changedFields.drop_height_meters : existingRide.drop_height_meters, + max_g_force: changedFields.max_g_force !== undefined ? changedFields.max_g_force : existingRide.max_g_force, + intensity_level: changedFields.intensity_level !== undefined ? changedFields.intensity_level : existingRide.intensity_level, + coaster_type: changedFields.coaster_type !== undefined ? changedFields.coaster_type : existingRide.coaster_type, + seating_type: changedFields.seating_type !== undefined ? changedFields.seating_type : existingRide.seating_type, + ride_sub_type: changedFields.ride_sub_type !== undefined ? changedFields.ride_sub_type : existingRide.ride_sub_type, + banner_image_url: changedFields.banner_image_url !== undefined ? changedFields.banner_image_url : existingRide.banner_image_url, + banner_image_id: changedFields.banner_image_id !== undefined ? changedFields.banner_image_id : existingRide.banner_image_id, + card_image_url: changedFields.card_image_url !== undefined ? changedFields.card_image_url : existingRide.card_image_url, + card_image_id: changedFields.card_image_id !== undefined ? changedFields.card_image_id : existingRide.card_image_id, + }) + .select('id') + .single(); + + if (rideSubmissionError) throw rideSubmissionError; + + // ✅ Create submission_items referencing ride_submission (no JSON data) const { error: itemError } = await supabase .from('submission_items') .insert({ @@ -1448,13 +1548,13 @@ export async function submitRideUpdate( item_type: 'ride', action_type: 'edit', item_data: { - ...extractChangedFields(data, existingRide as any), - ride_id: rideId, // Always include for relational integrity + ride_id: rideId, // Only reference IDs images: processedImages as unknown as Json }, original_data: JSON.parse(JSON.stringify(existingRide)), status: 'pending' as const, - order_index: 0 + order_index: 0, + ride_submission_id: rideSubmission.id }); if (itemError) throw itemError; @@ -2248,58 +2348,83 @@ export async function submitTimelineEvent( throw new Error('User ID is required for timeline event submission'); } - // Create submission content (minimal reference data only) - const content: Json = { - action: 'create', - entity_type: entityType, - entity_id: entityId, - }; - // Create the main submission record - // Use atomic RPC function to create submission + items in transaction - const itemData: Record = { - entity_type: entityType, - entity_id: entityId, - event_type: data.event_type, - event_date: data.event_date.toISOString().split('T')[0], - event_date_precision: data.event_date_precision, - title: data.title, - description: data.description, - from_value: data.from_value, - to_value: data.to_value, - from_entity_id: data.from_entity_id, - to_entity_id: data.to_entity_id, - from_location_id: data.from_location_id, - to_location_id: data.to_location_id, - is_public: true, - }; + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'timeline_event', + status: 'pending' as const + }) + .select('id') + .single(); - const items = [{ - item_type: 'timeline_event', - action_type: 'create', - item_data: itemData, - order_index: 0, - }]; - - const { data: submissionId, error } = await supabase - .rpc('create_submission_with_items', { - p_user_id: userId, - p_submission_type: 'timeline_event', - p_content: content, - p_items: items as unknown as Json[], - }); - - if (error || !submissionId) { - handleError(error || new Error('No submission ID returned'), { + if (submissionError) { + handleError(submissionError, { action: 'Submit timeline event', userId, }); + throw new Error('Failed to create timeline event submission'); + } + + // ✅ FIXED: Insert into timeline_event_submissions table (relational pattern) + const { data: timelineSubmission, error: timelineSubmissionError } = await supabase + .from('timeline_event_submissions') + .insert({ + submission_id: submissionData.id, + entity_type: entityType, + entity_id: entityId, + event_type: data.event_type, + event_date: data.event_date.toISOString().split('T')[0], + event_date_precision: data.event_date_precision, + title: data.title, + description: data.description, + from_value: data.from_value, + to_value: data.to_value, + from_entity_id: data.from_entity_id, + to_entity_id: data.to_entity_id, + from_location_id: data.from_location_id, + to_location_id: data.to_location_id, + is_public: true, + }) + .select('id') + .single(); + + if (timelineSubmissionError) { + handleError(timelineSubmissionError, { + action: 'Submit timeline event data', + userId, + }); throw new Error('Failed to submit timeline event for review'); } + // ✅ Create submission_items referencing timeline_event_submission (no JSON data) + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'timeline_event', + action_type: 'create', + item_data: { + entity_type: entityType, + entity_id: entityId + } as Json, + status: 'pending' as const, + order_index: 0, + timeline_event_submission_id: timelineSubmission.id + }); + + if (itemError) { + handleError(itemError, { + action: 'Create timeline event submission item', + userId, + }); + throw new Error('Failed to link timeline event submission'); + } + return { submitted: true, - submissionId: submissionId, + submissionId: submissionData.id, }; } @@ -2332,49 +2457,85 @@ export async function submitTimelineEventUpdate( // Extract only changed fields from form data const changedFields = extractChangedFields(data, originalEvent as Partial>); - const itemData: Record = { - ...changedFields, - // Always include entity reference (for FK integrity) - entity_type: originalEvent.entity_type, - entity_id: originalEvent.entity_id, - is_public: true, - }; + // Create the main submission record + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'timeline_event', + status: 'pending' as const + }) + .select('id') + .single(); - // Use atomic RPC function to create submission and item together - const { data: result, error: rpcError } = await supabase.rpc( - 'create_submission_with_items', - { - p_user_id: userId, - p_submission_type: 'timeline_event', - p_content: { - action: 'edit', - event_id: eventId, - entity_type: originalEvent.entity_type, - } as unknown as Json, - p_items: [ - { - item_type: 'timeline_event', - action_type: 'edit', - item_data: itemData, - original_data: originalEvent, - status: 'pending' as const, - order_index: 0, - } - ] as unknown as Json[], - } - ); - - if (rpcError || !result) { - handleError(rpcError || new Error('No result returned'), { + if (submissionError) { + handleError(submissionError, { action: 'Update timeline event', metadata: { eventId }, }); + throw new Error('Failed to create timeline event update submission'); + } + + // ✅ FIXED: Insert into timeline_event_submissions table (relational pattern) + const { data: timelineSubmission, error: timelineSubmissionError } = await supabase + .from('timeline_event_submissions') + .insert({ + submission_id: submissionData.id, + entity_type: originalEvent.entity_type, + entity_id: originalEvent.entity_id, + event_type: changedFields.event_type !== undefined ? changedFields.event_type : originalEvent.event_type, + event_date: changedFields.event_date !== undefined ? (typeof changedFields.event_date === 'string' ? changedFields.event_date : changedFields.event_date.toISOString().split('T')[0]) : originalEvent.event_date, + event_date_precision: (changedFields.event_date_precision !== undefined ? changedFields.event_date_precision : originalEvent.event_date_precision) || 'day', + title: changedFields.title !== undefined ? changedFields.title : originalEvent.title, + description: changedFields.description !== undefined ? changedFields.description : originalEvent.description, + from_value: changedFields.from_value !== undefined ? changedFields.from_value : originalEvent.from_value, + to_value: changedFields.to_value !== undefined ? changedFields.to_value : originalEvent.to_value, + from_entity_id: changedFields.from_entity_id !== undefined ? changedFields.from_entity_id : originalEvent.from_entity_id, + to_entity_id: changedFields.to_entity_id !== undefined ? changedFields.to_entity_id : originalEvent.to_entity_id, + from_location_id: changedFields.from_location_id !== undefined ? changedFields.from_location_id : originalEvent.from_location_id, + to_location_id: changedFields.to_location_id !== undefined ? changedFields.to_location_id : originalEvent.to_location_id, + is_public: true, + }) + .select('id') + .single(); + + if (timelineSubmissionError) { + handleError(timelineSubmissionError, { + action: 'Update timeline event data', + metadata: { eventId }, + }); throw new Error('Failed to submit timeline event update'); } + // ✅ Create submission_items referencing timeline_event_submission (no JSON data) + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'timeline_event', + action_type: 'edit', + item_data: { + event_id: eventId, + entity_type: originalEvent.entity_type, + entity_id: originalEvent.entity_id + } as Json, + original_data: JSON.parse(JSON.stringify(originalEvent)), + status: 'pending' as const, + order_index: 0, + timeline_event_submission_id: timelineSubmission.id + }); + + if (itemError) { + handleError(itemError, { + action: 'Create timeline event update submission item', + metadata: { eventId }, + }); + throw new Error('Failed to link timeline event update submission'); + } + return { submitted: true, - submissionId: result, + submissionId: submissionData.id, }; } diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index f03aa84a..793424c4 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -1653,6 +1653,37 @@ async function createPark(supabase: any, data: any): Promise { parkId = data.park_id; delete data.park_id; // Remove ID from update data + // ✅ FIXED: Handle location updates from temp_location_data + if (data.temp_location_data && !data.location_id) { + edgeLogger.info('Creating location from temp data for update', { + action: 'approval_create_location_update', + locationName: data.temp_location_data.name + }); + + const { data: newLocation, error: locationError } = await supabase + .from('locations') + .insert({ + name: data.temp_location_data.name, + street_address: data.temp_location_data.street_address || null, + city: data.temp_location_data.city, + state_province: data.temp_location_data.state_province, + country: data.temp_location_data.country, + latitude: data.temp_location_data.latitude, + longitude: data.temp_location_data.longitude, + timezone: data.temp_location_data.timezone, + postal_code: data.temp_location_data.postal_code + }) + .select('id') + .single(); + + if (locationError) { + throw new Error(`Failed to create location: ${locationError.message}`); + } + + data.location_id = newLocation.id; + } + delete data.temp_location_data; + const normalizedData = normalizeParkTypeValue(normalizeStatusValue(data)); const sanitizedData = sanitizeDateFields(normalizedData); const filteredData = filterDatabaseFields(sanitizedData, PARK_FIELDS); @@ -1764,6 +1795,89 @@ async function createRide(supabase: any, data: any): Promise { if (error) throw new Error(`Failed to update ride: ${error.message}`); + // ✅ FIXED: Handle nested data updates (technical specs, coaster stats, name history) + // For updates, we typically replace all related data rather than merge + // Delete existing and insert new + if (technicalSpecifications.length > 0) { + // Delete existing specs + await supabase + .from('ride_technical_specifications') + .delete() + .eq('ride_id', rideId); + + // Insert new specs + const techSpecsToInsert = technicalSpecifications.map((spec: any) => ({ + ride_id: rideId, + spec_name: spec.spec_name, + spec_value: spec.spec_value, + spec_unit: spec.spec_unit || null, + category: spec.category || null, + display_order: spec.display_order || 0 + })); + + const { error: techSpecError } = await supabase + .from('ride_technical_specifications') + .insert(techSpecsToInsert); + + if (techSpecError) { + edgeLogger.error('Failed to update technical specifications', { action: 'approval_update_specs', error: techSpecError.message, rideId }); + } + } + + if (coasterStatistics.length > 0) { + // Delete existing stats + await supabase + .from('ride_coaster_stats') + .delete() + .eq('ride_id', rideId); + + // Insert new stats + const statsToInsert = coasterStatistics.map((stat: any) => ({ + ride_id: rideId, + stat_name: stat.stat_name, + stat_value: stat.stat_value, + unit: stat.unit || null, + category: stat.category || null, + description: stat.description || null, + display_order: stat.display_order || 0 + })); + + const { error: statsError } = await supabase + .from('ride_coaster_stats') + .insert(statsToInsert); + + if (statsError) { + edgeLogger.error('Failed to update coaster statistics', { action: 'approval_update_stats', error: statsError.message, rideId }); + } + } + + if (nameHistory.length > 0) { + // Delete existing name history + await supabase + .from('ride_name_history') + .delete() + .eq('ride_id', rideId); + + // Insert new name history + const namesToInsert = nameHistory.map((name: any) => ({ + ride_id: rideId, + former_name: name.former_name, + date_changed: name.date_changed || null, + reason: name.reason || null, + from_year: name.from_year || null, + to_year: name.to_year || null, + order_index: name.order_index || 0 + })); + + const { error: namesError } = await supabase + .from('ride_name_history') + .insert(namesToInsert); + + if (namesError) { + edgeLogger.error('Failed to update name history', { action: 'approval_update_names', error: namesError.message, rideId }); + } + } + // Update park ride counts after successful ride update if (parkId) { edgeLogger.info('Updating ride counts for park', { action: 'approval_update_counts', parkId });