Files
thrilltrack-explorer/src/lib/entitySubmissionHelpers.ts
2025-10-03 16:51:47 +00:00

342 lines
9.2 KiB
TypeScript

import { supabase } from '@/integrations/supabase/client';
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 async function submitParkCreation(
data: ParkFormData,
userId: string
) {
// Upload any pending local images first
let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded);
processedImages = {
...data.images,
uploaded: uploadedImages
};
}
// 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 any
},
status: 'pending',
order_index: 0
});
if (itemError) throw itemError;
return { submitted: true, submissionId: submissionData.id };
}
export async function submitParkUpdate(
parkId: string,
data: ParkFormData,
userId: 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) {
const uploadedImages = await uploadPendingImages(data.images.uploaded);
processedImages = {
...data.images,
uploaded: uploadedImages
};
}
// 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 };
}
export async function submitRideCreation(
data: RideFormData,
userId: string
) {
// Upload any pending local images first
let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded);
processedImages = {
...data.images,
uploaded: uploadedImages
};
}
// 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 any
},
status: 'pending',
order_index: 0
});
if (itemError) throw itemError;
return { submitted: true, submissionId: submissionData.id };
}
export async function submitRideUpdate(
rideId: string,
data: RideFormData,
userId: 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) {
const uploadedImages = await uploadPendingImages(data.images.uploaded);
processedImages = {
...data.images,
uploaded: uploadedImages
};
}
// 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 };
}