import { supabase } from '@/integrations/supabase/client'; import type { Json } from '@/integrations/supabase/types'; import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { uploadPendingImages } from './imageUploadHelper'; /** * ═══════════════════════════════════════════════════════════════════ * SUBMISSION PATTERN STANDARD - CRITICAL PROJECT RULE * ═══════════════════════════════════════════════════════════════════ * * ⚠️ NEVER STORE JSON IN SQL COLUMNS ⚠️ * * content_submissions.content should ONLY contain: * ✅ action: 'create' | 'edit' | 'delete' * ✅ Minimal reference IDs (entity_id, parent_id, etc.) - MAX 3 fields * ❌ NO actual form data * ❌ NO submission content * ❌ NO large objects * * ALL actual data MUST go in: * ✅ submission_items.item_data (new data) * ✅ submission_items.original_data (for edits) * ✅ Specialized relational tables: * - photo_submissions + photo_submission_items * - park_submissions * - ride_submissions * - company_submissions * - ride_model_submissions * * If your data is relational, model it relationally. * JSON blobs destroy: * - Queryability (can't filter/join) * - Performance (slower, larger) * - Data integrity (no constraints) * - Maintainability (impossible to refactor) * * EXAMPLES: * * ✅ CORRECT: * content: { action: 'create' } * content: { action: 'edit', park_id: uuid } * content: { action: 'delete', photo_id: uuid } * * ❌ WRONG: * content: { name: '...', description: '...', ...formData } * content: { photos: [...], metadata: {...} } * content: data // entire object dump * * ═══════════════════════════════════════════════════════════════════ */ export interface ParkFormData { name: string; slug: string; description?: string; park_type: string; status: string; opening_date?: string; closing_date?: string; website_url?: string; phone?: string; email?: string; operator_id?: string; property_owner_id?: string; // Location can be stored as object for new submissions or ID for editing location?: { name: string; city?: string; state_province?: string; country: string; postal_code?: string; latitude: number; longitude: number; timezone?: string; display_name: string; }; location_id?: string; images?: ImageAssignments; banner_image_url?: string; banner_image_id?: string; card_image_url?: string; card_image_id?: string; } export interface RideFormData { name: string; slug: string; description?: string; category: string; status: string; park_id: string; manufacturer_id?: string; designer_id?: string; ride_model_id?: string; opening_date?: string; closing_date?: string; max_speed_kmh?: number; max_height_meters?: number; length_meters?: number; duration_seconds?: number; capacity_per_hour?: number; height_requirement?: number; age_requirement?: number; inversions?: number; drop_height_meters?: number; max_g_force?: number; intensity_level?: string; coaster_type?: string; seating_type?: string; ride_sub_type?: string; images?: ImageAssignments; banner_image_url?: string; banner_image_id?: string; card_image_url?: string; card_image_id?: string; } export interface CompanyFormData { name: string; slug: string; description?: string; person_type: 'company' | 'individual' | 'firm' | 'organization'; founded_year?: number; founded_date?: string; founded_date_precision?: string; headquarters_location?: string; website_url?: string; images?: ImageAssignments; banner_image_url?: string; banner_image_id?: string; card_image_url?: string; card_image_id?: string; } /** * ⚠️ CRITICAL SECURITY PATTERN ⚠️ * * Submits a new park for creation through the moderation queue. * This is the ONLY correct way to create parks. * * DO NOT use direct database inserts: * ❌ await supabase.from('parks').insert(data) // BYPASSES MODERATION! * ✅ await submitParkCreation(data, userId) // CORRECT * * Flow: User Submit → Moderation Queue → Approval → Versioning → Live * * Even moderators/admins must use this function to ensure proper versioning and audit trails. * * @see docs/SUBMISSION_FLOW.md for complete documentation * @param data - The park form data to submit * @param userId - The ID of the user submitting the park * @returns Object containing submitted boolean and submissionId */ export async function submitParkCreation( data: ParkFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images for park creation:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } // Create the main submission record const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'park', content: { action: 'create' }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; // Create the submission item with actual park data const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'park', item_data: { ...data, images: processedImages as unknown as Json }, status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } /** * ⚠️ CRITICAL SECURITY PATTERN ⚠️ * * Submits an update to an existing park through the moderation queue. * This is the ONLY correct way to update parks. * * DO NOT use direct database updates: * ❌ await supabase.from('parks').update(data) // BYPASSES MODERATION! * ✅ await submitParkUpdate(parkId, data, userId) // CORRECT * * Flow: User Submit → Moderation Queue → Approval → Versioning → Live * * Even moderators/admins must use this function to ensure proper versioning and audit trails. * * @see docs/SUBMISSION_FLOW.md for complete documentation * @param parkId - The ID of the park to update * @param data - The updated park form data * @param userId - The ID of the user submitting the update * @returns Object containing submitted boolean and submissionId */ export async function submitParkUpdate( parkId: string, data: ParkFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { // Fetch existing park data first const { data: existingPark, error: fetchError } = await supabase .from('parks') .select('*') .eq('id', parkId) .single(); if (fetchError) throw new Error(`Failed to fetch park: ${fetchError.message}`); if (!existingPark) throw new Error('Park not found'); // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images for park update:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } // Create the main submission record const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'park', content: { action: 'edit', park_id: parkId }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; // Create the submission item with actual park data AND original data const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'park', item_data: { ...data, park_id: parkId, images: processedImages as any }, original_data: JSON.parse(JSON.stringify(existingPark)), status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } /** * ⚠️ CRITICAL SECURITY PATTERN ⚠️ * * Submits a new ride for creation through the moderation queue. * This is the ONLY correct way to create rides. * * DO NOT use direct database inserts: * ❌ await supabase.from('rides').insert(data) // BYPASSES MODERATION! * ✅ await submitRideCreation(data, userId) // CORRECT * * Flow: User Submit → Moderation Queue → Approval → Versioning → Live * * Even moderators/admins must use this function to ensure proper versioning and audit trails. * * @see docs/SUBMISSION_FLOW.md for complete documentation * @param data - The ride form data to submit * @param userId - The ID of the user submitting the ride * @returns Object containing submitted boolean and submissionId */ export async function submitRideCreation( data: RideFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images for ride creation:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } // Create the main submission record const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'ride', content: { action: 'create' }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; // Create the submission item with actual ride data const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'ride', item_data: { ...data, images: processedImages as unknown as Json }, status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } /** * ⚠️ CRITICAL SECURITY PATTERN ⚠️ * * Submits an update to an existing ride through the moderation queue. * This is the ONLY correct way to update rides. * * DO NOT use direct database updates: * ❌ await supabase.from('rides').update(data) // BYPASSES MODERATION! * ✅ await submitRideUpdate(rideId, data, userId) // CORRECT * * Flow: User Submit → Moderation Queue → Approval → Versioning → Live * * Even moderators/admins must use this function to ensure proper versioning and audit trails. * * @see docs/SUBMISSION_FLOW.md for complete documentation * @param rideId - The ID of the ride to update * @param data - The updated ride form data * @param userId - The ID of the user submitting the update * @returns Object containing submitted boolean and submissionId */ export async function submitRideUpdate( rideId: string, data: RideFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { // Fetch existing ride data first const { data: existingRide, error: fetchError } = await supabase .from('rides') .select('*') .eq('id', rideId) .single(); if (fetchError) throw new Error(`Failed to fetch ride: ${fetchError.message}`); if (!existingRide) throw new Error('Ride not found'); // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images for ride update:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } // Create the main submission record const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'ride', content: { action: 'edit', ride_id: rideId }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; // Create the submission item with actual ride data AND original data const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'ride', item_data: { ...data, ride_id: rideId, images: processedImages as any }, original_data: JSON.parse(JSON.stringify(existingRide)), status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } /** * ⚠️ CRITICAL SECURITY PATTERN ⚠️ * * Submits a new manufacturer for creation through the moderation queue. */ export async function submitManufacturerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'manufacturer', content: { action: 'create' }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'manufacturer', item_data: { ...data, company_type: 'manufacturer', images: processedImages as unknown as Json }, status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } export async function submitManufacturerUpdate( companyId: string, data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') .eq('id', companyId) .single(); if (fetchError) throw new Error(`Failed to fetch manufacturer: ${fetchError.message}`); if (!existingCompany) throw new Error('Manufacturer not found'); let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'manufacturer', content: { action: 'edit', company_id: companyId }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'manufacturer', item_data: { ...data, company_id: companyId, company_type: 'manufacturer', images: processedImages as any }, original_data: JSON.parse(JSON.stringify(existingCompany)), status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } export async function submitDesignerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'designer', content: { action: 'create' }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'designer', item_data: { ...data, company_type: 'designer', images: processedImages as unknown as Json }, status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } export async function submitDesignerUpdate( companyId: string, data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') .eq('id', companyId) .single(); if (fetchError) throw new Error(`Failed to fetch designer: ${fetchError.message}`); if (!existingCompany) throw new Error('Designer not found'); let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'designer', content: { action: 'edit', company_id: companyId }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'designer', item_data: { ...data, company_id: companyId, company_type: 'designer', images: processedImages as any }, original_data: JSON.parse(JSON.stringify(existingCompany)), status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } export async function submitOperatorCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'operator', content: { action: 'create' }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'operator', item_data: { ...data, company_type: 'operator', images: processedImages as unknown as Json }, status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } export async function submitOperatorUpdate( companyId: string, data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') .eq('id', companyId) .single(); if (fetchError) throw new Error(`Failed to fetch operator: ${fetchError.message}`); if (!existingCompany) throw new Error('Operator not found'); let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'operator', content: { action: 'edit', company_id: companyId }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'operator', item_data: { ...data, company_id: companyId, company_type: 'operator', images: processedImages as any }, original_data: JSON.parse(JSON.stringify(existingCompany)), status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } export async function submitPropertyOwnerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'property_owner', content: { action: 'create' }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'property_owner', item_data: { ...data, company_type: 'property_owner', images: processedImages as unknown as Json }, status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; } export async function submitPropertyOwnerUpdate( companyId: string, data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') .eq('id', companyId) .single(); if (fetchError) throw new Error(`Failed to fetch property owner: ${fetchError.message}`); if (!existingCompany) throw new Error('Property owner not found'); let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error) { console.error('Failed to upload images:', error); throw new Error('Failed to upload images. Please check your connection and try again.'); } } const { data: submissionData, error: submissionError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'property_owner', content: { action: 'edit', company_id: companyId }, status: 'pending' }) .select() .single(); if (submissionError) throw submissionError; const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: 'property_owner', item_data: { ...data, company_id: companyId, company_type: 'property_owner', images: processedImages as any }, original_data: JSON.parse(JSON.stringify(existingCompany)), status: 'pending', order_index: 0 }); if (itemError) throw itemError; return { submitted: true, submissionId: submissionData.id }; }