From 0d0e352a1e59f578a62f8291630e2b62910d0c59 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:57:37 +0000 Subject: [PATCH] Implement Phase 6: Lock Management --- .../moderation/LockStatusDisplay.tsx | 115 ++++++++++++++++++ src/components/moderation/ModerationQueue.tsx | 74 +++-------- src/components/moderation/QueueItem.tsx | 27 ++-- src/hooks/useModerationQueue.ts | 33 +++++ src/lib/moderation/index.ts | 11 ++ src/lib/moderation/lockHelpers.ts | 91 ++++++++++++++ 6 files changed, 281 insertions(+), 70 deletions(-) create mode 100644 src/components/moderation/LockStatusDisplay.tsx create mode 100644 src/lib/moderation/lockHelpers.ts diff --git a/src/components/moderation/LockStatusDisplay.tsx b/src/components/moderation/LockStatusDisplay.tsx new file mode 100644 index 00000000..58ea1791 --- /dev/null +++ b/src/components/moderation/LockStatusDisplay.tsx @@ -0,0 +1,115 @@ +import { Lock, Clock, Unlock } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; + +interface LockState { + submissionId: string; + expiresAt: Date; +} + +interface QueueStats { + pendingCount: number; + assignedToMe: number; + avgWaitHours: number; +} + +interface LockStatusDisplayProps { + currentLock: LockState | null; + queueStats: QueueStats | null; + isLoading: boolean; + onClaimNext: () => Promise; + onExtendLock: (submissionId: string) => Promise; + onReleaseLock: (submissionId: string) => Promise; + getTimeRemaining: () => number | null; + getLockProgress: () => number; +} + +/** + * LockStatusDisplay Component + * + * Displays lock timer, progress bar, and lock management controls. + * Shows "Claim Next" button when no lock is active, or lock controls when locked. + */ +export const LockStatusDisplay = ({ + currentLock, + queueStats, + isLoading, + onClaimNext, + onExtendLock, + onReleaseLock, + getTimeRemaining, + getLockProgress, +}: LockStatusDisplayProps) => { + // Format milliseconds as MM:SS + const formatLockTimer = (ms: number): string => { + const minutes = Math.floor(ms / 60000); + const seconds = Math.floor((ms % 60000) / 1000); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + const timeRemaining = getTimeRemaining(); + const showExtendButton = timeRemaining !== null && timeRemaining < 5 * 60 * 1000; + + // No active lock - show claim button + if (!currentLock) { + return ( +
+ +
+ ); + } + + // Active lock - show timer and controls + return ( +
+ {/* Lock Timer */} +
+ + + Lock: {formatLockTimer(timeRemaining || 0)} + +
+ + {/* Progress Bar */} + + + {/* Extend Lock Button (show when < 5 min left) */} + {showExtendButton && ( + + )} + + {/* Release Lock Button */} + +
+ ); +}; diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 7cf977f8..570832ee 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -33,6 +33,8 @@ import { smartMergeArray } from '@/lib/smartStateUpdate'; import { useDebounce } from '@/hooks/useDebounce'; import { QueueItem } from './QueueItem'; import { QueueSkeleton } from './QueueSkeleton'; +import { LockStatusDisplay } from './LockStatusDisplay'; +import { getLockStatus } from '@/lib/moderation/lockHelpers'; import type { ModerationItem, EntityFilter, @@ -60,7 +62,6 @@ export const ModerationQueue = forwardRef((props, ref) => { const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(0); const [reviewManagerOpen, setReviewManagerOpen] = useState(false); const [selectedSubmissionId, setSelectedSubmissionId] = useState(null); - const [lockedSubmissions, setLockedSubmissions] = useState>(new Set()); const [escalationDialogOpen, setEscalationDialogOpen] = useState(false); const [reassignDialogOpen, setReassignDialogOpen] = useState(false); const [selectedItemForAction, setSelectedItemForAction] = useState(null); @@ -1969,7 +1970,9 @@ export const ModerationQueue = forwardRef((props, ref) => { item={item} isMobile={isMobile} actionLoading={actionLoading} - lockedSubmissions={lockedSubmissions} + isLockedByMe={queue.isLockedByMe(item.id)} + isLockedByOther={queue.isLockedByOther(item.id, item.assigned_to, item.locked_until)} + lockStatus={getLockStatus({ assigned_to: item.assigned_to, locked_until: item.locked_until }, user?.id || '')} currentLockSubmissionId={queue.currentLock?.submissionId} notes={notes} isAdmin={isAdmin()} @@ -2008,13 +2011,6 @@ export const ModerationQueue = forwardRef((props, ref) => { } }; - // Helper to format lock timer - const formatLockTimer = (ms: number): string => { - const minutes = Math.floor(ms / 60000); - const seconds = Math.floor((ms % 60000) / 1000); - return `${minutes}:${seconds.toString().padStart(2, '0')}`; - }; - // Handle claim next action const handleClaimNext = async () => { await queue.claimNext(); @@ -2047,56 +2043,16 @@ export const ModerationQueue = forwardRef((props, ref) => { {/* Claim/Lock Status */} -
- {queue.currentLock ? ( - <> - {/* Lock Timer */} -
- - - Lock: {formatLockTimer(queue.getTimeRemaining() || 0)} - -
- - {/* Extend Lock Button (show when < 5 min left) */} - {(queue.getTimeRemaining() || 0) < 5 * 60 * 1000 && ( - - )} - - - ) : ( - - )} -
+ diff --git a/src/components/moderation/QueueItem.tsx b/src/components/moderation/QueueItem.tsx index 07095b7b..1a95d038 100644 --- a/src/components/moderation/QueueItem.tsx +++ b/src/components/moderation/QueueItem.tsx @@ -50,11 +50,15 @@ interface ModerationItem { import { ValidationSummary } from './ValidationSummary'; import type { ValidationResult } from '@/lib/entityValidationSchemas'; +import type { LockStatus } from '@/lib/moderation/lockHelpers'; + interface QueueItemProps { item: ModerationItem; isMobile: boolean; actionLoading: string | null; - lockedSubmissions: Set; + isLockedByMe: boolean; + isLockedByOther: boolean; + lockStatus: LockStatus; currentLockSubmissionId?: string; notes: Record; isAdmin: boolean; @@ -88,7 +92,9 @@ export const QueueItem = memo(({ item, isMobile, actionLoading, - lockedSubmissions, + isLockedByMe, + isLockedByOther, + lockStatus, currentLockSubmissionId, notes, isAdmin, @@ -162,7 +168,7 @@ export const QueueItem = memo(({ Needs Retry )} - {lockedSubmissions.has(item.id) && item.type === 'content_submission' && ( + {isLockedByOther && item.type === 'content_submission' && ( Locked by Another Moderator @@ -427,7 +433,7 @@ export const QueueItem = memo(({ {(item.status === 'pending' || item.status === 'flagged') && ( <> {/* Claim button for unclaimed submissions */} - {!lockedSubmissions.has(item.id) && currentLockSubmissionId !== item.id && ( + {!isLockedByOther && currentLockSubmissionId !== item.id && (
@@ -460,7 +466,7 @@ export const QueueItem = memo(({ onFocus={() => onInteractionFocus(item.id)} onBlur={() => onInteractionBlur(item.id)} rows={2} - disabled={lockedSubmissions.has(item.id) || currentLockSubmissionId !== item.id} + disabled={isLockedByOther || currentLockSubmissionId !== item.id} />
@@ -469,7 +475,7 @@ export const QueueItem = memo(({ {item.type === 'content_submission' && (