mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 15:31:12 -05:00
342 lines
9.2 KiB
TypeScript
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 };
|
|
}
|