Implement complete roadmap

This commit is contained in:
gpt-engineer-app[bot]
2025-11-02 21:22:03 +00:00
parent 665918741e
commit 41560d9c42
16 changed files with 770 additions and 144 deletions

View File

@@ -0,0 +1,59 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
interface ConfirmationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmLabel?: string;
cancelLabel?: string;
onConfirm: () => void;
variant?: 'default' | 'destructive';
}
export const ConfirmationDialog = ({
open,
onOpenChange,
title,
description,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
onConfirm,
variant = 'default',
}: ConfirmationDialogProps) => {
const handleConfirm = () => {
onConfirm();
onOpenChange(false);
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{cancelLabel}</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirm}
className={variant === 'destructive' ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : ''}
>
{confirmLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
ConfirmationDialog.displayName = 'ConfirmationDialog';

View File

@@ -49,3 +49,5 @@ export const EmptyQueueState = ({
</div>
);
};
EmptyQueueState.displayName = 'EmptyQueueState';

View File

@@ -0,0 +1,93 @@
import { CheckCircle, Search, PartyPopper, HelpCircle, LucideIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import type { EntityFilter, StatusFilter } from '@/types/moderation';
interface EnhancedEmptyStateProps {
entityFilter: EntityFilter;
statusFilter: StatusFilter;
onClearFilters?: () => void;
onLearnMore?: () => void;
}
type EmptyStateVariant = {
icon: LucideIcon;
title: string;
description: string;
action?: {
label: string;
onClick: () => void;
};
};
const getEmptyStateVariant = (
entityFilter: EntityFilter,
statusFilter: StatusFilter,
onClearFilters?: () => void,
onLearnMore?: () => void
): EmptyStateVariant => {
const entityLabel = entityFilter === 'all' ? 'items' :
entityFilter === 'reviews' ? 'reviews' :
entityFilter === 'photos' ? 'photos' : 'submissions';
// Success state: No pending items
if (statusFilter === 'pending' && entityFilter === 'all') {
return {
icon: PartyPopper,
title: 'All caught up!',
description: 'No pending items require moderation at this time. Great work!',
};
}
// Filtered but no results: Suggest clearing filters
if (entityFilter !== 'all' || statusFilter !== 'pending') {
return {
icon: Search,
title: `No ${entityLabel} found`,
description: `No ${entityLabel} match your current filters. Try clearing filters to see all items.`,
action: onClearFilters ? {
label: 'Clear Filters',
onClick: onClearFilters,
} : undefined,
};
}
// First-time user: Onboarding
return {
icon: HelpCircle,
title: 'Welcome to the Moderation Queue',
description: 'Submissions will appear here when users contribute content. Claim, review, and approve or reject items.',
action: onLearnMore ? {
label: 'Learn More',
onClick: onLearnMore,
} : undefined,
};
};
export const EnhancedEmptyState = ({
entityFilter,
statusFilter,
onClearFilters,
onLearnMore
}: EnhancedEmptyStateProps) => {
const variant = getEmptyStateVariant(entityFilter, statusFilter, onClearFilters, onLearnMore);
const Icon = variant.icon;
return (
<div className="flex flex-col items-center justify-center py-12 px-4">
<div className="rounded-full bg-primary/10 p-4 mb-4">
<Icon className="w-8 h-8 text-primary" />
</div>
<h3 className="text-xl font-semibold mb-2 text-foreground">{variant.title}</h3>
<p className="text-muted-foreground text-center max-w-md mb-6">
{variant.description}
</p>
{variant.action && (
<Button onClick={variant.action.onClick} variant="outline">
{variant.action.label}
</Button>
)}
</div>
);
};
EnhancedEmptyState.displayName = 'EnhancedEmptyState';

View File

@@ -0,0 +1,164 @@
import { useState, useEffect, useMemo } from 'react';
import { Clock, AlertTriangle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
interface LockState {
submissionId: string;
expiresAt: Date;
}
interface QueueStats {
pendingCount: number;
assignedToMe: number;
avgWaitHours: number;
}
interface EnhancedLockStatusDisplayProps {
currentLock: LockState | null;
queueStats: QueueStats | null;
loading: boolean;
onExtendLock: () => void;
onReleaseLock: () => void;
getCurrentTime: () => Date;
}
const LOCK_DURATION_MS = 15 * 60 * 1000; // 15 minutes
const WARNING_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
const CRITICAL_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
export const EnhancedLockStatusDisplay = ({
currentLock,
queueStats,
loading,
onExtendLock,
onReleaseLock,
getCurrentTime,
}: EnhancedLockStatusDisplayProps) => {
const [timeLeft, setTimeLeft] = useState<number>(0);
useEffect(() => {
if (!currentLock) return;
const updateTimer = () => {
const now = getCurrentTime();
const remaining = currentLock.expiresAt.getTime() - now.getTime();
setTimeLeft(Math.max(0, remaining));
};
updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [currentLock, getCurrentTime]);
const { urgency, progressPercent } = useMemo(() => {
if (timeLeft <= 0) return { urgency: 'expired', progressPercent: 0 };
if (timeLeft <= CRITICAL_THRESHOLD_MS) return { urgency: 'critical', progressPercent: (timeLeft / LOCK_DURATION_MS) * 100 };
if (timeLeft <= WARNING_THRESHOLD_MS) return { urgency: 'warning', progressPercent: (timeLeft / LOCK_DURATION_MS) * 100 };
return { urgency: 'safe', progressPercent: (timeLeft / LOCK_DURATION_MS) * 100 };
}, [timeLeft]);
const formatTime = (ms: number): string => {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const showExtendButton = timeLeft > 0 && timeLeft <= WARNING_THRESHOLD_MS;
if (!currentLock) {
return (
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="w-4 h-4" />
<span>No submission claimed</span>
</div>
{queueStats && (
<div className="text-sm text-muted-foreground">
{queueStats.pendingCount} pending
</div>
)}
</div>
);
}
return (
<div
className={cn(
'p-4 rounded-lg border transition-colors',
urgency === 'critical' && 'bg-destructive/10 border-destructive animate-pulse',
urgency === 'warning' && 'bg-yellow-500/10 border-yellow-500',
urgency === 'safe' && 'bg-primary/10 border-primary'
)}
data-testid="lock-status-display"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Clock className={cn(
'w-4 h-4',
urgency === 'critical' && 'text-destructive',
urgency === 'warning' && 'text-yellow-600',
urgency === 'safe' && 'text-primary'
)} />
<span className="text-sm font-medium">Lock expires in</span>
</div>
<Badge
variant={urgency === 'critical' ? 'destructive' : urgency === 'warning' ? 'default' : 'secondary'}
className={cn(
'font-mono text-base',
urgency === 'critical' && 'animate-pulse'
)}
>
{formatTime(timeLeft)}
</Badge>
</div>
<Progress
value={progressPercent}
className={cn(
'h-2 mb-3',
urgency === 'critical' && '[&>div]:bg-destructive',
urgency === 'warning' && '[&>div]:bg-yellow-500',
urgency === 'safe' && '[&>div]:bg-primary'
)}
/>
{urgency === 'critical' && (
<div className="flex items-start gap-2 mb-3 text-sm text-destructive">
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span className="font-medium">Lock expiring soon! Extend or release.</span>
</div>
)}
<div className="flex gap-2">
{showExtendButton && (
<Button
onClick={onExtendLock}
disabled={loading}
size="sm"
variant={urgency === 'critical' ? 'default' : 'outline'}
className="flex-1"
>
<Clock className="w-4 h-4 mr-2" />
Extend Lock (+15min)
</Button>
)}
<Button
onClick={onReleaseLock}
disabled={loading}
size="sm"
variant="outline"
className={!showExtendButton ? 'flex-1' : ''}
>
Release Lock
</Button>
</div>
</div>
);
};
EnhancedLockStatusDisplay.displayName = 'EnhancedLockStatusDisplay';

View File

@@ -0,0 +1,55 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Keyboard } from 'lucide-react';
interface KeyboardShortcut {
key: string;
description: string;
}
interface KeyboardShortcutsHelpProps {
open: boolean;
onOpenChange: (open: boolean) => void;
shortcuts: KeyboardShortcut[];
}
export const KeyboardShortcutsHelp = ({
open,
onOpenChange,
shortcuts,
}: KeyboardShortcutsHelpProps) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Keyboard className="w-5 h-5" />
Keyboard Shortcuts
</DialogTitle>
<DialogDescription>
Speed up your moderation workflow with these keyboard shortcuts
</DialogDescription>
</DialogHeader>
<div className="space-y-3 py-4">
{shortcuts.map((shortcut, index) => (
<div key={index} className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{shortcut.description}
</span>
<kbd className="px-2 py-1 text-xs font-semibold text-foreground bg-muted border border-border rounded">
{shortcut.key}
</kbd>
</div>
))}
</div>
</DialogContent>
</Dialog>
);
};
KeyboardShortcutsHelp.displayName = 'KeyboardShortcutsHelp';

View File

@@ -1,4 +1,4 @@
import { useState, useImperativeHandle, forwardRef, useMemo } from 'react';
import { useState, useImperativeHandle, forwardRef, useMemo, useCallback } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { TooltipProvider } from '@/components/ui/tooltip';
import { useToast } from '@/hooks/use-toast';
@@ -14,15 +14,18 @@ import { useModerationQueueManager } from '@/hooks/moderation';
import { QueueItem } from './QueueItem';
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';
import { QueueSkeleton } from './QueueSkeleton';
import { LockStatusDisplay } from './LockStatusDisplay';
import { EnhancedLockStatusDisplay } from './EnhancedLockStatusDisplay';
import { getLockStatus } from '@/lib/moderation/lockHelpers';
import { QueueStats } from './QueueStats';
import { QueueFilters } from './QueueFilters';
import { ActiveFiltersDisplay } from './ActiveFiltersDisplay';
import { AutoRefreshIndicator } from './AutoRefreshIndicator';
import { NewItemsAlert } from './NewItemsAlert';
import { EmptyQueueState } from './EmptyQueueState';
import { EnhancedEmptyState } from './EnhancedEmptyState';
import { QueuePagination } from './QueuePagination';
import { ConfirmationDialog } from './ConfirmationDialog';
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
import type { ModerationQueueRef } from '@/types/moderation';
import type { PhotoItem } from '@/types/photos';
@@ -75,11 +78,68 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
const [showItemEditDialog, setShowItemEditDialog] = useState(false);
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
// Confirmation dialog state
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean;
title: string;
description: string;
onConfirm: () => void;
}>({
open: false,
title: '',
description: '',
onConfirm: () => {},
});
// Keyboard shortcuts help dialog
const [showShortcutsHelp, setShowShortcutsHelp] = useState(false);
// UI-specific handlers
const handleNoteChange = (id: string, value: string) => {
setNotes(prev => ({ ...prev, [id]: value }));
};
// Wrapped delete with confirmation
const handleDeleteSubmission = useCallback((item: any) => {
setConfirmDialog({
open: true,
title: 'Delete Submission',
description: 'Are you sure you want to permanently delete this submission? This action cannot be undone.',
onConfirm: () => queueManager.deleteSubmission(item),
});
}, [queueManager]);
// Clear filters handler
const handleClearFilters = useCallback(() => {
queueManager.filters.clearFilters();
}, [queueManager.filters]);
// Keyboard shortcuts
const { shortcuts } = useKeyboardShortcuts({
shortcuts: [
{
key: '?',
handler: () => setShowShortcutsHelp(true),
description: 'Show keyboard shortcuts',
},
{
key: 'r',
handler: () => queueManager.refresh(),
description: 'Refresh queue',
},
{
key: 'k',
ctrlOrCmd: true,
handler: () => {
// Focus search/filter (if implemented)
document.querySelector<HTMLInputElement>('[data-filter-search]')?.focus();
},
description: 'Focus filters',
},
],
enabled: true,
});
const handleOpenPhotos = (photos: any[], index: number) => {
setSelectedPhotos(photos);
setSelectedPhotoIndex(index);
@@ -135,14 +195,13 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<QueueStats stats={queueManager.queue.queueStats} isMobile={isMobile} />
<LockStatusDisplay
<EnhancedLockStatusDisplay
currentLock={queueManager.queue.currentLock}
queueStats={queueManager.queue.queueStats}
isLoading={queueManager.queue.isLoading}
onExtendLock={queueManager.queue.extendLock}
onReleaseLock={queueManager.queue.releaseLock}
getTimeRemaining={queueManager.queue.getTimeRemaining}
getLockProgress={queueManager.queue.getLockProgress}
loading={queueManager.queue.isLoading}
onExtendLock={() => queueManager.queue.extendLock(queueManager.queue.currentLock?.submissionId || '')}
onReleaseLock={() => queueManager.queue.releaseLock(queueManager.queue.currentLock?.submissionId || '', false)}
getCurrentTime={() => new Date()}
/>
</div>
</CardContent>
@@ -192,9 +251,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
{queueManager.loadingState === 'loading' || queueManager.loadingState === 'initial' ? (
<QueueSkeleton count={queueManager.pagination.pageSize} />
) : queueManager.items.length === 0 ? (
<EmptyQueueState
<EnhancedEmptyState
entityFilter={queueManager.filters.entityFilter}
statusFilter={queueManager.filters.statusFilter}
onClearFilters={queueManager.filters.hasActiveFilters ? handleClearFilters : undefined}
/>
) : (
<TooltipProvider>
@@ -222,7 +282,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
onOpenReviewManager={handleOpenReviewManager}
onOpenItemEditor={handleOpenItemEditor}
onClaimSubmission={queueManager.queue.claimSubmission}
onDeleteSubmission={queueManager.deleteSubmission}
onDeleteSubmission={handleDeleteSubmission}
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
/>
@@ -274,6 +334,24 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
}}
/>
)}
{/* Confirmation Dialog */}
<ConfirmationDialog
open={confirmDialog.open}
onOpenChange={(open) => setConfirmDialog(prev => ({ ...prev, open }))}
title={confirmDialog.title}
description={confirmDialog.description}
onConfirm={confirmDialog.onConfirm}
variant="destructive"
confirmLabel="Delete"
/>
{/* Keyboard Shortcuts Help */}
<KeyboardShortcutsHelp
open={showShortcutsHelp}
onOpenChange={setShowShortcutsHelp}
shortcuts={shortcuts}
/>
</div>
);
});

