diff --git a/src/components/admin/SystemActivityLog.tsx b/src/components/admin/SystemActivityLog.tsx index 96f235a8..c991d07d 100644 --- a/src/components/admin/SystemActivityLog.tsx +++ b/src/components/admin/SystemActivityLog.tsx @@ -41,7 +41,8 @@ import { ReviewModerationDetails, PhotoApprovalDetails, AccountLifecycleDetails, - ReviewLifecycleDetails + ReviewLifecycleDetails, + SubmissionWorkflowDetails } from '@/lib/systemActivityService'; export interface SystemActivityLogRef { @@ -138,6 +139,30 @@ const activityTypeConfig = { bgColor: 'bg-red-600/10', label: 'Review Deleted', }, + submission_created: { + icon: Plus, + color: 'text-blue-500', + bgColor: 'bg-blue-500/10', + label: 'Submission Created', + }, + submission_claimed: { + icon: UserCog, + color: 'text-indigo-500', + bgColor: 'bg-indigo-500/10', + label: 'Submission Claimed', + }, + submission_escalated: { + icon: AlertTriangle, + color: 'text-orange-600', + bgColor: 'bg-orange-600/10', + label: 'Submission Escalated', + }, + submission_reassigned: { + icon: History, + color: 'text-purple-600', + bgColor: 'bg-purple-600/10', + label: 'Submission Reassigned', + }, }; export const SystemActivityLog = forwardRef( @@ -565,6 +590,92 @@ export const SystemActivityLog = forwardRef +
+ New Submission + {details.submission_type.replace(/_/g, ' ')} + {details.username && ( + by @{details.username} + )} +
+ + ); + } + + case 'submission_claimed': { + const details = activity.details as SubmissionWorkflowDetails; + return ( +
+
+ Claimed + {details.submission_type.replace(/_/g, ' ')} + {details.username && ( + by @{details.username} + )} +
+ {isExpanded && details.assigned_username && ( +
+ + + Assigned to @{details.assigned_username} + +
+ )} +
+ ); + } + + case 'submission_escalated': { + const details = activity.details as SubmissionWorkflowDetails; + return ( +
+
+ Escalated + {details.submission_type.replace(/_/g, ' ')} +
+ {isExpanded && details.escalation_reason && ( +
+ + + Reason: {details.escalation_reason} + +
+ )} +
+ ); + } + + case 'submission_reassigned': { + const details = activity.details as SubmissionWorkflowDetails; + return ( +
+
+ Reassigned + {details.submission_type.replace(/_/g, ' ')} +
+ {isExpanded && (details.from_moderator_username || details.to_moderator_username) && ( +
+ + + {details.from_moderator_username && details.to_moderator_username ? ( + <> + From @{details.from_moderator_username} to @{details.to_moderator_username} + + ) : details.to_moderator_username ? ( + <> + To @{details.to_moderator_username} + + ) : null} + +
+ )} +
+ ); + } + default: return null; } diff --git a/src/lib/systemActivityService.ts b/src/lib/systemActivityService.ts index 6e0d9b6a..4ac8db00 100644 --- a/src/lib/systemActivityService.ts +++ b/src/lib/systemActivityService.ts @@ -15,7 +15,11 @@ export type ActivityType = | 'user_banned' | 'user_unbanned' | 'review_created' - | 'review_deleted'; + | 'review_deleted' + | 'submission_created' + | 'submission_claimed' + | 'submission_escalated' + | 'submission_reassigned'; export interface ActivityActor { id: string; @@ -99,6 +103,20 @@ export interface ReviewLifecycleDetails { was_moderated?: boolean; } +export interface SubmissionWorkflowDetails { + submission_id: string; + submission_type: string; + user_id?: string; + username?: string; + assigned_to?: string; + assigned_username?: string; + escalation_reason?: string; + from_moderator?: string; + from_moderator_username?: string; + to_moderator?: string; + to_moderator_username?: string; +} + export type ActivityDetails = | EntityChangeDetails | AdminActionDetails @@ -107,7 +125,8 @@ export type ActivityDetails = | ReviewModerationDetails | PhotoApprovalDetails | AccountLifecycleDetails - | ReviewLifecycleDetails; + | ReviewLifecycleDetails + | SubmissionWorkflowDetails; export interface SystemActivity { id: string; @@ -588,6 +607,87 @@ export async function fetchSystemActivities( } } + // Fetch submission workflow events (recent 7 days) + // 1. Submission creations + const { data: newSubmissions, error: newSubmissionsError } = await supabase + .from('content_submissions') + .select('id, user_id, submission_type, submitted_at') + .gte('submitted_at', sevenDaysAgo.toISOString()) + .order('submitted_at', { ascending: false }) + .limit(Math.ceil(limit / 2)); + + if (!newSubmissionsError && newSubmissions) { + for (const submission of newSubmissions) { + activities.push({ + id: `submission-created-${submission.id}`, + type: 'submission_created', + timestamp: submission.submitted_at, + actor_id: submission.user_id, + action: 'created submission', + details: { + submission_id: submission.id, + submission_type: submission.submission_type, + user_id: submission.user_id, + } as SubmissionWorkflowDetails, + }); + } + } + + // 2. Submission claims/assignments + const { data: claimedSubmissions, error: claimsError } = await supabase + .from('content_submissions') + .select('id, submission_type, assigned_to, assigned_at, user_id') + .not('assigned_at', 'is', null) + .gte('assigned_at', sevenDaysAgo.toISOString()) + .order('assigned_at', { ascending: false }) + .limit(Math.ceil(limit / 2)); + + if (!claimsError && claimedSubmissions) { + for (const submission of claimedSubmissions) { + activities.push({ + id: `submission-claimed-${submission.id}`, + type: 'submission_claimed', + timestamp: submission.assigned_at!, + actor_id: submission.assigned_to, + action: 'claimed submission', + details: { + submission_id: submission.id, + submission_type: submission.submission_type, + user_id: submission.user_id, + assigned_to: submission.assigned_to, + } as SubmissionWorkflowDetails, + }); + } + } + + // 3. Submission escalations + const { data: escalatedSubmissions, error: escalationsError } = await supabase + .from('content_submissions') + .select('id, submission_type, escalated_by, escalated_at, escalation_reason, user_id') + .eq('escalated', true) + .not('escalated_at', 'is', null) + .gte('escalated_at', sevenDaysAgo.toISOString()) + .order('escalated_at', { ascending: false }) + .limit(Math.ceil(limit / 2)); + + if (!escalationsError && escalatedSubmissions) { + for (const submission of escalatedSubmissions) { + activities.push({ + id: `submission-escalated-${submission.id}`, + type: 'submission_escalated', + timestamp: submission.escalated_at!, + actor_id: submission.escalated_by, + action: 'escalated submission', + details: { + submission_id: submission.id, + submission_type: submission.submission_type, + user_id: submission.user_id, + escalation_reason: submission.escalation_reason || undefined, + } as SubmissionWorkflowDetails, + }); + } + } + // Fetch review lifecycle events // 1. Review creations (recent 7 days) const { data: newReviews, error: newReviewsError } = await supabase @@ -780,6 +880,48 @@ export async function fetchSystemActivities( } } + // Enrich submission workflow users + const submissionWorkflowUserIds = filteredActivities + .filter(a => ['submission_created', 'submission_claimed', 'submission_escalated', 'submission_reassigned'].includes(a.type)) + .flatMap(a => { + const details = a.details as SubmissionWorkflowDetails; + return [details.user_id, details.assigned_to, details.from_moderator, details.to_moderator].filter(Boolean); + }) + .filter(Boolean) as string[]; + + if (submissionWorkflowUserIds.length > 0) { + const { data: submissionProfiles } = await supabase + .from('profiles') + .select('user_id, username') + .in('user_id', submissionWorkflowUserIds); + + if (submissionProfiles) { + const submissionProfileMap = new Map(submissionProfiles.map(p => [p.user_id, p])); + + for (const activity of filteredActivities) { + if (['submission_created', 'submission_claimed', 'submission_escalated', 'submission_reassigned'].includes(activity.type)) { + const details = activity.details as SubmissionWorkflowDetails; + if (details.user_id && !details.username) { + const profile = submissionProfileMap.get(details.user_id); + if (profile) details.username = profile.username; + } + if (details.assigned_to && !details.assigned_username) { + const profile = submissionProfileMap.get(details.assigned_to); + if (profile) details.assigned_username = profile.username; + } + if (details.from_moderator && !details.from_moderator_username) { + const profile = submissionProfileMap.get(details.from_moderator); + if (profile) details.from_moderator_username = profile.username; + } + if (details.to_moderator && !details.to_moderator_username) { + const profile = submissionProfileMap.get(details.to_moderator); + if (profile) details.to_moderator_username = profile.username; + } + } + } + } + } + // Enrich review entity names const parkReviewIds = filteredActivities .filter(a => ['review_created', 'review_deleted'].includes(a.type) && (a.details as ReviewLifecycleDetails).entity_type === 'park')