diff --git a/src/components/moderation/ConflictResolutionDialog.tsx b/src/components/moderation/ConflictResolutionDialog.tsx index 79c53858..2a8177f5 100644 --- a/src/components/moderation/ConflictResolutionDialog.tsx +++ b/src/components/moderation/ConflictResolutionDialog.tsx @@ -4,10 +4,8 @@ import { Button } from '@/components/ui/button'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { AlertCircle, Loader2 } from 'lucide-react'; -import { toast } from 'sonner'; -import { type DependencyConflict, type SubmissionItemWithDeps, resolveConflicts } from '@/lib/submissionItemsService'; -import { useAuth } from '@/hooks/useAuth'; +import { AlertCircle } from 'lucide-react'; +import { type DependencyConflict, type SubmissionItemWithDeps } from '@/lib/submissionItemsService'; interface ConflictResolutionDialogProps { open: boolean; @@ -24,9 +22,7 @@ export function ConflictResolutionDialog({ items, onResolve, }: ConflictResolutionDialogProps) { - const { user } = useAuth(); const [resolutions, setResolutions] = useState>({}); - const [isProcessing, setIsProcessing] = useState(false); const handleResolutionChange = (itemId: string, action: string) => { setResolutions(prev => ({ ...prev, [itemId]: action })); @@ -36,36 +32,10 @@ export function ConflictResolutionDialog({ conflict => resolutions[conflict.itemId] ); - const handleApply = async () => { - if (!user) { - toast.error('You must be logged in to resolve conflicts'); - return; - } - - setIsProcessing(true); - - try { - const { updatedItems, newConflicts } = await resolveConflicts( - conflicts, - resolutions, - items, - user.id - ); - - if (newConflicts.length > 0) { - toast.error(`Resolution completed with ${newConflicts.length} remaining conflicts`); - } else { - toast.success('All conflicts resolved successfully'); - } - - onResolve(); - onOpenChange(false); - } catch (error) { - console.error('Error resolving conflicts:', error); - toast.error(error instanceof Error ? error.message : 'Failed to resolve conflicts'); - } finally { - setIsProcessing(false); - } + const handleApply = () => { + // TODO: Apply resolutions + onResolve(); + onOpenChange(false); }; return ( @@ -113,12 +83,11 @@ export function ConflictResolutionDialog({ - - diff --git a/src/components/moderation/ItemEditDialog.tsx b/src/components/moderation/ItemEditDialog.tsx deleted file mode 100644 index c1ddfee2..00000000 --- a/src/components/moderation/ItemEditDialog.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { useState } from 'react'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { ParkForm } from '@/components/admin/ParkForm'; -import { RideForm } from '@/components/admin/RideForm'; -import { ManufacturerForm } from '@/components/admin/ManufacturerForm'; -import { DesignerForm } from '@/components/admin/DesignerForm'; -import { OperatorForm } from '@/components/admin/OperatorForm'; -import { PropertyOwnerForm } from '@/components/admin/PropertyOwnerForm'; -import { RideModelForm } from '@/components/admin/RideModelForm'; -import { PhotoCaptionEditor } from '@/components/upload/PhotoCaptionEditor'; -import { useIsMobile } from '@/hooks/use-mobile'; -import { editSubmissionItem } from '@/lib/submissionItemsService'; -import { useAuth } from '@/hooks/useAuth'; -import { toast } from '@/hooks/use-toast'; -import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService'; - -interface ItemEditDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - item: SubmissionItemWithDeps | null; - onEditComplete: () => void; -} - -export function ItemEditDialog({ open, onOpenChange, item, onEditComplete }: ItemEditDialogProps) { - const isMobile = useIsMobile(); - const { user } = useAuth(); - const [isSubmitting, setIsSubmitting] = useState(false); - - if (!item) return null; - - const handleFormSubmit = async (data: any) => { - if (!user) { - toast({ - title: "Authentication Required", - description: "You must be logged in to edit items.", - variant: "destructive" - }); - return; - } - - setIsSubmitting(true); - try { - await editSubmissionItem(item.id, data, user.id); - - toast({ - title: "Item Updated", - description: "The submission item has been updated successfully." - }); - - onOpenChange(false); - onEditComplete(); - } catch (error: any) { - toast({ - title: "Error", - description: error.message || "Failed to update submission item.", - variant: "destructive" - }); - } finally { - setIsSubmitting(false); - } - }; - - const handleCancel = () => { - if (!isSubmitting) { - onOpenChange(false); - } - }; - - const renderForm = () => { - const itemData = item.item_data as any; - - switch (item.item_type) { - case 'park': - return ( - - ); - - case 'ride': - return ( - - ); - - case 'manufacturer': - return ( - - ); - - case 'designer': - return ( - - ); - - case 'operator': - return ( - - ); - - case 'property_owner': - return ( - - ); - - case 'ride_model': - return ( - - ); - - case 'photo': - return ( -
-
- - { - itemData.caption = e.target.value; - }} - placeholder="Enter photo caption" - /> -
- {itemData.image_url && ( - Preview - )} -
- - -
-
- ); - - default: - return ( -
- Form not available for item type: {item.item_type} -
- ); - } - }; - - const content = ( - <> - {isMobile ? ( - - Edit {item.item_type.replace('_', ' ')} - - ) : ( - - Edit {item.item_type.replace('_', ' ')} - - )} -
- {renderForm()} -
- - ); - - if (isMobile) { - return ( - - - {content} - - - ); - } - - return ( - - - {content} - - - ); -} diff --git a/src/components/moderation/ItemReviewCard.tsx b/src/components/moderation/ItemReviewCard.tsx index 9e29dd4b..0e6ce77a 100644 --- a/src/components/moderation/ItemReviewCard.tsx +++ b/src/components/moderation/ItemReviewCard.tsx @@ -141,11 +141,6 @@ export function ItemReviewCard({ item, onEdit, onStatusChange }: ItemReviewCardP )} - {item.original_data && ( - - Edited - - )} diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index 2f67f5b8..ac8a7f0c 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -27,7 +27,6 @@ import { DependencyVisualizer } from './DependencyVisualizer'; import { ConflictResolutionDialog } from './ConflictResolutionDialog'; import { EscalationDialog } from './EscalationDialog'; import { RejectionDialog } from './RejectionDialog'; -import { ItemEditDialog } from './ItemEditDialog'; interface SubmissionReviewManagerProps { submissionId: string; @@ -49,8 +48,6 @@ export function SubmissionReviewManager({ const [showConflictDialog, setShowConflictDialog] = useState(false); const [showEscalationDialog, setShowEscalationDialog] = useState(false); const [showRejectionDialog, setShowRejectionDialog] = useState(false); - const [showEditDialog, setShowEditDialog] = useState(false); - const [editingItem, setEditingItem] = useState(null); const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items'); const { toast } = useToast(); @@ -243,12 +240,6 @@ export function SubmissionReviewManager({ } }; - const handleEditComplete = async () => { - // Reload items after edit - await loadSubmissionItems(); - setEditingItem(null); - }; - const pendingCount = items.filter(item => item.status === 'pending').length; const selectedCount = selectedItemIds.size; @@ -301,13 +292,6 @@ export function SubmissionReviewManager({ )} onReject={handleReject} /> - - ); @@ -347,10 +331,7 @@ export function SubmissionReviewManager({ /> { - setEditingItem(item); - setShowEditDialog(true); - }} + onEdit={() => {/* TODO: Implement editing */}} onStatusChange={(status) => {/* TODO: Update status */}} /> diff --git a/src/lib/submissionItemsService.ts b/src/lib/submissionItemsService.ts index 7a9b29fa..d75a7aa6 100644 --- a/src/lib/submissionItemsService.ts +++ b/src/lib/submissionItemsService.ts @@ -97,7 +97,7 @@ export async function detectDependencyConflicts( }); } - // Suggest linking to existing entity based on type + // Suggest linking to existing entity if (parent.item_type === 'park') { const { data: parks } = await supabase .from('parks') @@ -112,35 +112,6 @@ export async function detectDependencyConflicts( 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({ @@ -150,7 +121,7 @@ export async function detectDependencyConflicts( conflicts.push({ itemId: item.id, - type: parent.status === 'rejected' ? 'rejected_parent' : 'missing_parent', + type: 'missing_parent', message: `Cannot approve ${item.item_type} without its parent ${parent.item_type}`, suggestions, }); @@ -158,301 +129,9 @@ export async function detectDependencyConflicts( } } - // 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 */ @@ -482,45 +161,29 @@ export async function approveSubmissionItems( // 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); - } + // Create the entity based on type + switch (item.item_type) { + case 'park': + entityId = await createPark(item.item_data); + break; + case 'ride': + entityId = await createRide(item.item_data); + break; + case 'manufacturer': + case 'operator': + case 'property_owner': + case 'designer': + entityId = await createCompany(item.item_data, item.item_type); + break; + case 'ride_model': + entityId = await createRideModel(item.item_data); + break; + case 'photo': + entityId = await approvePhotos(item.item_data); + break; } // Update item status @@ -531,31 +194,6 @@ export async function approveSubmissionItems( } } -/** - * 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 */