View File

@@ -201,68 +201,12 @@ export const QueueItem = memo(({
})()}
</div>
) : item.submission_type === 'photo' ? (
<div>
<div className="text-sm text-muted-foreground mb-3">
Photo Submission
</div>
{/* Submission Title */}
{item.content.title && (
<div className="mb-3">
<div className="text-sm font-medium mb-1">Title:</div>
<p className="text-sm">{item.content.title}</p>
</div>
)}
{/* Photos from relational table */}
{photosLoading ? (
<div className="text-sm text-muted-foreground">Loading photos...</div>
) : photoItems.length > 0 ? (
<div className="space-y-2">
<div className="text-sm font-medium flex items-center justify-between">
<span>Photos ({photoItems.length}):</span>
{import.meta.env.DEV && photoItems[0] && (
<span className="text-xs text-muted-foreground">
URL: {photoItems[0].cloudflare_image_url?.slice(0, 30)}...
</span>
)}
</div>
<PhotoGrid
photos={photoItems.map(photo => ({
id: photo.id,
url: photo.cloudflare_image_url,
filename: photo.filename || `Photo ${photo.order_index + 1}`,
caption: photo.caption,
title: photo.title,
date_taken: photo.date_taken,
}))}
onPhotoClick={onOpenPhotos}
/>
</div>
) : (
<Alert variant="destructive" className="mt-4">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>No Photos Found</AlertTitle>
<AlertDescription>
This photo submission has no photos attached. This may be a data integrity issue.
</AlertDescription>
</Alert>
)}
{/* Context Information */}
{item.entity_name && (
<div className="mt-3 pt-3 border-t text-sm">
<span className="text-muted-foreground">For: </span>
<span className="font-medium">{item.entity_name}</span>
{item.park_name && (
<>
<span className="text-muted-foreground"> at </span>
<span className="font-medium">{item.park_name}</span>
</>
)}
</div>
)}
</div>
<PhotoSubmissionDisplay
item={item}
photoItems={photoItems}
loading={photosLoading}
onOpenPhotos={onOpenPhotos}
/>
) : (
<>
{/* Main content area - spans 1st column on all layouts */}

View File

@@ -159,3 +159,5 @@ export const QueuePagination = ({
</div>
);
};
QueuePagination.displayName = 'QueuePagination';

View File

@@ -27,3 +27,5 @@ export const QueueStats = ({ stats, isMobile }: QueueStatsProps) => {
</div>
);
};
QueueStats.displayName = 'QueueStats';

