Files
thrilltrack-explorer/src/components/moderation/ModerationQueue.tsx
2025-10-15 14:48:37 +00:00

276 lines
11 KiB
TypeScript

import { useState, useImperativeHandle, forwardRef, useMemo } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { TooltipProvider } from '@/components/ui/tooltip';
import { useToast } from '@/hooks/use-toast';
import { useUserRole } from '@/hooks/useUserRole';
import { useAuth } from '@/hooks/useAuth';
import { PhotoModal } from './PhotoModal';
import { SubmissionReviewManager } from './SubmissionReviewManager';
import { ItemEditDialog } from './ItemEditDialog';
import { useIsMobile } from '@/hooks/use-mobile';
import { useAdminSettings } from '@/hooks/useAdminSettings';
import { useModerationQueueManager } from '@/hooks/moderation';
import { QueueItem } from './QueueItem';
import { QueueSkeleton } from './QueueSkeleton';
import { LockStatusDisplay } from './LockStatusDisplay';
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 { QueuePagination } from './QueuePagination';
import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
import type { ModerationQueueRef } from '@/types/moderation';
import type { PhotoItem } from '@/types/photos';
export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const isMobile = useIsMobile();
const { user } = useAuth();
const { toast } = useToast();
const { isAdmin, isSuperuser } = useUserRole();
const adminSettings = useAdminSettings();
// Extract settings values to stable primitives for memoization
const refreshMode = adminSettings.getAdminPanelRefreshMode();
const pollInterval = adminSettings.getAdminPanelPollInterval();
const refreshStrategy = adminSettings.getAutoRefreshStrategy();
const preserveInteraction = adminSettings.getPreserveInteractionState();
const useRealtimeQueue = adminSettings.getUseRealtimeQueue();
// Memoize settings object using stable primitive dependencies
const settings = useMemo(() => ({
refreshMode,
pollInterval,
refreshStrategy,
preserveInteraction,
useRealtimeQueue,
}), [refreshMode, pollInterval, refreshStrategy, preserveInteraction, useRealtimeQueue]);
// Initialize queue manager (replaces all state management, fetchItems, effects)
const queueManager = useModerationQueueManager({
user,
isAdmin: isAdmin(),
isSuperuser: isSuperuser(),
toast,
settings,
});
// UI-only state
const [notes, setNotes] = useState<Record<string, string>>({});
const [photoModalOpen, setPhotoModalOpen] = useState(false);
const [selectedPhotos, setSelectedPhotos] = useState<PhotoItem[]>([]);
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(0);
const [reviewManagerOpen, setReviewManagerOpen] = useState(false);
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
const [showItemEditDialog, setShowItemEditDialog] = useState(false);
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
// UI-specific handlers
const handleNoteChange = (id: string, value: string) => {
setNotes(prev => ({ ...prev, [id]: value }));
};
const handleOpenPhotos = (photos: any[], index: number) => {
setSelectedPhotos(photos);
setSelectedPhotoIndex(index);
setPhotoModalOpen(true);
};
const handleOpenReviewManager = (submissionId: string) => {
setSelectedSubmissionId(submissionId);
setReviewManagerOpen(true);
};
const handleOpenItemEditor = async (submissionId: string) => {
try {
const items = await fetchSubmissionItems(submissionId);
// Find first pending item, fallback to first available
let itemToEdit = items.find(item => item.status === 'pending');
if (!itemToEdit && items.length > 0) {
itemToEdit = items[0];
}
if (itemToEdit) {
setEditingItem(itemToEdit);
setShowItemEditDialog(true);
} else {
toast({
title: 'No Items Found',
description: 'This submission has no items to edit',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error fetching items for edit:', error);
toast({
title: 'Error',
description: 'Failed to load submission items',
variant: 'destructive',
});
}
};
// Expose imperative API
useImperativeHandle(ref, () => ({
refresh: async () => {
await queueManager.refresh();
}
}));
return (
<div className="space-y-4">
{/* Queue Statistics & Lock Status */}
{queueManager.queue.queueStats && (
<Card className="bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">
<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
currentLock={queueManager.queue.currentLock}
queueStats={queueManager.queue.queueStats}
isLoading={queueManager.queue.isLoading}
onClaimNext={async () => { await queueManager.queue.claimNext(); }}
onExtendLock={queueManager.queue.extendLock}
onReleaseLock={queueManager.queue.releaseLock}
getTimeRemaining={queueManager.queue.getTimeRemaining}
getLockProgress={queueManager.queue.getLockProgress}
/>
</div>
</CardContent>
</Card>
)}
{/* Filter Bar */}
<QueueFilters
activeEntityFilter={queueManager.filters.entityFilter}
activeStatusFilter={queueManager.filters.statusFilter}
sortConfig={queueManager.filters.sortConfig}
isMobile={isMobile}
isLoading={queueManager.loadingState === 'loading'}
onEntityFilterChange={queueManager.filters.setEntityFilter}
onStatusFilterChange={queueManager.filters.setStatusFilter}
onSortChange={queueManager.filters.setSortConfig}
onClearFilters={queueManager.filters.clearFilters}
showClearButton={queueManager.filters.hasActiveFilters}
/>
{/* Active Filters Display */}
{queueManager.filters.hasActiveFilters && (
<ActiveFiltersDisplay
entityFilter={queueManager.filters.entityFilter}
statusFilter={queueManager.filters.statusFilter}
/>
)}
{/* Auto-refresh Indicator */}
{adminSettings.getAdminPanelRefreshMode() === 'auto' && (
<AutoRefreshIndicator
enabled={true}
intervalSeconds={Math.round(adminSettings.getAdminPanelPollInterval() / 1000)}
mode={adminSettings.getUseRealtimeQueue() ? 'realtime' : 'polling'}
/>
)}
{/* New Items Alert */}
{queueManager.newItemsCount > 0 && (
<NewItemsAlert
count={queueManager.newItemsCount}
onShowNewItems={queueManager.showNewItems}
/>
)}
{/* Queue Content */}
{queueManager.loadingState === 'loading' || queueManager.loadingState === 'initial' ? (
<QueueSkeleton count={queueManager.pagination.pageSize} />
) : queueManager.items.length === 0 ? (
<EmptyQueueState
entityFilter={queueManager.filters.entityFilter}
statusFilter={queueManager.filters.statusFilter}
/>
) : (
<TooltipProvider>
<div className="space-y-6">
{queueManager.items.map((item, index) => (
<QueueItem
key={item.id}
item={item}
isMobile={isMobile}
actionLoading={queueManager.actionLoading}
isLockedByMe={queueManager.queue.isLockedByMe(item.id, item.assigned_to, item.locked_until)}
isLockedByOther={queueManager.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={queueManager.queue.currentLock?.submissionId}
notes={notes}
isAdmin={isAdmin()}
isSuperuser={isSuperuser()}
queueIsLoading={queueManager.queue.isLoading}
onNoteChange={handleNoteChange}
onApprove={queueManager.performAction}
onResetToPending={queueManager.resetToPending}
onRetryFailed={queueManager.retryFailedItems}
onOpenPhotos={handleOpenPhotos}
onOpenReviewManager={handleOpenReviewManager}
onOpenItemEditor={handleOpenItemEditor}
onClaimSubmission={queueManager.queue.claimSubmission}
onDeleteSubmission={queueManager.deleteSubmission}
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
/>
))}
</div>
</TooltipProvider>
)}
{/* Pagination */}
{queueManager.loadingState === 'ready' && queueManager.pagination.totalPages > 1 && (
<QueuePagination
currentPage={queueManager.pagination.currentPage}
totalPages={queueManager.pagination.totalPages}
pageSize={queueManager.pagination.pageSize}
totalCount={queueManager.pagination.totalCount}
isMobile={isMobile}
onPageChange={queueManager.pagination.setCurrentPage}
onPageSizeChange={queueManager.pagination.setPageSize}
/>
)}
{/* Modals */}
<PhotoModal
photos={selectedPhotos}
initialIndex={selectedPhotoIndex}
isOpen={photoModalOpen}
onClose={() => setPhotoModalOpen(false)}
/>
{selectedSubmissionId && (
<SubmissionReviewManager
submissionId={selectedSubmissionId}
open={reviewManagerOpen}
onOpenChange={setReviewManagerOpen}
onComplete={() => setReviewManagerOpen(false)}
/>
)}
{editingItem && (
<ItemEditDialog
item={editingItem}
open={showItemEditDialog}
onOpenChange={setShowItemEditDialog}
onComplete={async () => {
setShowItemEditDialog(false);
setEditingItem(null);
await queueManager.refresh();
}}
/>
)}
</div>
);
});
ModerationQueue.displayName = 'ModerationQueue';
export type { ModerationQueueRef } from '@/types/moderation';