feat: Implement submission workflow tracking

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 20:33:20 +00:00
parent 9f5e19922c
commit db84e99746
2 changed files with 256 additions and 3 deletions

View File

@@ -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;
}

View File

@@ -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')