mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 03:31:13 -05:00
Implement Phase 4 optimizations
This commit is contained in:
282
src/hooks/moderation/useModerationActions.ts
Normal file
282
src/hooks/moderation/useModerationActions.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { useCallback } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { logger } from '@/lib/logger';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import type { ModerationItem } from '@/types/moderation';
|
||||
|
||||
/**
|
||||
* Configuration for moderation actions
|
||||
*/
|
||||
export interface ModerationActionsConfig {
|
||||
user: User | null;
|
||||
onActionStart: (itemId: string) => void;
|
||||
onActionComplete: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type for useModerationActions
|
||||
*/
|
||||
export interface ModerationActions {
|
||||
performAction: (item: ModerationItem, action: 'approved' | 'rejected', moderatorNotes?: string) => Promise<void>;
|
||||
deleteSubmission: (item: ModerationItem) => Promise<void>;
|
||||
resetToPending: (item: ModerationItem) => Promise<void>;
|
||||
retryFailedItems: (item: ModerationItem) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for moderation action handlers
|
||||
* Extracted from useModerationQueueManager for better separation of concerns
|
||||
*
|
||||
* @param config - Configuration object with user, callbacks, and dependencies
|
||||
* @returns Object with action handler functions
|
||||
*/
|
||||
export function useModerationActions(config: ModerationActionsConfig): ModerationActions {
|
||||
const { user, onActionStart, onActionComplete } = config;
|
||||
const { toast } = useToast();
|
||||
|
||||
/**
|
||||
* Perform moderation action (approve/reject)
|
||||
*/
|
||||
const performAction = useCallback(
|
||||
async (item: ModerationItem, action: 'approved' | 'rejected', moderatorNotes?: string) => {
|
||||
onActionStart(item.id);
|
||||
|
||||
try {
|
||||
// Handle photo submissions
|
||||
if (action === 'approved' && item.submission_type === 'photo') {
|
||||
const { data: photoSubmission } = await supabase
|
||||
.from('photo_submissions')
|
||||
.select(`*, items:photo_submission_items(*), submission:content_submissions!inner(user_id)`)
|
||||
.eq('submission_id', item.id)
|
||||
.single();
|
||||
|
||||
if (photoSubmission && photoSubmission.items) {
|
||||
const { data: existingPhotos } = await supabase
|
||||
.from('photos')
|
||||
.select('id')
|
||||
.eq('submission_id', item.id);
|
||||
|
||||
if (!existingPhotos || existingPhotos.length === 0) {
|
||||
const photoRecords = photoSubmission.items.map((photoItem: any) => ({
|
||||
entity_id: photoSubmission.entity_id,
|
||||
entity_type: photoSubmission.entity_type,
|
||||
cloudflare_image_id: photoItem.cloudflare_image_id,
|
||||
cloudflare_image_url: photoItem.cloudflare_image_url,
|
||||
title: photoItem.title || null,
|
||||
caption: photoItem.caption || null,
|
||||
date_taken: photoItem.date_taken || null,
|
||||
order_index: photoItem.order_index,
|
||||
submission_id: photoSubmission.submission_id,
|
||||
submitted_by: photoSubmission.submission?.user_id,
|
||||
approved_by: user?.id,
|
||||
approved_at: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
await supabase.from('photos').insert(photoRecords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for submission items
|
||||
const { data: submissionItems } = await supabase
|
||||
.from('submission_items')
|
||||
.select('id, status')
|
||||
.eq('submission_id', item.id)
|
||||
.in('status', ['pending', 'rejected']);
|
||||
|
||||
if (submissionItems && submissionItems.length > 0) {
|
||||
if (action === 'approved') {
|
||||
await supabase.functions.invoke('process-selective-approval', {
|
||||
body: {
|
||||
itemIds: submissionItems.map((i) => i.id),
|
||||
submissionId: item.id,
|
||||
},
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Submission Approved',
|
||||
description: `Successfully processed ${submissionItems.length} item(s)`,
|
||||
});
|
||||
return;
|
||||
} else if (action === 'rejected') {
|
||||
await supabase
|
||||
.from('submission_items')
|
||||
.update({
|
||||
status: 'rejected',
|
||||
rejection_reason: moderatorNotes || 'Parent submission rejected',
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('submission_id', item.id)
|
||||
.eq('status', 'pending');
|
||||
}
|
||||
}
|
||||
|
||||
// Standard update
|
||||
const table = item.type === 'review' ? 'reviews' : 'content_submissions';
|
||||
const statusField = item.type === 'review' ? 'moderation_status' : 'status';
|
||||
const timestampField = item.type === 'review' ? 'moderated_at' : 'reviewed_at';
|
||||
const reviewerField = item.type === 'review' ? 'moderated_by' : 'reviewer_id';
|
||||
|
||||
const updateData: any = {
|
||||
[statusField]: action,
|
||||
[timestampField]: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (user) {
|
||||
updateData[reviewerField] = user.id;
|
||||
}
|
||||
|
||||
if (moderatorNotes) {
|
||||
updateData.reviewer_notes = moderatorNotes;
|
||||
}
|
||||
|
||||
const { error } = await supabase.from(table).update(updateData).eq('id', item.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: `Content ${action}`,
|
||||
description: `The ${item.type} has been ${action}`,
|
||||
});
|
||||
|
||||
logger.log(`✅ Action ${action} completed for ${item.id}`);
|
||||
} catch (error: any) {
|
||||
logger.error('❌ Error performing action:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || `Failed to ${action} content`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
onActionComplete();
|
||||
}
|
||||
},
|
||||
[user, toast, onActionStart, onActionComplete]
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete a submission permanently
|
||||
*/
|
||||
const deleteSubmission = useCallback(
|
||||
async (item: ModerationItem) => {
|
||||
if (item.type !== 'content_submission') return;
|
||||
|
||||
onActionStart(item.id);
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('content_submissions').delete().eq('id', item.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Submission deleted',
|
||||
description: 'The submission has been permanently deleted',
|
||||
});
|
||||
|
||||
logger.log(`✅ Submission ${item.id} deleted`);
|
||||
} catch (error: any) {
|
||||
logger.error('❌ Error deleting submission:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to delete submission',
|
||||
variant: 'destructive',
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
onActionComplete();
|
||||
}
|
||||
},
|
||||
[toast, onActionStart, onActionComplete]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reset submission to pending status
|
||||
*/
|
||||
const resetToPending = useCallback(
|
||||
async (item: ModerationItem) => {
|
||||
onActionStart(item.id);
|
||||
|
||||
try {
|
||||
const { resetRejectedItemsToPending } = await import('@/lib/submissionItemsService');
|
||||
await resetRejectedItemsToPending(item.id);
|
||||
|
||||
toast({
|
||||
title: 'Reset Complete',
|
||||
description: 'Submission and all items have been reset to pending status',
|
||||
});
|
||||
|
||||
logger.log(`✅ Submission ${item.id} reset to pending`);
|
||||
} catch (error: any) {
|
||||
logger.error('❌ Error resetting submission:', error);
|
||||
toast({
|
||||
title: 'Reset Failed',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
onActionComplete();
|
||||
}
|
||||
},
|
||||
[toast, onActionStart, onActionComplete]
|
||||
);
|
||||
|
||||
/**
|
||||
* Retry failed items in a submission
|
||||
*/
|
||||
const retryFailedItems = useCallback(
|
||||
async (item: ModerationItem) => {
|
||||
onActionStart(item.id);
|
||||
|
||||
try {
|
||||
const { data: failedItems } = await supabase
|
||||
.from('submission_items')
|
||||
.select('id')
|
||||
.eq('submission_id', item.id)
|
||||
.eq('status', 'rejected');
|
||||
|
||||
if (!failedItems || failedItems.length === 0) {
|
||||
toast({
|
||||
title: 'No Failed Items',
|
||||
description: 'All items have been processed successfully',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase.functions.invoke('process-selective-approval', {
|
||||
body: {
|
||||
itemIds: failedItems.map((i) => i.id),
|
||||
submissionId: item.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Items Retried',
|
||||
description: `Successfully retried ${failedItems.length} failed item(s)`,
|
||||
});
|
||||
|
||||
logger.log(`✅ Retried ${failedItems.length} failed items for ${item.id}`);
|
||||
} catch (error: any) {
|
||||
logger.error('❌ Error retrying items:', error);
|
||||
toast({
|
||||
title: 'Retry Failed',
|
||||
description: error.message || 'Failed to retry items',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
onActionComplete();
|
||||
}
|
||||
},
|
||||
[toast, onActionStart, onActionComplete]
|
||||
);
|
||||
|
||||
return {
|
||||
performAction,
|
||||
deleteSubmission,
|
||||
resetToPending,
|
||||
retryFailedItems,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user