mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:31:12 -05:00
feat: Implement complete queue system
This commit is contained in:
@@ -1,14 +1,29 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
interface EscalationDialogProps {
|
interface EscalationDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onEscalate: (reason: string) => void;
|
onEscalate: (reason: string) => Promise<void>;
|
||||||
|
submissionType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const escalationReasons = [
|
const escalationReasons = [
|
||||||
@@ -24,30 +39,40 @@ export function EscalationDialog({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onEscalate,
|
onEscalate,
|
||||||
|
submissionType,
|
||||||
}: EscalationDialogProps) {
|
}: EscalationDialogProps) {
|
||||||
const [selectedReason, setSelectedReason] = useState('');
|
const [selectedReason, setSelectedReason] = useState('');
|
||||||
const [additionalNotes, setAdditionalNotes] = useState('');
|
const [additionalNotes, setAdditionalNotes] = useState('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleEscalate = () => {
|
const handleEscalate = async () => {
|
||||||
const reason = selectedReason === 'Other'
|
const reason = selectedReason === 'Other'
|
||||||
? additionalNotes
|
? additionalNotes
|
||||||
: `${selectedReason}${additionalNotes ? ': ' + additionalNotes : ''}`;
|
: `${selectedReason}${additionalNotes ? ': ' + additionalNotes : ''}`;
|
||||||
|
|
||||||
onEscalate(reason);
|
if (!reason.trim()) return;
|
||||||
onOpenChange(false);
|
|
||||||
|
setIsSubmitting(true);
|
||||||
// Reset form
|
try {
|
||||||
setSelectedReason('');
|
await onEscalate(reason);
|
||||||
setAdditionalNotes('');
|
setSelectedReason('');
|
||||||
|
setAdditionalNotes('');
|
||||||
|
onOpenChange(false);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Escalate to Admin</DialogTitle>
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||||
|
Escalate Submission
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
This submission will be flagged for admin review. Please provide a reason.
|
Escalating this {submissionType} will mark it as high priority and notify senior moderators.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -75,16 +100,25 @@ export function EscalationDialog({
|
|||||||
onChange={(e) => setAdditionalNotes(e.target.value)}
|
onChange={(e) => setAdditionalNotes(e.target.value)}
|
||||||
placeholder="Provide any additional context..."
|
placeholder="Provide any additional context..."
|
||||||
rows={4}
|
rows={4}
|
||||||
|
className="resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleEscalate} disabled={!selectedReason}>
|
<Button
|
||||||
Escalate
|
variant="destructive"
|
||||||
|
onClick={handleEscalate}
|
||||||
|
disabled={!selectedReason || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Escalating...' : 'Escalate'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
import { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||||
import { CheckCircle, XCircle, Eye, Calendar, User, Filter, MessageSquare, FileText, Image, X, Trash2, ListTree, RefreshCw, AlertCircle, Clock, Lock, Unlock } from 'lucide-react';
|
import { CheckCircle, XCircle, Eye, Calendar, User, Filter, MessageSquare, FileText, Image, X, Trash2, ListTree, RefreshCw, AlertCircle, Clock, Lock, Unlock, AlertTriangle, UserCog, Zap } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
@@ -21,6 +21,9 @@ import { MeasurementDisplay } from '@/components/ui/measurement-display';
|
|||||||
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
||||||
import { useModerationQueue } from '@/hooks/useModerationQueue';
|
import { useModerationQueue } from '@/hooks/useModerationQueue';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { QueueStatsDashboard } from './QueueStatsDashboard';
|
||||||
|
import { EscalationDialog } from './EscalationDialog';
|
||||||
|
import { ReassignDialog } from './ReassignDialog';
|
||||||
|
|
||||||
interface ModerationItem {
|
interface ModerationItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -69,6 +72,9 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
const [reviewManagerOpen, setReviewManagerOpen] = useState(false);
|
const [reviewManagerOpen, setReviewManagerOpen] = useState(false);
|
||||||
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
||||||
const [lockedSubmissions, setLockedSubmissions] = useState<Set<string>>(new Set());
|
const [lockedSubmissions, setLockedSubmissions] = useState<Set<string>>(new Set());
|
||||||
|
const [escalationDialogOpen, setEscalationDialogOpen] = useState(false);
|
||||||
|
const [reassignDialogOpen, setReassignDialogOpen] = useState(false);
|
||||||
|
const [selectedItemForAction, setSelectedItemForAction] = useState<ModerationItem | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { isAdmin, isSuperuser } = useUserRole();
|
const { isAdmin, isSuperuser } = useUserRole();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|||||||
90
src/components/moderation/QueueStatsDashboard.tsx
Normal file
90
src/components/moderation/QueueStatsDashboard.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Clock, AlertTriangle, CheckCircle, Users } from 'lucide-react';
|
||||||
|
import { useModerationQueue } from '@/hooks/useModerationQueue';
|
||||||
|
|
||||||
|
export function QueueStatsDashboard() {
|
||||||
|
const { queueStats } = useModerationQueue();
|
||||||
|
|
||||||
|
if (!queueStats) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSLAStatus = (avgWaitHours: number) => {
|
||||||
|
if (avgWaitHours < 24) return 'good';
|
||||||
|
if (avgWaitHours < 48) return 'warning';
|
||||||
|
return 'critical';
|
||||||
|
};
|
||||||
|
|
||||||
|
const slaStatus = getSLAStatus(queueStats.avgWaitHours);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Pending Queue</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{queueStats.pendingCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Total submissions waiting
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Assigned to Me</CardTitle>
|
||||||
|
<CheckCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{queueStats.assignedToMe}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Currently locked by you
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Avg Wait Time</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{queueStats.avgWaitHours.toFixed(1)}h
|
||||||
|
</div>
|
||||||
|
{slaStatus === 'warning' && (
|
||||||
|
<Badge variant="outline" className="bg-warning/10 text-warning border-warning/20">
|
||||||
|
Warning
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{slaStatus === 'critical' && (
|
||||||
|
<Badge variant="destructive">Critical</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Average time in queue
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">High Priority</CardTitle>
|
||||||
|
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-destructive">
|
||||||
|
{queueStats.highPriorityCount}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Escalated submissions
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
170
src/components/moderation/ReassignDialog.tsx
Normal file
170
src/components/moderation/ReassignDialog.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { UserCog } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
interface Moderator {
|
||||||
|
user_id: string;
|
||||||
|
username: string;
|
||||||
|
display_name?: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReassignDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onReassign: (moderatorId: string) => Promise<void>;
|
||||||
|
submissionType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReassignDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onReassign,
|
||||||
|
submissionType,
|
||||||
|
}: ReassignDialogProps) {
|
||||||
|
const [selectedModerator, setSelectedModerator] = useState<string>('');
|
||||||
|
const [moderators, setModerators] = useState<Moderator[]>([]);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchModerators();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const fetchModerators = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data: roles, error: rolesError } = await supabase
|
||||||
|
.from('user_roles')
|
||||||
|
.select('user_id, role')
|
||||||
|
.in('role', ['moderator', 'admin', 'superuser']);
|
||||||
|
|
||||||
|
if (rolesError) throw rolesError;
|
||||||
|
|
||||||
|
if (!roles || roles.length === 0) {
|
||||||
|
setModerators([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = roles.map((r) => r.user_id);
|
||||||
|
|
||||||
|
const { data: profiles, error: profilesError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('user_id, username, display_name')
|
||||||
|
.in('user_id', userIds);
|
||||||
|
|
||||||
|
if (profilesError) throw profilesError;
|
||||||
|
|
||||||
|
const moderatorsList = roles.map((role) => {
|
||||||
|
const profile = profiles?.find((p) => p.user_id === role.user_id);
|
||||||
|
return {
|
||||||
|
user_id: role.user_id,
|
||||||
|
username: profile?.username || 'Unknown',
|
||||||
|
display_name: profile?.display_name,
|
||||||
|
role: role.role,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setModerators(moderatorsList);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching moderators:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to load moderators list',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReassign = async () => {
|
||||||
|
if (!selectedModerator) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onReassign(selectedModerator);
|
||||||
|
setSelectedModerator('');
|
||||||
|
onOpenChange(false);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<UserCog className="h-5 w-5" />
|
||||||
|
Reassign Submission
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Assign this {submissionType} to another moderator. They will receive a lock for 15 minutes.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="moderator">Select Moderator</Label>
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-sm text-muted-foreground">Loading moderators...</div>
|
||||||
|
) : moderators.length === 0 ? (
|
||||||
|
<div className="text-sm text-muted-foreground">No moderators available</div>
|
||||||
|
) : (
|
||||||
|
<Select value={selectedModerator} onValueChange={setSelectedModerator}>
|
||||||
|
<SelectTrigger id="moderator">
|
||||||
|
<SelectValue placeholder="Choose a moderator" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{moderators.map((mod) => (
|
||||||
|
<SelectItem key={mod.user_id} value={mod.user_id}>
|
||||||
|
{mod.display_name || mod.username} ({mod.role})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleReassign}
|
||||||
|
disabled={!selectedModerator || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Reassigning...' : 'Reassign'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ export function SubmissionReviewManager({
|
|||||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
|
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items');
|
const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items');
|
||||||
|
const [submissionType, setSubmissionType] = useState<string>('submission');
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { isAdmin, isSuperuser } = useUserRole();
|
const { isAdmin, isSuperuser } = useUserRole();
|
||||||
@@ -68,6 +69,19 @@ export function SubmissionReviewManager({
|
|||||||
const loadSubmissionItems = async () => {
|
const loadSubmissionItems = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const { supabase } = await import('@/integrations/supabase/client');
|
||||||
|
|
||||||
|
// Fetch submission type
|
||||||
|
const { data: submission } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.select('submission_type')
|
||||||
|
.eq('id', submissionId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submission) {
|
||||||
|
setSubmissionType(submission.submission_type || 'submission');
|
||||||
|
}
|
||||||
|
|
||||||
const fetchedItems = await fetchSubmissionItems(submissionId);
|
const fetchedItems = await fetchSubmissionItems(submissionId);
|
||||||
const itemsWithDeps = buildDependencyTree(fetchedItems);
|
const itemsWithDeps = buildDependencyTree(fetchedItems);
|
||||||
setItems(itemsWithDeps);
|
setItems(itemsWithDeps);
|
||||||
@@ -400,6 +414,7 @@ export function SubmissionReviewManager({
|
|||||||
open={showEscalationDialog}
|
open={showEscalationDialog}
|
||||||
onOpenChange={setShowEscalationDialog}
|
onOpenChange={setShowEscalationDialog}
|
||||||
onEscalate={handleEscalate}
|
onEscalate={handleEscalate}
|
||||||
|
submissionType={submissionType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RejectionDialog
|
<RejectionDialog
|
||||||
|
|||||||
@@ -266,6 +266,90 @@ export const useModerationQueue = () => {
|
|||||||
return Math.max(0, currentLock.expiresAt.getTime() - Date.now());
|
return Math.max(0, currentLock.expiresAt.getTime() - Date.now());
|
||||||
}, [currentLock]);
|
}, [currentLock]);
|
||||||
|
|
||||||
|
// Escalate submission
|
||||||
|
const escalateSubmission = useCallback(async (submissionId: string, reason: string): Promise<boolean> => {
|
||||||
|
if (!user?.id) return false;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.update({
|
||||||
|
escalated: true,
|
||||||
|
escalated_at: new Date().toISOString(),
|
||||||
|
escalated_by: user.id,
|
||||||
|
escalation_reason: reason,
|
||||||
|
priority: 10, // Max priority
|
||||||
|
})
|
||||||
|
.eq('id', submissionId);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Submission Escalated',
|
||||||
|
description: 'This submission has been marked as high priority',
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchStats();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error escalating submission:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: error.message || 'Failed to escalate submission',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [user, toast, fetchStats]);
|
||||||
|
|
||||||
|
// Reassign submission
|
||||||
|
const reassignSubmission = useCallback(async (submissionId: string, newModeratorId: string): Promise<boolean> => {
|
||||||
|
if (!user?.id) return false;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.update({
|
||||||
|
assigned_to: newModeratorId,
|
||||||
|
assigned_at: new Date().toISOString(),
|
||||||
|
locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
|
||||||
|
})
|
||||||
|
.eq('id', submissionId);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// If this was our lock, clear it
|
||||||
|
if (currentLock?.submissionId === submissionId) {
|
||||||
|
setCurrentLock(null);
|
||||||
|
if (lockTimerRef.current) {
|
||||||
|
clearInterval(lockTimerRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Submission Reassigned',
|
||||||
|
description: 'The submission has been assigned to another moderator',
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchStats();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error reassigning submission:', error);
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: error.message || 'Failed to reassign submission',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [user, currentLock, toast, fetchStats]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentLock,
|
currentLock,
|
||||||
queueStats,
|
queueStats,
|
||||||
@@ -274,6 +358,8 @@ export const useModerationQueue = () => {
|
|||||||
extendLock,
|
extendLock,
|
||||||
releaseLock,
|
releaseLock,
|
||||||
getTimeRemaining,
|
getTimeRemaining,
|
||||||
|
escalateSubmission,
|
||||||
|
reassignSubmission,
|
||||||
refreshStats: fetchStats,
|
refreshStats: fetchStats,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user