mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 12:31:14 -05:00
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
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<string, any>;
|
|
}
|
|
|
|
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<SystemActivity[]> {
|
|
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;
|
|
}
|