mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
feat: Implement submission workflow tracking
This commit is contained in:
@@ -41,7 +41,8 @@ import {
|
|||||||
ReviewModerationDetails,
|
ReviewModerationDetails,
|
||||||
PhotoApprovalDetails,
|
PhotoApprovalDetails,
|
||||||
AccountLifecycleDetails,
|
AccountLifecycleDetails,
|
||||||
ReviewLifecycleDetails
|
ReviewLifecycleDetails,
|
||||||
|
SubmissionWorkflowDetails
|
||||||
} from '@/lib/systemActivityService';
|
} from '@/lib/systemActivityService';
|
||||||
|
|
||||||
export interface SystemActivityLogRef {
|
export interface SystemActivityLogRef {
|
||||||
@@ -138,6 +139,30 @@ const activityTypeConfig = {
|
|||||||
bgColor: 'bg-red-600/10',
|
bgColor: 'bg-red-600/10',
|
||||||
label: 'Review Deleted',
|
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>(
|
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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ export type ActivityType =
|
|||||||
| 'user_banned'
|
| 'user_banned'
|
||||||
| 'user_unbanned'
|
| 'user_unbanned'
|
||||||
| 'review_created'
|
| 'review_created'
|
||||||
| 'review_deleted';
|
| 'review_deleted'
|
||||||
|
| 'submission_created'
|
||||||
|
| 'submission_claimed'
|
||||||
|
| 'submission_escalated'
|
||||||
|
| 'submission_reassigned';
|
||||||
|
|
||||||
export interface ActivityActor {
|
export interface ActivityActor {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -99,6 +103,20 @@ export interface ReviewLifecycleDetails {
|
|||||||
was_moderated?: boolean;
|
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 =
|
export type ActivityDetails =
|
||||||
| EntityChangeDetails
|
| EntityChangeDetails
|
||||||
| AdminActionDetails
|
| AdminActionDetails
|
||||||
@@ -107,7 +125,8 @@ export type ActivityDetails =
|
|||||||
| ReviewModerationDetails
|
| ReviewModerationDetails
|
||||||
| PhotoApprovalDetails
|
| PhotoApprovalDetails
|
||||||
| AccountLifecycleDetails
|
| AccountLifecycleDetails
|
||||||
| ReviewLifecycleDetails;
|
| ReviewLifecycleDetails
|
||||||
|
| SubmissionWorkflowDetails;
|
||||||
|
|
||||||
export interface SystemActivity {
|
export interface SystemActivity {
|
||||||
id: string;
|
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
|
// Fetch review lifecycle events
|
||||||
// 1. Review creations (recent 7 days)
|
// 1. Review creations (recent 7 days)
|
||||||
const { data: newReviews, error: newReviewsError } = await supabase
|
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
|
// Enrich review entity names
|
||||||
const parkReviewIds = filteredActivities
|
const parkReviewIds = filteredActivities
|
||||||
.filter(a => ['review_created', 'review_deleted'].includes(a.type) && (a.details as ReviewLifecycleDetails).entity_type === 'park')
|
.filter(a => ['review_created', 'review_deleted'].includes(a.type) && (a.details as ReviewLifecycleDetails).entity_type === 'park')
|
||||||
|
|||||||
Reference in New Issue
Block a user