diff --git a/package-lock.json b/package-lock.json
index b367cee5..f0e1af49 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -49,6 +49,7 @@
"@supabase/supabase-js": "^2.57.4",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.90.2",
+ "@tanstack/react-virtual": "^3.13.12",
"@uppy/core": "^5.0.2",
"@uppy/dashboard": "^5.0.2",
"@uppy/image-editor": "^4.0.1",
@@ -4760,6 +4761,33 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
+ "node_modules/@tanstack/react-virtual": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
+ "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.13.12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
+ "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@transloadit/prettier-bytes": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz",
diff --git a/package.json b/package.json
index 59f924e8..eaa1c941 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"@supabase/supabase-js": "^2.57.4",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.90.2",
+ "@tanstack/react-virtual": "^3.13.12",
"@uppy/core": "^5.0.2",
"@uppy/dashboard": "^5.0.2",
"@uppy/image-editor": "^4.0.1",
diff --git a/src/components/moderation/ConfirmationDialog.tsx b/src/components/moderation/ConfirmationDialog.tsx
new file mode 100644
index 00000000..779c0c1e
--- /dev/null
+++ b/src/components/moderation/ConfirmationDialog.tsx
@@ -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 (
+
+
+
+ {title}
+ {description}
+
+
+ {cancelLabel}
+
+ {confirmLabel}
+
+
+
+
+ );
+};
+
+ConfirmationDialog.displayName = 'ConfirmationDialog';
diff --git a/src/components/moderation/EmptyQueueState.tsx b/src/components/moderation/EmptyQueueState.tsx
index efbca20f..4acda1e6 100644
--- a/src/components/moderation/EmptyQueueState.tsx
+++ b/src/components/moderation/EmptyQueueState.tsx
@@ -49,3 +49,5 @@ export const EmptyQueueState = ({
);
};
+
+EmptyQueueState.displayName = 'EmptyQueueState';
diff --git a/src/components/moderation/EnhancedEmptyState.tsx b/src/components/moderation/EnhancedEmptyState.tsx
new file mode 100644
index 00000000..20fc5ac9
--- /dev/null
+++ b/src/components/moderation/EnhancedEmptyState.tsx
@@ -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 (
+
+
+
+
+
{variant.title}
+
+ {variant.description}
+
+ {variant.action && (
+
+ {variant.action.label}
+
+ )}
+
+ );
+};
+
+EnhancedEmptyState.displayName = 'EnhancedEmptyState';
diff --git a/src/components/moderation/EnhancedLockStatusDisplay.tsx b/src/components/moderation/EnhancedLockStatusDisplay.tsx
new file mode 100644
index 00000000..680a76e1
--- /dev/null
+++ b/src/components/moderation/EnhancedLockStatusDisplay.tsx
@@ -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(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 (
+
+
+
+ No submission claimed
+
+ {queueStats && (
+
+ {queueStats.pendingCount} pending
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+
+ Lock expires in
+
+
+ {formatTime(timeLeft)}
+
+
+
+
div]:bg-destructive',
+ urgency === 'warning' && '[&>div]:bg-yellow-500',
+ urgency === 'safe' && '[&>div]:bg-primary'
+ )}
+ />
+
+ {urgency === 'critical' && (
+
+
+
Lock expiring soon! Extend or release.
+
+ )}
+
+
+ {showExtendButton && (
+
+
+ Extend Lock (+15min)
+
+ )}
+
+ Release Lock
+
+
+
+ );
+};
+
+EnhancedLockStatusDisplay.displayName = 'EnhancedLockStatusDisplay';
diff --git a/src/components/moderation/KeyboardShortcutsHelp.tsx b/src/components/moderation/KeyboardShortcutsHelp.tsx
new file mode 100644
index 00000000..f04b15bc
--- /dev/null
+++ b/src/components/moderation/KeyboardShortcutsHelp.tsx
@@ -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 (
+
+
+
+
+
+ Keyboard Shortcuts
+
+
+ Speed up your moderation workflow with these keyboard shortcuts
+
+
+
+ {shortcuts.map((shortcut, index) => (
+
+
+ {shortcut.description}
+
+
+ {shortcut.key}
+
+
+ ))}
+
+
+
+ );
+};
+
+KeyboardShortcutsHelp.displayName = 'KeyboardShortcutsHelp';
diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx
index 9116f927..8686a35f 100644
--- a/src/components/moderation/ModerationQueue.tsx
+++ b/src/components/moderation/ModerationQueue.tsx
@@ -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(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('[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
- queueManager.queue.extendLock(queueManager.queue.currentLock?.submissionId || '')}
+ onReleaseLock={() => queueManager.queue.releaseLock(queueManager.queue.currentLock?.submissionId || '', false)}
+ getCurrentTime={() => new Date()}
/>
@@ -192,9 +251,10 @@ export const ModerationQueue = forwardRef
) : queueManager.items.length === 0 ? (
-
) : (
@@ -222,7 +282,7 @@ export const ModerationQueue = forwardRef queueManager.markInteracting(id, true)}
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
/>
@@ -274,6 +334,24 @@ export const ModerationQueue = forwardRef
)}
+
+ {/* Confirmation Dialog */}
+ setConfirmDialog(prev => ({ ...prev, open }))}
+ title={confirmDialog.title}
+ description={confirmDialog.description}
+ onConfirm={confirmDialog.onConfirm}
+ variant="destructive"
+ confirmLabel="Delete"
+ />
+
+ {/* Keyboard Shortcuts Help */}
+
);
});
diff --git a/src/components/moderation/QueueItem.tsx b/src/components/moderation/QueueItem.tsx
index abc2a066..0bc73b07 100644
--- a/src/components/moderation/QueueItem.tsx
+++ b/src/components/moderation/QueueItem.tsx
@@ -201,68 +201,12 @@ export const QueueItem = memo(({
})()}
) : item.submission_type === 'photo' ? (
-
-
- Photo Submission
-
-
- {/* Submission Title */}
- {item.content.title && (
-
-
Title:
-
{item.content.title}
-
- )}
-
- {/* Photos from relational table */}
- {photosLoading ? (
-
Loading photos...
- ) : photoItems.length > 0 ? (
-
-
- Photos ({photoItems.length}):
- {import.meta.env.DEV && photoItems[0] && (
-
- URL: {photoItems[0].cloudflare_image_url?.slice(0, 30)}...
-
- )}
-
-
({
- 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}
- />
-
- ) : (
-
-
- No Photos Found
-
- This photo submission has no photos attached. This may be a data integrity issue.
-
-
- )}
-
- {/* Context Information */}
- {item.entity_name && (
-
- For:
- {item.entity_name}
- {item.park_name && (
- <>
- at
- {item.park_name}
- >
- )}
-
- )}
-
+
) : (
<>
{/* Main content area - spans 1st column on all layouts */}
diff --git a/src/components/moderation/QueuePagination.tsx b/src/components/moderation/QueuePagination.tsx
index f1950808..242092ad 100644
--- a/src/components/moderation/QueuePagination.tsx
+++ b/src/components/moderation/QueuePagination.tsx
@@ -159,3 +159,5 @@ export const QueuePagination = ({
);
};
+
+QueuePagination.displayName = 'QueuePagination';
diff --git a/src/components/moderation/QueueStats.tsx b/src/components/moderation/QueueStats.tsx
index ad756dce..58485475 100644
--- a/src/components/moderation/QueueStats.tsx
+++ b/src/components/moderation/QueueStats.tsx
@@ -27,3 +27,5 @@ export const QueueStats = ({ stats, isMobile }: QueueStatsProps) => {
);
};
+
+QueueStats.displayName = 'QueueStats';
diff --git a/src/components/moderation/renderers/QueueItemActions.tsx b/src/components/moderation/renderers/QueueItemActions.tsx
index 1500fa65..782aec8b 100644
--- a/src/components/moderation/renderers/QueueItemActions.tsx
+++ b/src/components/moderation/renderers/QueueItemActions.tsx
@@ -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) => {
+ 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) => {
+ 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' && (
<>
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(({
onOpenItemEditor(item.id)}
+ onClick={handleOpenItemEditor}
disabled={actionLoading === item.id}
variant="ghost"
className={isMobile ? 'h-11' : ''}
@@ -190,7 +243,7 @@ export const QueueItemActions = memo(({
)}
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(({
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(({
onResetToPending(item)}
+ onClick={handleResetToPending}
disabled={actionLoading === item.id}
variant="outline"
className="w-full"
@@ -247,7 +300,7 @@ export const QueueItemActions = memo(({
onOpenReviewManager(item.id)}
+ onClick={handleOpenReviewManager}
disabled={actionLoading === item.id}
variant="outline"
className="flex-1"
@@ -256,7 +309,7 @@ export const QueueItemActions = memo(({
Review Items
onResetToPending(item)}
+ onClick={handleResetToPending}
disabled={actionLoading === item.id}
variant="outline"
className="flex-1"
@@ -265,7 +318,7 @@ export const QueueItemActions = memo(({
Reset All
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(({