import { supabase } from '@/integrations/supabase/client'; export interface SubmissionItemWithDeps { id: string; submission_id: string; item_type: string; item_data: any; original_data: any; status: 'pending' | 'approved' | 'rejected'; depends_on: string | null; order_index: number; approved_entity_id: string | null; rejection_reason: string | null; created_at: string; updated_at: string; dependencies?: SubmissionItemWithDeps[]; dependents?: SubmissionItemWithDeps[]; } export interface DependencyConflict { itemId: string; type: 'missing_parent' | 'rejected_parent' | 'circular_dependency'; message: string; suggestions: Array<{ action: 'link_existing' | 'cascade_reject' | 'escalate' | 'create_parent'; label: string; entityId?: string; }>; } /** * Fetch all items for a submission with their dependencies */ export async function fetchSubmissionItems(submissionId: string): Promise { const { data, error } = await supabase .from('submission_items') .select('*') .eq('submission_id', submissionId) .order('order_index', { ascending: true }); if (error) throw error; // Cast the data to the correct type return (data || []).map(item => ({ ...item, status: item.status as 'pending' | 'approved' | 'rejected', })) as SubmissionItemWithDeps[]; } /** * Build dependency tree for submission items */ export function buildDependencyTree(items: SubmissionItemWithDeps[]): SubmissionItemWithDeps[] { const itemMap = new Map(items.map(item => [item.id, { ...item, dependencies: [], dependents: [] }])); // Build relationships items.forEach(item => { if (item.depends_on) { const parent = itemMap.get(item.depends_on); const child = itemMap.get(item.id); if (parent && child) { parent.dependents = parent.dependents || []; parent.dependents.push(child); child.dependencies = child.dependencies || []; child.dependencies.push(parent); } } }); return Array.from(itemMap.values()); } /** * Detect dependency conflicts for selective approval */ export async function detectDependencyConflicts( items: SubmissionItemWithDeps[], selectedItemIds: string[] ): Promise { const conflicts: DependencyConflict[] = []; const selectedSet = new Set(selectedItemIds); for (const item of items) { // Check if parent is rejected but child is selected if (item.depends_on && selectedSet.has(item.id)) { const parent = items.find(i => i.id === item.depends_on); if (parent && (parent.status === 'rejected' || !selectedSet.has(parent.id))) { // Find existing entities that could be linked const suggestions: DependencyConflict['suggestions'] = []; // Suggest creating parent if (parent.status !== 'rejected') { suggestions.push({ action: 'create_parent', label: `Also approve ${parent.item_type}: ${parent.item_data.name}`, }); } // Suggest linking to existing entity based on type if (parent.item_type === 'park') { const { data: parks } = await supabase .from('parks') .select('id, name') .ilike('name', `%${parent.item_data.name}%`) .limit(3); parks?.forEach(park => { suggestions.push({ action: 'link_existing', label: `Link to existing park: ${park.name}`, entityId: park.id, }); }); } else if (['manufacturer', 'operator', 'property_owner', 'designer'].includes(parent.item_type)) { const { data: companies } = await supabase .from('companies') .select('id, name') .eq('company_type', parent.item_type) .ilike('name', `%${parent.item_data.name}%`) .limit(3); companies?.forEach(company => { suggestions.push({ action: 'link_existing', label: `Link to existing ${parent.item_type}: ${company.name}`, entityId: company.id, }); }); } else if (parent.item_type === 'ride_model') { const { data: models } = await supabase .from('ride_models') .select('id, name') .ilike('name', `%${parent.item_data.name}%`) .limit(3); models?.forEach(model => { suggestions.push({ action: 'link_existing', label: `Link to existing ride model: ${model.name}`, entityId: model.id, }); }); } suggestions.push({ action: 'escalate', label: 'Escalate to admin for resolution', }); conflicts.push({ itemId: item.id, type: parent.status === 'rejected' ? 'rejected_parent' : 'missing_parent', message: `Cannot approve ${item.item_type} without its parent ${parent.item_type}`, suggestions, }); } } } // Check for circular dependencies const circularDeps = detectCircularDependencies(items); circularDeps.forEach(itemId => { conflicts.push({ itemId, type: 'circular_dependency', message: 'Circular dependency detected in submission items', suggestions: [ { action: 'escalate', label: 'Escalate for manual review', }, ], }); }); return conflicts; } /** * Detect circular dependencies */ function detectCircularDependencies(items: SubmissionItemWithDeps[]): string[] { const circular: string[] = []; const visited = new Set(); const recursionStack = new Set(); function hasCycle(itemId: string): boolean { visited.add(itemId); recursionStack.add(itemId); const item = items.find(i => i.id === itemId); if (item?.depends_on) { if (!visited.has(item.depends_on)) { if (hasCycle(item.depends_on)) { return true; } } else if (recursionStack.has(item.depends_on)) { return true; } } recursionStack.delete(itemId); return false; } items.forEach(item => { if (!visited.has(item.id) && hasCycle(item.id)) { circular.push(item.id); } }); return circular; } /** * Link submission item to existing entity */ export async function linkToExistingEntity( itemId: string, entityId: string, entityType: string ): Promise { const { data: item, error: fetchError } = await supabase .from('submission_items') .select('*') .eq('id', itemId) .single(); if (fetchError || !item) throw fetchError || new Error('Item not found'); // Update item_data to mark it as linked to existing entity const currentItemData = (item.item_data as any) || {}; const updatedItemData = { ...currentItemData, _linked_entity_id: entityId, _skip_creation: true, }; const { error: updateError } = await supabase .from('submission_items') .update({ item_data: updatedItemData, approved_entity_id: entityId, updated_at: new Date().toISOString(), }) .eq('id', itemId); if (updateError) throw updateError; } /** * Resolve conflicts based on user selections */ export async function resolveConflicts( conflicts: DependencyConflict[], resolutions: Record, items: SubmissionItemWithDeps[], userId: string ): Promise<{ updatedItems: SubmissionItemWithDeps[]; newConflicts: DependencyConflict[] }> { if (!userId) { throw new Error('User authentication required to resolve conflicts'); } const updatedItems = [...items]; const newConflicts: DependencyConflict[] = []; for (const conflict of conflicts) { const resolution = resolutions[conflict.itemId]; if (!resolution) continue; const suggestion = conflict.suggestions.find(s => s.action === resolution); if (!suggestion) continue; try { switch (suggestion.action) { case 'create_parent': { // Add parent item to be approved along with child const item = items.find(i => i.id === conflict.itemId); if (item?.depends_on) { const parent = items.find(i => i.id === item.depends_on); if (parent && parent.status === 'pending') { // Mark parent for approval (will be handled in approval flow) const idx = updatedItems.findIndex(i => i.id === parent.id); if (idx >= 0) { updatedItems[idx] = { ...updatedItems[idx], status: 'pending' as const }; } } } break; } case 'link_existing': { // Link to existing entity if (suggestion.entityId) { const item = items.find(i => i.id === conflict.itemId); if (item?.depends_on) { await linkToExistingEntity(item.depends_on, suggestion.entityId, item.item_type); // Update local copy const parentIdx = updatedItems.findIndex(i => i.id === item.depends_on); if (parentIdx >= 0) { updatedItems[parentIdx] = { ...updatedItems[parentIdx], approved_entity_id: suggestion.entityId, item_data: { ...updatedItems[parentIdx].item_data, _linked_entity_id: suggestion.entityId, _skip_creation: true, }, }; } } } break; } case 'escalate': { // Escalate submission for admin review const item = items.find(i => i.id === conflict.itemId); if (item?.submission_id) { await escalateSubmission( item.submission_id, `Dependency conflict: ${conflict.message}`, userId ); } break; } case 'cascade_reject': { // This would reject the item and its dependents const item = items.find(i => i.id === conflict.itemId); if (item) { await rejectSubmissionItems( [item], 'Rejected due to parent dependency conflict', userId, true ); } break; } } } catch (error) { console.error(`Error resolving conflict for item ${conflict.itemId}:`, error); newConflicts.push({ ...conflict, message: `Failed to resolve: ${error instanceof Error ? error.message : 'Unknown error'}`, }); } } return { updatedItems, newConflicts }; } /** * Edit a submission item * Moderators can edit directly, regular users trigger auto-escalation */ export async function editSubmissionItem( itemId: string, newData: any, userId: string ): Promise { // Check user permissions const { data: userRoles } = await supabase .from('user_roles') .select('role') .eq('user_id', userId); const isModerator = userRoles?.some(r => ['moderator', 'admin', 'superuser'].includes(r.role) ); // Get current item const { data: currentItem, error: fetchError } = await supabase .from('submission_items') .select('*') .eq('id', itemId) .single(); if (fetchError || !currentItem) { throw new Error('Failed to fetch submission item'); } // Preserve original data if not already saved const originalData = currentItem.original_data || currentItem.item_data; if (isModerator) { // Moderators can edit directly const { error } = await supabase .from('submission_items') .update({ item_data: newData, original_data: originalData, updated_at: new Date().toISOString() }) .eq('id', itemId); if (error) { console.error('Error updating submission item:', error); throw new Error(`Failed to update item: ${error.message}`); } // Log the edit await supabase .from('admin_audit_log') .insert({ admin_user_id: userId, target_user_id: currentItem.submission_id, action: 'edit_submission_item', details: { item_id: itemId, item_type: currentItem.item_type, changes: newData } }); } else { // Regular users trigger auto-escalation const { data: submission } = await supabase .from('content_submissions') .select('id, status') .eq('id', currentItem.submission_id) .single(); if (submission && submission.status !== 'escalated') { await escalateSubmission( currentItem.submission_id, `User requested edit for ${currentItem.item_type} item`, userId ); } // Update item with edit request const { error } = await supabase .from('submission_items') .update({ item_data: newData, original_data: originalData, status: 'pending', updated_at: new Date().toISOString() }) .eq('id', itemId); if (error) { console.error('Error updating submission item:', error); throw new Error(`Failed to update item: ${error.message}`); } } } /** * Update individual submission item status */ export async function updateSubmissionItem( itemId: string, updates: Partial ): Promise { const { error } = await supabase .from('submission_items') .update(updates) .eq('id', itemId); if (error) throw error; } /** * Approve multiple items with dependency handling */ export async function approveSubmissionItems( items: SubmissionItemWithDeps[], userId: string ): Promise { if (!userId) { throw new Error('User authentication required to approve items'); } // Sort by dependency order (parents first) const sortedItems = topologicalSort(items); // Track entity ID mappings for dependency resolution const entityIdMap = new Map(); for (const item of sortedItems) { let entityId: string | null = null; // Check if item is linked to existing entity (skip creation) if (item.item_data._skip_creation && item.item_data._linked_entity_id) { entityId = item.item_data._linked_entity_id; entityIdMap.set(item.id, entityId); } else { // Resolve dependencies using entityIdMap const resolvedItemData = resolveDependencies(item.item_data, entityIdMap); // Create the entity based on type switch (item.item_type) { case 'park': entityId = await createPark(resolvedItemData); break; case 'ride': entityId = await createRide(resolvedItemData); break; case 'manufacturer': case 'operator': case 'property_owner': case 'designer': entityId = await createCompany(resolvedItemData, item.item_type); break; case 'ride_model': entityId = await createRideModel(resolvedItemData); break; case 'photo': entityId = await approvePhotos(resolvedItemData); break; } if (entityId) { entityIdMap.set(item.id, entityId); } } // Update item status await updateSubmissionItem(item.id, { status: 'approved', approved_entity_id: entityId, }); } } /** * Resolve dependencies in item data using entity ID mappings */ function resolveDependencies(itemData: any, entityIdMap: Map): any { const resolved = { ...itemData }; // Map common dependency fields const dependencyFields = [ 'park_id', 'manufacturer_id', 'operator_id', 'property_owner_id', 'designer_id', 'ride_model_id', ]; dependencyFields.forEach(field => { if (resolved[field] && entityIdMap.has(resolved[field])) { resolved[field] = entityIdMap.get(resolved[field]); } }); return resolved; } /** * Topological sort for dependency-ordered processing */ function topologicalSort(items: SubmissionItemWithDeps[]): SubmissionItemWithDeps[] { const sorted: SubmissionItemWithDeps[] = []; const visited = new Set(); const temp = new Set(); function visit(item: SubmissionItemWithDeps) { if (temp.has(item.id)) { throw new Error('Circular dependency detected'); } if (visited.has(item.id)) return; temp.add(item.id); if (item.dependencies) { item.dependencies.forEach(dep => visit(dep)); } temp.delete(item.id); visited.add(item.id); sorted.push(item); } items.forEach(item => { if (!visited.has(item.id)) { visit(item); } }); return sorted; } /** * Helper functions to create entities */ async function createPark(data: any): Promise { const { data: park, error } = await supabase .from('parks') .insert(data) .select('id') .single(); if (error) throw error; return park.id; } async function createRide(data: any): Promise { const { data: ride, error } = await supabase .from('rides') .insert(data) .select('id') .single(); if (error) throw error; return ride.id; } async function createCompany(data: any, companyType: string): Promise { const { data: company, error } = await supabase .from('companies') .insert({ ...data, company_type: companyType }) .select('id') .single(); if (error) throw error; return company.id; } async function createRideModel(data: any): Promise { const { data: model, error } = await supabase .from('ride_models') .insert(data) .select('id') .single(); if (error) throw error; return model.id; } async function approvePhotos(data: any): Promise { // Photos are already uploaded to Cloudflare // Just need to associate them with the entity return data.photos?.[0]?.url || ''; } /** * Reject multiple items with optional cascade to dependents */ export async function rejectSubmissionItems( items: SubmissionItemWithDeps[], reason: string, userId: string, cascade: boolean = true ): Promise { if (!userId) { throw new Error('User authentication required to reject items'); } if (!reason || !reason.trim()) { throw new Error('Rejection reason is required'); } const itemsToReject = new Set(items.map(i => i.id)); // If cascading, collect all dependent items if (cascade) { for (const item of items) { await collectDependents(item, itemsToReject); } } // Update all items to rejected status const updates = Array.from(itemsToReject).map(async (itemId) => { const { error } = await supabase .from('submission_items') .update({ status: 'rejected', rejection_reason: reason, updated_at: new Date().toISOString(), }) .eq('id', itemId); if (error) { console.error(`Error rejecting item ${itemId}:`, error); throw error; } }); await Promise.all(updates); // Update parent submission status const submissionId = items[0]?.submission_id; if (submissionId) { await updateSubmissionStatusAfterRejection(submissionId); } } async function collectDependents( item: SubmissionItemWithDeps, rejectedSet: Set ): Promise { if (item.dependents && item.dependents.length > 0) { for (const dependent of item.dependents) { rejectedSet.add(dependent.id); await collectDependents(dependent, rejectedSet); } } } async function updateSubmissionStatusAfterRejection(submissionId: string): Promise { // Get all items for this submission const { data: allItems, error: fetchError } = await supabase .from('submission_items') .select('status') .eq('submission_id', submissionId); if (fetchError) { console.error('Error fetching submission items:', fetchError); return; } if (!allItems || allItems.length === 0) return; const statuses = allItems.map(i => i.status); const allRejected = statuses.every(s => s === 'rejected'); const allApproved = statuses.every(s => s === 'approved'); const anyPending = statuses.some(s => s === 'pending'); let newStatus: string; if (allRejected) { newStatus = 'rejected'; } else if (allApproved) { newStatus = 'approved'; } else if (anyPending) { newStatus = 'pending'; } else { newStatus = 'partially_approved'; } const { error: updateError } = await supabase .from('content_submissions') .update({ status: newStatus, updated_at: new Date().toISOString(), }) .eq('id', submissionId); if (updateError) { console.error('Error updating submission status:', updateError); } } /** * Escalate submission for admin review */ export async function escalateSubmission( submissionId: string, reason: string, userId: string ): Promise { if (!userId) { throw new Error('User authentication required to escalate submission'); } const { error } = await supabase .from('content_submissions') .update({ status: 'pending', escalation_reason: reason, escalated_by: userId, reviewer_notes: `Escalated: ${reason}`, }) .eq('id', submissionId); if (error) throw error; }