mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:51:13 -05:00
feat: Show moderation notes and allow reversing decisions
This commit is contained in:
@@ -28,6 +28,14 @@ interface ModerationItem {
|
||||
};
|
||||
entity_name?: string;
|
||||
park_name?: string;
|
||||
reviewed_at?: string;
|
||||
reviewed_by?: string;
|
||||
reviewer_notes?: string;
|
||||
reviewer_profile?: {
|
||||
username: string;
|
||||
display_name?: string;
|
||||
avatar_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type EntityFilter = 'all' | 'reviews' | 'submissions' | 'photos';
|
||||
@@ -107,6 +115,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
photos,
|
||||
park_id,
|
||||
ride_id,
|
||||
moderated_at,
|
||||
moderated_by,
|
||||
parks:park_id (
|
||||
name
|
||||
),
|
||||
@@ -135,7 +145,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
submission_type,
|
||||
created_at,
|
||||
user_id,
|
||||
status
|
||||
status,
|
||||
reviewed_at,
|
||||
reviewer_id,
|
||||
reviewer_notes
|
||||
`)
|
||||
.in('status', submissionStatuses);
|
||||
|
||||
@@ -193,11 +206,13 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
submissions = submissionsWithEntities;
|
||||
}
|
||||
|
||||
// Get unique user IDs to fetch profiles
|
||||
// Get unique user IDs to fetch profiles (including reviewers)
|
||||
const userIds = [
|
||||
...reviews.map(r => r.user_id),
|
||||
...submissions.map(s => s.user_id)
|
||||
];
|
||||
...submissions.map(s => s.user_id),
|
||||
...reviews.filter(r => r.moderated_by).map(r => r.moderated_by),
|
||||
...submissions.filter(s => s.reviewer_id).map(s => s.reviewer_id)
|
||||
].filter((id, index, arr) => id && arr.indexOf(id) === index); // Remove duplicates and nulls
|
||||
|
||||
// Fetch profiles for all users with avatars
|
||||
const { data: profiles } = await supabase
|
||||
@@ -230,6 +245,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
user_profile: profileMap.get(review.user_id),
|
||||
entity_name,
|
||||
park_name,
|
||||
reviewed_at: review.moderated_at,
|
||||
reviewed_by: review.moderated_by,
|
||||
reviewer_notes: (review as any).reviewer_notes,
|
||||
reviewer_profile: review.moderated_by ? profileMap.get(review.moderated_by) : undefined,
|
||||
};
|
||||
}),
|
||||
...submissions.map(submission => ({
|
||||
@@ -243,6 +262,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
user_profile: profileMap.get(submission.user_id),
|
||||
entity_name: (submission as any).entity_name,
|
||||
park_name: (submission as any).park_name,
|
||||
reviewed_at: submission.reviewed_at,
|
||||
reviewed_by: submission.reviewer_id,
|
||||
reviewer_notes: submission.reviewer_notes,
|
||||
reviewer_profile: submission.reviewer_id ? profileMap.get(submission.reviewer_id) : undefined,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -850,6 +873,73 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Reviewer Information for approved/rejected items */}
|
||||
{(item.status === 'approved' || item.status === 'rejected') && (item.reviewed_at || item.reviewer_notes) && (
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>Reviewed {item.reviewed_at ? format(new Date(item.reviewed_at), 'MMM d, yyyy HH:mm') : 'recently'}</span>
|
||||
{item.reviewer_profile && (
|
||||
<>
|
||||
<span>by</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={item.reviewer_profile.avatar_url} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{(item.reviewer_profile.display_name || item.reviewer_profile.username)?.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="font-medium">
|
||||
{item.reviewer_profile.display_name || item.reviewer_profile.username}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{item.reviewer_notes && (
|
||||
<div className="bg-muted/30 p-3 rounded-lg">
|
||||
<p className="text-sm font-medium mb-1">Reviewer Notes:</p>
|
||||
<p className="text-sm text-muted-foreground">{item.reviewer_notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reverse Decision Buttons */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">Reverse Decision</Label>
|
||||
<Textarea
|
||||
placeholder="Add notes about reversing this decision..."
|
||||
value={notes[`reverse-${item.id}`] || ''}
|
||||
onChange={(e) => setNotes(prev => ({ ...prev, [`reverse-${item.id}`]: e.target.value }))}
|
||||
rows={2}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{item.status === 'approved' && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => handleModerationAction(item, 'rejected', notes[`reverse-${item.id}`])}
|
||||
disabled={actionLoading === item.id}
|
||||
className="flex-1"
|
||||
>
|
||||
<XCircle className="w-4 h-4 mr-2" />
|
||||
Change to Rejected
|
||||
</Button>
|
||||
)}
|
||||
{item.status === 'rejected' && (
|
||||
<Button
|
||||
onClick={() => handleModerationAction(item, 'approved', notes[`reverse-${item.id}`])}
|
||||
disabled={actionLoading === item.id}
|
||||
className="flex-1"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Change to Approved
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete button for rejected submissions (admin/superadmin only) */}
|
||||
{item.status === 'rejected' && item.type === 'content_submission' && (isAdmin() || isSuperuser()) && (
|
||||
<div className="pt-2">
|
||||
|
||||
Reference in New Issue
Block a user