mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:51:13 -05:00
feat: Implement submission workflow tracking
This commit is contained in:
@@ -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<SystemActivityLogRef, SystemActivityLogProps>(
|
||||
@@ -565,6 +590,92 @@ export const SystemActivityLog = forwardRef<SystemActivityLogRef, SystemActivity
|
||||
);
|
||||
}
|
||||
|
||||
case 'submission_created': {
|
||||
const details = activity.details as SubmissionWorkflowDetails;
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm flex-wrap">
|
||||
<Badge className="bg-blue-500/10 text-blue-500">New Submission</Badge>
|
||||
<span className="text-muted-foreground capitalize">{details.submission_type.replace(/_/g, ' ')}</span>
|
||||
{details.username && (
|
||||
<span className="font-medium">by @{details.username}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'submission_claimed': {
|
||||
const details = activity.details as SubmissionWorkflowDetails;
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm flex-wrap">
|
||||
<Badge className="bg-indigo-500/10 text-indigo-500">Claimed</Badge>
|
||||
<span className="text-muted-foreground capitalize">{details.submission_type.replace(/_/g, ' ')}</span>
|
||||
{details.username && (
|
||||
<span className="text-muted-foreground">by @{details.username}</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && details.assigned_username && (
|
||||
<div className="flex items-center gap-2 p-2 bg-muted rounded text-sm">
|
||||
<UserCog className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
Assigned to <strong>@{details.assigned_username}</strong>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'submission_escalated': {
|
||||
const details = activity.details as SubmissionWorkflowDetails;
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm flex-wrap">
|
||||
<Badge className="bg-orange-600/10 text-orange-600">Escalated</Badge>
|
||||
<span className="text-muted-foreground capitalize">{details.submission_type.replace(/_/g, ' ')}</span>
|
||||
</div>
|
||||
{isExpanded && details.escalation_reason && (
|
||||
<div className="flex items-start gap-2 p-2 bg-orange-500/5 rounded text-sm">
|
||||
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0 text-orange-600" />
|
||||
<span className="text-muted-foreground">
|
||||
<strong>Reason:</strong> {details.escalation_reason}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'submission_reassigned': {
|
||||
const details = activity.details as SubmissionWorkflowDetails;
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm flex-wrap">
|
||||
<Badge className="bg-purple-600/10 text-purple-600">Reassigned</Badge>
|
||||
<span className="text-muted-foreground capitalize">{details.submission_type.replace(/_/g, ' ')}</span>
|
||||
</div>
|
||||
{isExpanded && (details.from_moderator_username || details.to_moderator_username) && (
|
||||
<div className="flex items-center gap-2 p-2 bg-muted rounded text-sm">
|
||||
<History className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
{details.from_moderator_username && details.to_moderator_username ? (
|
||||
<>
|
||||
From <strong>@{details.from_moderator_username}</strong> to <strong>@{details.to_moderator_username}</strong>
|
||||
</>
|
||||
) : details.to_moderator_username ? (
|
||||
<>
|
||||
To <strong>@{details.to_moderator_username}</strong>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user