import { supabase } from '@/integrations/supabase/client'; export type ActivityType = | 'entity_change' | 'admin_action' | 'submission_review' | 'report_resolution' | 'review_moderation' | 'photo_approval'; export interface ActivityActor { id: string; username: string; display_name?: string; avatar_url?: string; } export interface EntityChangeDetails { entity_type: string; entity_id: string; entity_name?: string; change_type: string; change_reason?: string; version_number?: number; } export interface AdminActionDetails { action: string; target_user_id?: string; target_username?: string; details?: Record; } export interface SubmissionReviewDetails { submission_id: string; submission_type: string; status: string; entity_name?: string; // Photo-specific fields photo_url?: string; photo_caption?: string; photo_title?: string; entity_type?: string; entity_id?: string; deletion_reason?: string; } export interface ReportResolutionDetails { report_id: string; reported_entity_type: string; reported_entity_id: string; status: string; resolution_notes?: string; } export interface ReviewModerationDetails { review_id: string; entity_type: string; entity_id: string; moderation_status: string; entity_name?: string; } export interface PhotoApprovalDetails { photo_id: string; entity_type: string; entity_id: string; entity_name?: string; } export type ActivityDetails = | EntityChangeDetails | AdminActionDetails | SubmissionReviewDetails | ReportResolutionDetails | ReviewModerationDetails | PhotoApprovalDetails; export interface SystemActivity { id: string; type: ActivityType; timestamp: string; actor_id: string | null; actor?: ActivityActor; action: string; details: ActivityDetails; } export interface ActivityFilters { type?: ActivityType; userId?: string; entityType?: string; dateFrom?: string; dateTo?: string; } /** * Fetch unified system activity log from multiple sources */ export async function fetchSystemActivities( limit: number = 50, filters?: ActivityFilters ): Promise { const activities: SystemActivity[] = []; // Fetch entity versions (entity changes) // Use simplified query without foreign key join - we'll fetch profiles separately const { data: versions, error: versionsError } = await supabase .from('entity_versions') .select('id, entity_type, entity_id, version_number, version_data, changed_by, changed_at, change_type, change_reason') .eq('is_current', true) .order('changed_at', { ascending: false }) .limit(limit * 2); // Fetch more to account for filtering if (!versionsError && versions) { for (const version of versions) { const versionData = version.version_data as any; activities.push({ id: version.id, type: 'entity_change', timestamp: version.changed_at, actor_id: version.changed_by || null, action: `${version.change_type} ${version.entity_type}`, details: { entity_type: version.entity_type, entity_id: version.entity_id, entity_name: versionData?.name || versionData?.title, change_type: version.change_type, change_reason: version.change_reason, version_number: version.version_number, } as EntityChangeDetails, }); } } // Fetch admin audit log (admin actions) const { data: auditLogs, error: auditError } = await supabase .from('admin_audit_log') .select('id, admin_user_id, target_user_id, action, details, created_at') .order('created_at', { ascending: false }) .limit(limit); if (!auditError && auditLogs) { for (const log of auditLogs) { activities.push({ id: log.id, type: 'admin_action', timestamp: log.created_at, actor_id: log.admin_user_id, action: log.action, details: { action: log.action, target_user_id: log.target_user_id, details: log.details, } as AdminActionDetails, }); } } // Fetch submission reviews (approved/rejected submissions) const { data: submissions, error: submissionsError } = await supabase .from('content_submissions') .select('id, submission_type, status, reviewer_id, reviewed_at, content') .not('reviewed_at', 'is', null) .in('status', ['approved', 'rejected', 'partially_approved']) .order('reviewed_at', { ascending: false }) .limit(limit); if (!submissionsError && submissions) { // Fetch submission_items for photo submissions to get detailed info const submissionIds = submissions.map(s => s.id); const { data: submissionItems } = await supabase .from('submission_items') .select('submission_id, item_type, item_data') .in('submission_id', submissionIds) .in('item_type', ['photo', 'photo_delete', 'photo_edit']); const itemsMap = new Map(submissionItems?.map(item => [item.submission_id, item]) || []); for (const submission of submissions) { const contentData = submission.content as any; const submissionItem = itemsMap.get(submission.id); const itemData = submissionItem?.item_data as any; // Build base details const details: SubmissionReviewDetails = { submission_id: submission.id, submission_type: submission.submission_type, status: submission.status, entity_name: contentData?.name, }; // Enrich with photo-specific data for photo submissions if (submissionItem && itemData) { if (submissionItem.item_type === 'photo_delete') { details.photo_url = itemData.cloudflare_image_url; details.photo_caption = itemData.caption; details.photo_title = itemData.title; details.entity_type = itemData.entity_type; details.entity_id = itemData.entity_id; details.deletion_reason = itemData.reason; } else if (submissionItem.item_type === 'photo') { // Photo additions details.photo_url = itemData.cloudflare_image_url; details.photo_caption = itemData.caption; details.photo_title = itemData.title; details.entity_type = itemData.entity_type; details.entity_id = itemData.entity_id; } } activities.push({ id: submission.id, type: 'submission_review', timestamp: submission.reviewed_at!, actor_id: submission.reviewer_id, action: `${submission.status} ${submission.submission_type} submission`, details, }); } } // Fetch report resolutions const { data: reports, error: reportsError } = await supabase .from('reports') .select('id, reported_entity_type, reported_entity_id, status, reviewed_by, reviewed_at') .not('reviewed_at', 'is', null) .order('reviewed_at', { ascending: false }) .limit(limit); if (!reportsError && reports) { for (const report of reports) { activities.push({ id: report.id, type: 'report_resolution', timestamp: report.reviewed_at!, actor_id: report.reviewed_by, action: `${report.status} report`, details: { report_id: report.id, reported_entity_type: report.reported_entity_type, reported_entity_id: report.reported_entity_id, status: report.status, } as ReportResolutionDetails, }); } } // Fetch review moderation const { data: reviews, error: reviewsError } = await supabase .from('reviews') .select('id, park_id, ride_id, moderation_status, moderated_by, moderated_at') .not('moderated_at', 'is', null) .neq('moderation_status', 'pending') .order('moderated_at', { ascending: false }) .limit(limit); if (!reviewsError && reviews) { for (const review of reviews) { const entityType = review.park_id ? 'park' : 'ride'; const entityId = review.park_id || review.ride_id; activities.push({ id: review.id, type: 'review_moderation', timestamp: review.moderated_at!, actor_id: review.moderated_by, action: `${review.moderation_status} review`, details: { review_id: review.id, entity_type: entityType, entity_id: entityId!, moderation_status: review.moderation_status, } as ReviewModerationDetails, }); } } // Fetch photo approvals const { data: photos, error: photosError } = await supabase .from('photos') .select('id, entity_type, entity_id, approved_by, approved_at') .not('approved_at', 'is', null) .order('approved_at', { ascending: false }) .limit(limit); if (!photosError && photos) { for (const photo of photos) { activities.push({ id: photo.id, type: 'photo_approval', timestamp: photo.approved_at!, actor_id: photo.approved_by, action: `approved ${photo.entity_type} photo`, details: { photo_id: photo.id, entity_type: photo.entity_type, entity_id: photo.entity_id, } as PhotoApprovalDetails, }); } } // Sort all activities by timestamp (newest first) activities.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); // Apply filters let filteredActivities = activities; if (filters?.type) { filteredActivities = filteredActivities.filter(a => a.type === filters.type); } if (filters?.userId) { filteredActivities = filteredActivities.filter(a => a.actor_id === filters.userId); } // Limit to requested amount filteredActivities = filteredActivities.slice(0, limit); // Enrich with user profile data const uniqueUserIds = [...new Set(filteredActivities.map(a => a.actor_id).filter(Boolean))] as string[]; if (uniqueUserIds.length > 0) { const { data: profiles } = await supabase .from('profiles') .select('user_id, username, display_name, avatar_url') .in('user_id', uniqueUserIds); if (profiles) { const profileMap = new Map(profiles.map(p => [p.user_id, p])); for (const activity of filteredActivities) { if (activity.actor_id) { const profile = profileMap.get(activity.actor_id); if (profile) { activity.actor = { id: profile.user_id, username: profile.username, display_name: profile.display_name || undefined, avatar_url: profile.avatar_url || undefined, }; } } } // Also enrich admin action target users const targetUserIds = filteredActivities .filter(a => a.type === 'admin_action') .map(a => (a.details as AdminActionDetails).target_user_id) .filter(Boolean) as string[]; if (targetUserIds.length > 0) { const { data: targetProfiles } = await supabase .from('profiles') .select('user_id, username') .in('user_id', targetUserIds); if (targetProfiles) { const targetProfileMap = new Map(targetProfiles.map(p => [p.user_id, p])); for (const activity of filteredActivities) { if (activity.type === 'admin_action') { const details = activity.details as AdminActionDetails; if (details.target_user_id) { const targetProfile = targetProfileMap.get(details.target_user_id); if (targetProfile) { details.target_username = targetProfile.username; } } } } } } } } return filteredActivities; }