View File

@@ -1,4 +1,4 @@
import { memo } from 'react';
import { memo, useCallback } from 'react';
import {
CheckCircle, XCircle, RefreshCw, AlertCircle, Lock, Trash2,
Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar
@@ -60,6 +60,59 @@ export const QueueItemActions = memo(({
onInteractionBlur,
onClaim
}: QueueItemActionsProps) => {
// Memoize all handlers to prevent re-renders
const handleNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
onNoteChange(item.id, e.target.value);
}, [onNoteChange, item.id]);
const handleApprove = useCallback(() => {
onApprove(item, 'approved', notes[item.id]);
}, [onApprove, item, notes]);
const handleReject = useCallback(() => {
onApprove(item, 'rejected', notes[item.id]);
}, [onApprove, item, notes]);
const handleResetToPending = useCallback(() => {
onResetToPending(item);
}, [onResetToPending, item]);
const handleRetryFailed = useCallback(() => {
onRetryFailed(item);
}, [onRetryFailed, item]);
const handleOpenReviewManager = useCallback(() => {
onOpenReviewManager(item.id);
}, [onOpenReviewManager, item.id]);
const handleOpenItemEditor = useCallback(() => {
onOpenItemEditor(item.id);
}, [onOpenItemEditor, item.id]);
const handleDeleteSubmission = useCallback(() => {
onDeleteSubmission(item);
}, [onDeleteSubmission, item]);
const handleFocus = useCallback(() => {
onInteractionFocus(item.id);
}, [onInteractionFocus, item.id]);
const handleBlur = useCallback(() => {
onInteractionBlur(item.id);
}, [onInteractionBlur, item.id]);
const handleReverseNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
onNoteChange(`reverse-${item.id}`, e.target.value);
}, [onNoteChange, item.id]);
const handleReverseApprove = useCallback(() => {
onApprove(item, 'approved', notes[`reverse-${item.id}`]);
}, [onApprove, item, notes]);
const handleReverseReject = useCallback(() => {
onApprove(item, 'rejected', notes[`reverse-${item.id}`]);
}, [onApprove, item, notes]);
return (
<>
{/* Action buttons based on status */}
@@ -142,9 +195,9 @@ export const QueueItemActions = memo(({
id={`notes-${item.id}`}
placeholder="Add notes about your moderation decision..."
value={notes[item.id] || ''}
onChange={(e) => onNoteChange(item.id, e.target.value)}
onFocus={() => onInteractionFocus(item.id)}
onBlur={() => onInteractionBlur(item.id)}
onChange={handleNoteChange}
onFocus={handleFocus}
onBlur={handleBlur}
rows={isMobile ? 2 : 4}
className={!isMobile ? 'min-h-[120px]' : ''}
disabled={isLockedByOther || currentLockSubmissionId !== item.id}
@@ -157,7 +210,7 @@ export const QueueItemActions = memo(({
{item.type === 'content_submission' && (
<>
<Button
onClick={() => onOpenReviewManager(item.id)}
onClick={handleOpenReviewManager}
disabled={actionLoading === item.id || isLockedByOther || currentLockSubmissionId !== item.id}
variant="outline"
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
@@ -171,7 +224,7 @@ export const QueueItemActions = memo(({
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => onOpenItemEditor(item.id)}
onClick={handleOpenItemEditor}
disabled={actionLoading === item.id}
variant="ghost"
className={isMobile ? 'h-11' : ''}
@@ -190,7 +243,7 @@ export const QueueItemActions = memo(({
)}
<Button
onClick={() => onApprove(item, 'approved', notes[item.id])}
onClick={handleApprove}
disabled={actionLoading === item.id || isLockedByOther || currentLockSubmissionId !== item.id}
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
size={isMobile ? "default" : "default"}
@@ -200,7 +253,7 @@ export const QueueItemActions = memo(({
</Button>
<Button
variant="destructive"
onClick={() => onApprove(item, 'rejected', notes[item.id])}
onClick={handleReject}
disabled={actionLoading === item.id || isLockedByOther || currentLockSubmissionId !== item.id}
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
size={isMobile ? "default" : "default"}
@@ -224,7 +277,7 @@ export const QueueItemActions = memo(({
</div>
</div>
<Button
onClick={() => onResetToPending(item)}
onClick={handleResetToPending}
disabled={actionLoading === item.id}
variant="outline"
className="w-full"
@@ -247,7 +300,7 @@ export const QueueItemActions = memo(({
</div>
<div className="flex gap-2">
<Button
onClick={() => onOpenReviewManager(item.id)}
onClick={handleOpenReviewManager}
disabled={actionLoading === item.id}
variant="outline"
className="flex-1"
@@ -256,7 +309,7 @@ export const QueueItemActions = memo(({
Review Items
</Button>
<Button
onClick={() => onResetToPending(item)}
onClick={handleResetToPending}
disabled={actionLoading === item.id}
variant="outline"
className="flex-1"
@@ -265,7 +318,7 @@ export const QueueItemActions = memo(({
Reset All
</Button>
<Button
onClick={() => onRetryFailed(item)}
onClick={handleRetryFailed}
disabled={actionLoading === item.id}
className="flex-1 bg-yellow-600 hover:bg-yellow-700"
>
@@ -348,16 +401,16 @@ export const QueueItemActions = memo(({
<Textarea
placeholder="Add notes about reversing this decision..."
value={notes[`reverse-${item.id}`] || ''}
onChange={(e) => onNoteChange(`reverse-${item.id}`, e.target.value)}
onFocus={() => onInteractionFocus(item.id)}
onBlur={() => onInteractionBlur(item.id)}
onChange={handleReverseNoteChange}
onFocus={handleFocus}
onBlur={handleBlur}
rows={2}
/>
<div className={`flex gap-2 ${isMobile ? 'flex-col' : ''}`}>
{item.status === 'approved' && (
<Button
variant="destructive"
onClick={() => onApprove(item, 'rejected', notes[`reverse-${item.id}`])}
onClick={handleReverseReject}
disabled={actionLoading === item.id}
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
size={isMobile ? "default" : "default"}
@@ -368,7 +421,7 @@ export const QueueItemActions = memo(({
)}
{item.status === 'rejected' && (
<Button
onClick={() => onApprove(item, 'approved', notes[`reverse-${item.id}`])}
onClick={handleReverseApprove}
disabled={actionLoading === item.id}
className={`flex-1 ${isMobile ? 'h-11' : ''}`}
size={isMobile ? "default" : "default"}
@@ -387,7 +440,7 @@ export const QueueItemActions = memo(({
<div className="pt-2">
<Button
variant="destructive"
onClick={() => onDeleteSubmission(item)}
onClick={handleDeleteSubmission}
disabled={actionLoading === item.id}
className={`w-full ${isMobile ? 'h-11' : ''}`}
size={isMobile ? "default" : "default"}