import { useState, useMemo, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Clock, Trash2, Play, X, CheckCircle, XCircle, AlertCircle, Calendar, Printer, GripVertical, SkipForward, ExternalLink, Power, StopCircle, Pencil, RefreshCw, Timer, ListOrdered, Layers, ArrowUp, ArrowDown, Hand, Check, CheckSquare, Square, User, Pause, Weight, } from 'lucide-react'; import { api } from '../api/client'; import { type TimeFormat, formatETA, formatDuration, formatRelativeTime } from '../utils/date'; import type { PrintQueueItem, PrintQueueBulkUpdate, Permission } from '../api/client'; import { Card, CardContent } from '../components/Card'; import { Button } from '../components/Button'; import { ConfirmModal } from '../components/ConfirmModal'; import { PrintModal } from '../components/PrintModal'; import { useToast } from '../contexts/ToastContext'; import { useAuth } from '../contexts/AuthContext'; function formatWeight(g: number, useKg = false): string { if (useKg && g >= 1000) return `${(g / 1000).toFixed(1)}kg`; return `${Math.round(g)}g`; } function StatusBadge({ status, waitingReason, printerState, t }: { status: PrintQueueItem['status']; waitingReason?: string | null; printerState?: string | null; t: (key: string) => string }) { // Special case: pending with waiting_reason shows as "Waiting" if (status === 'pending' && waitingReason) { return ( {t('queue.status.waiting')} ); } // Special case: printing but printer is paused if (status === 'printing' && (printerState === 'PAUSE' || printerState === 'PAUSED')) { return ( {t('queue.status.paused')} ); } const config = { pending: { icon: Clock, color: 'text-status-warning bg-status-warning/10 border-status-warning/20', label: t('queue.status.pending') }, printing: { icon: Play, color: 'text-blue-400 bg-blue-400/10 border-blue-400/20', label: t('queue.status.printing') }, completed: { icon: CheckCircle, color: 'text-status-ok bg-status-ok/10 border-status-ok/20', label: t('queue.status.completed') }, failed: { icon: XCircle, color: 'text-status-error bg-status-error/10 border-status-error/20', label: t('queue.status.failed') }, skipped: { icon: SkipForward, color: 'text-orange-400 bg-orange-400/10 border-orange-400/20', label: t('queue.status.skipped') }, cancelled: { icon: X, color: 'text-gray-400 bg-gray-400/10 border-gray-400/20', label: t('queue.status.cancelled') }, }; const { icon: Icon, color, label } = config[status]; return ( {label} ); } // Bulk edit modal for multiple queue items function BulkEditModal({ selectedCount, printers, onSave, onClose, isSaving, t, }: { selectedCount: number; printers: { id: number; name: string }[]; onSave: (data: Partial) => void; onClose: () => void; isSaving: boolean; t: (key: string, options?: Record) => string; }) { const [printerId, setPrinterId] = useState('unchanged'); const [manualStart, setManualStart] = useState('unchanged'); const [autoOffAfter, setAutoOffAfter] = useState('unchanged'); const [requirePreviousSuccess, setRequirePreviousSuccess] = useState('unchanged'); const [bedLevelling, setBedLevelling] = useState('unchanged'); const [flowCali, setFlowCali] = useState('unchanged'); const [vibrationCali, setVibrationCali] = useState('unchanged'); const [layerInspect, setLayerInspect] = useState('unchanged'); const [timelapse, setTimelapse] = useState('unchanged'); const [useAms, setUseAms] = useState('unchanged'); const handleSave = () => { const data: Partial = {}; if (printerId !== 'unchanged') data.printer_id = printerId; if (manualStart !== 'unchanged') data.manual_start = manualStart; if (autoOffAfter !== 'unchanged') data.auto_off_after = autoOffAfter; if (requirePreviousSuccess !== 'unchanged') data.require_previous_success = requirePreviousSuccess; if (bedLevelling !== 'unchanged') data.bed_levelling = bedLevelling; if (flowCali !== 'unchanged') data.flow_cali = flowCali; if (vibrationCali !== 'unchanged') data.vibration_cali = vibrationCali; if (layerInspect !== 'unchanged') data.layer_inspect = layerInspect; if (timelapse !== 'unchanged') data.timelapse = timelapse; if (useAms !== 'unchanged') data.use_ams = useAms; onSave(data); }; const hasChanges = printerId !== 'unchanged' || manualStart !== 'unchanged' || autoOffAfter !== 'unchanged' || requirePreviousSuccess !== 'unchanged' || bedLevelling !== 'unchanged' || flowCali !== 'unchanged' || vibrationCali !== 'unchanged' || layerInspect !== 'unchanged' || timelapse !== 'unchanged' || useAms !== 'unchanged'; return (

{t('queue.bulkEdit.title', { count: selectedCount })}

{t('queue.bulkEdit.description')}

{/* Printer Assignment */}
{/* Queue Options */}
{/* Print Options */}
); } // Tri-state toggle for bulk edit (unchanged / on / off) function TriStateToggle({ label, value, onChange, t, }: { label: string; value: boolean | 'unchanged'; onChange: (val: boolean | 'unchanged') => void; t: (key: string) => string; }) { return (
{label}
); } // Sortable queue item for drag and drop function SortableQueueItem({ item, position, onEdit, onCancel, onRemove, onStop, onRequeue, onStart, timeFormat = 'system', isSelected = false, onToggleSelect, hasPermission, canModify, printerState, t, }: { item: PrintQueueItem; position?: number; onEdit: () => void; onCancel: () => void; onRemove: () => void; onStop: () => void; onRequeue: () => void; onStart: () => void; timeFormat?: TimeFormat; isSelected?: boolean; onToggleSelect?: () => void; hasPermission: (permission: Permission) => boolean; canModify: (resource: 'queue' | 'archives' | 'library', action: 'update' | 'delete' | 'reprint', createdById: number | null | undefined) => boolean; printerState?: string | null; t: (key: string, options?: Record) => string; }) { // Fetch printer status every 30 seconds while printing to monitor progress const { data: status } = useQuery({ queryKey: ['printerStatus', item.printer_id], queryFn: () => api.getPrinterStatus(item.printer_id!), refetchInterval: 30000, enabled: item.printer_id != null && printerState === 'printing', }); // Determine if we're printing a library file const isLibraryFile = !!item.library_file_id && !item.archive_id; // Fetch archive plate details const { data: archivePlatesData } = useQuery({ queryKey: ['archive-plates', item.archive_id], queryFn: () => api.getArchivePlates(item.archive_id!), enabled: !!item.archive_id && !isLibraryFile, }); // Fetch library file plate details const { data: libraryPlatesData } = useQuery({ queryKey: ['library-file-plates', item.library_file_id], queryFn: () => api.getLibraryFilePlates(item.library_file_id!), enabled: isLibraryFile && !!item.library_file_id, }); // Combine plates data from either source const platesData = isLibraryFile ? libraryPlatesData : archivePlatesData; const plates = platesData?.plates ?? []; const canReorder = hasPermission('queue:reorder'); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id, disabled: item.status !== 'pending' || !canReorder }); const style = { transform: CSS.Transform.toString(transform), transition, }; const isPrinting = item.status === 'printing'; const isPending = item.status === 'pending'; const isHistory = ['completed', 'failed', 'skipped', 'cancelled'].includes(item.status); const isMobileSelectable = isPending && onToggleSelect; return (
{ if (window.innerWidth < 640) onToggleSelect(); } : undefined} > {/* Mobile selected left accent bar */} {isMobileSelectable && isSelected && (
)}
{/* Mobile selection indicator — left accent bar only, no tick */} {/* Selection checkbox for pending items - hidden on mobile, tap card instead */} {isPending && onToggleSelect && ( )} {/* Drag handle or position number - hidden on mobile */} {isPending ? (
) : position !== undefined ? (
#{position}
) : (
)} {/* Thumbnail - use plate-specific thumbnail if plate_id is set */}
{item.archive_thumbnail ? ( ) : item.library_file_thumbnail ? ( ) : (
)}
{/* Info */}

{item.archive_name || item.library_file_name || `File #${item.archive_id || item.library_file_id}`} {(platesData?.is_multi_plate ?? false) && item.plate_id !== undefined && item.plate_id !== null && ` • ${plates.find(plate => plate.index === item.plate_id)?.name || t('queue.plateNumber', { index: item.plate_id })}`}

{item.archive_id ? ( ) : item.library_file_id ? ( ) : null}
{item.target_model ? `${t('queue.filter.any')} ${item.target_model}${item.target_location ? ` @ ${item.target_location}` : ''}${item.required_filament_types?.length ? ` (${item.required_filament_types.join(', ')})` : ''}` : item.printer_id === null ? t('queue.filter.unassigned') : (item.printer_name || `${t('common.printer')} #${item.printer_id}`)} {item.print_time_seconds && ( {formatDuration(item.print_time_seconds)} )} {item.filament_used_grams && ( {formatWeight(item.filament_used_grams)} )} {item.created_by_username && ( {item.created_by_username} )} {isPending && !item.manual_start && ( {item.scheduled_time ? (new Date(item.scheduled_time).getTime() - Date.now() < -60000 ? t?.('queue.time.overdue') ?? 'Overdue' : formatRelativeTime(item.scheduled_time, timeFormat, t)) : t?.('queue.time.asap') ?? 'ASAP'} )}
{/* Options badges */}
{item.manual_start && ( {t('queue.badges.staged')} )} {item.require_previous_success && ( {t('queue.badges.requiresPrevious')} )} {item.auto_off_after && ( {t('queue.badges.autoPowerOff')} )}
{/* Progress bar for printing items - TODO: integrate with WebSocket */} {isPrinting && status && (
{Math.round(status.progress || 0)}%
{status.remaining_time != null && status.remaining_time > 0 && ( <> {formatDuration(status.remaining_time * 60)} ETA {formatETA(status.remaining_time, timeFormat, t)} )} {status.layer_num != null && status.total_layers != null && status.total_layers > 0 && ( {status.layer_num}/{status.total_layers} )}
)} {/* Waiting reason for model-based assignments */} {item.waiting_reason && item.status === 'pending' && (

{item.waiting_reason}

)} {/* Error message */} {item.error_message && (

{item.error_message}

)}
{/* Status badge + Actions */}
e.stopPropagation()}>
{isPrinting && ( )} {isPending && ( <> {item.manual_start && ( )} )} {isHistory && ( <> )}
); } export function QueuePage() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const { hasPermission, hasAnyPermission, canModify } = useAuth(); const [filterPrinter, setFilterPrinter] = useState(null); const [filterStatus, setFilterStatus] = useState(''); const [filterLocation, setFilterLocation] = useState(''); const [showClearHistoryConfirm, setShowClearHistoryConfirm] = useState(false); const [editItem, setEditItem] = useState(null); const [requeueItem, setRequeueItem] = useState(null); const [confirmAction, setConfirmAction] = useState<{ type: 'cancel' | 'remove' | 'stop'; item: PrintQueueItem; } | null>(null); const [selectedItems, setSelectedItems] = useState([]); const [showBulkEditModal, setShowBulkEditModal] = useState(false); const [historySortBy, setHistorySortBy] = useState<'date' | 'name' | 'printer'>(() => { const saved = localStorage.getItem('queue.historySortBy'); return (saved as 'date' | 'name' | 'printer') || 'date'; }); const [historySortAsc, setHistorySortAsc] = useState(() => { const saved = localStorage.getItem('queue.historySortAsc'); return saved !== null ? saved === 'true' : false; }); const [pendingSortBy, setPendingSortBy] = useState<'position' | 'name' | 'printer' | 'time'>(() => { const saved = localStorage.getItem('queue.pendingSortBy'); return (saved as 'position' | 'name' | 'printer' | 'time') || 'position'; }); const [pendingSortAsc, setPendingSortAsc] = useState(() => { const saved = localStorage.getItem('queue.pendingSortAsc'); return saved !== null ? saved === 'true' : true; }); // Persist sort settings to localStorage useEffect(() => { localStorage.setItem('queue.historySortBy', historySortBy); }, [historySortBy]); useEffect(() => { localStorage.setItem('queue.historySortAsc', String(historySortAsc)); }, [historySortAsc]); useEffect(() => { localStorage.setItem('queue.pendingSortBy', pendingSortBy); }, [pendingSortBy]); useEffect(() => { localStorage.setItem('queue.pendingSortAsc', String(pendingSortAsc)); }, [pendingSortAsc]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings, }); const timeFormat: TimeFormat = settings?.time_format || 'system'; const { data: queue, isLoading } = useQuery({ queryKey: ['queue', filterPrinter, filterStatus], queryFn: () => api.getQueue(filterPrinter || undefined, filterStatus || undefined), refetchInterval: 5000, }); const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: () => api.getPrinters(), }); const cancelMutation = useMutation({ mutationFn: (id: number) => api.cancelQueueItem(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['queue'] }); showToast(t('queue.toast.cancelled')); }, onError: () => showToast(t('queue.toast.cancelFailed'), 'error'), }); const removeMutation = useMutation({ mutationFn: (id: number) => api.removeFromQueue(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['queue'] }); showToast(t('queue.toast.removed')); }, onError: () => showToast(t('queue.toast.removeFailed'), 'error'), }); const stopMutation = useMutation({ mutationFn: (id: number) => api.stopQueueItem(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['queue'] }); showToast(t('queue.toast.stopped')); }, onError: () => showToast(t('queue.toast.stopFailed'), 'error'), }); const startMutation = useMutation({ mutationFn: (id: number) => api.startQueueItem(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['queue'] }); showToast(t('queue.toast.released')); }, onError: () => showToast(t('queue.toast.startFailed'), 'error'), }); const reorderMutation = useMutation({ mutationFn: (items: { id: number; position: number }[]) => api.reorderQueue(items), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['queue'] }); }, onError: () => showToast(t('queue.toast.reorderFailed'), 'error'), }); const clearHistoryMutation = useMutation({ mutationFn: async () => { const historyItems = queue?.filter(i => ['completed', 'failed', 'skipped', 'cancelled'].includes(i.status) ) || []; for (const item of historyItems) { await api.removeFromQueue(item.id); } return historyItems.length; }, onSuccess: (count) => { queryClient.invalidateQueries({ queryKey: ['queue'] }); showToast(t('queue.toast.historyCleared', { count })); }, onError: () => showToast(t('queue.toast.clearHistoryFailed'), 'error'), }); const bulkUpdateMutation = useMutation({ mutationFn: (data: PrintQueueBulkUpdate) => api.bulkUpdateQueue(data), onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ['queue'] }); setSelectedItems([]); setShowBulkEditModal(false); showToast(result.message); }, onError: () => showToast(t('queue.toast.updateFailed'), 'error'), }); const bulkCancelMutation = useMutation({ mutationFn: async (ids: number[]) => { for (const id of ids) { await api.cancelQueueItem(id); } return ids.length; }, onSuccess: (count) => { queryClient.invalidateQueries({ queryKey: ['queue'] }); setSelectedItems([]); showToast(t('queue.toast.bulkCancelled', { count })); }, onError: () => showToast(t('queue.toast.bulkCancelFailed'), 'error'), }); const handleToggleSelect = (id: number) => { setSelectedItems(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id] ); }; // Get unique locations from printers for the filter dropdown const uniqueLocations = useMemo(() => { const locations = new Set(); printers?.forEach(p => { if (p.location) locations.add(p.location); }); // Also include locations from queue items (for model-based assignments) queue?.forEach(item => { if (item.target_location) locations.add(item.target_location); }); return Array.from(locations).sort(); }, [printers, queue]); // Helper to check if a queue item matches the location filter const matchesLocationFilter = useCallback((item: PrintQueueItem): boolean => { if (!filterLocation) return true; // For model-based assignments, check target_location if (item.target_location) return item.target_location === filterLocation; // For printer-based assignments, check the printer's location if (item.printer_id) { const printer = printers?.find(p => p.id === item.printer_id); return printer?.location === filterLocation; } return false; }, [filterLocation, printers]); const pendingItems = useMemo(() => { let items = queue?.filter(i => i.status === 'pending') || []; // Apply location filter if (filterLocation) { items = items.filter(matchesLocationFilter); } // Helper to get scheduled time as timestamp (ASAP/placeholder = 0 for earliest) const getScheduledTime = (item: PrintQueueItem): number => { if (!item.scheduled_time) return 0; const time = new Date(item.scheduled_time).getTime(); // Placeholder dates (> 6 months out) are treated as ASAP const sixMonthsFromNow = Date.now() + (180 * 24 * 60 * 60 * 1000); return time > sixMonthsFromNow ? 0 : time; }; return [...items].sort((a, b) => { let cmp: number; if (pendingSortBy === 'name') { const aName = a.archive_name || a.library_file_name || ''; const bName = b.archive_name || b.library_file_name || ''; cmp = aName.localeCompare(bName); } else if (pendingSortBy === 'printer') { cmp = (a.printer_name || '').localeCompare(b.printer_name || ''); } else if (pendingSortBy === 'time') { // Sort by scheduled start time (when print will begin) cmp = getScheduledTime(a) - getScheduledTime(b); } else { cmp = a.position - b.position; } return pendingSortAsc ? cmp : -cmp; }); }, [queue, pendingSortBy, pendingSortAsc, matchesLocationFilter, filterLocation]); const handleSelectAll = () => { const allPendingIds = pendingItems.map(i => i.id); if (selectedItems.length === allPendingIds.length) { setSelectedItems([]); } else { setSelectedItems(allPendingIds); } }; const activeItems = useMemo(() => { let items = queue?.filter(i => i.status === 'printing') || []; if (filterLocation) { items = items.filter(matchesLocationFilter); } return items; }, [queue, filterLocation, matchesLocationFilter]); // Get unique printer IDs from active items to fetch their statuses const activePrinterIds = useMemo(() => { const ids = new Set(); activeItems.forEach(item => { if (item.printer_id) ids.add(item.printer_id); }); return Array.from(ids); }, [activeItems]); // Fetch printer statuses for printers with active jobs const printerStatusQueries = useQueries({ queries: activePrinterIds.map(printerId => ({ queryKey: ['printerStatus', printerId], queryFn: () => api.getPrinterStatus(printerId), refetchInterval: 5000, })), }); // Build a map of printer_id -> state for quick lookup const printerStateMap = useMemo(() => { const map: Record = {}; activePrinterIds.forEach((printerId, index) => { const result = printerStatusQueries[index]; if (result?.data?.state) { map[printerId] = result.data.state; } }); return map; }, [activePrinterIds, printerStatusQueries]); const historyItems = useMemo(() => { let items = queue?.filter(i => ['completed', 'failed', 'skipped', 'cancelled'].includes(i.status)) || []; if (filterLocation) { items = items.filter(matchesLocationFilter); } return [...items].sort((a, b) => { let cmp: number; if (historySortBy === 'name') { const aName = a.archive_name || a.library_file_name || ''; const bName = b.archive_name || b.library_file_name || ''; cmp = aName.localeCompare(bName); } else if (historySortBy === 'printer') { cmp = (a.printer_name || '').localeCompare(b.printer_name || ''); } else { // Default: by date - most recent first (desc) is the natural order cmp = new Date(b.completed_at || b.created_at).getTime() - new Date(a.completed_at || a.created_at).getTime(); } return historySortAsc ? -cmp : cmp; }); }, [queue, historySortBy, historySortAsc, matchesLocationFilter, filterLocation]); // Calculate total queue time const totalQueueTime = useMemo(() => { return pendingItems.reduce((acc, item) => acc + (item.print_time_seconds || 0), 0); }, [pendingItems]); // Calculate total material weight const totalWeight = useMemo(() => { return pendingItems.reduce((acc, item) => acc + (item.filament_used_grams || 0), 0); }, [pendingItems]); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = pendingItems.findIndex(i => i.id === active.id); const newIndex = pendingItems.findIndex(i => i.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { const reordered = arrayMove(pendingItems, oldIndex, newIndex); const updates = reordered.map((item, index) => ({ id: item.id, position: index + 1, })); reorderMutation.mutate(updates); } }; return (
{/* Header */}

{t('queue.title')}

{t('queue.subtitle')}

{/* Summary Cards */}

{activeItems.length}

{t('queue.summary.printing')}

{pendingItems.length}

{t('queue.summary.queued')}

{formatDuration(totalQueueTime)}

{t('queue.summary.totalTime')}

{formatWeight(totalWeight)}

{t('queue.summary.totalWeight')}

{historyItems.length}

{t('queue.summary.history')}

{/* Filters */}
{uniqueLocations.length > 0 && ( )}
{historyItems.length > 0 && ( )}
{isLoading ? (
{t('common.loading')}
) : queue?.length === 0 ? (

{t('queue.empty.title')}

{t('queue.empty.description')}

) : (
{/* Active Prints */} {activeItems.length > 0 && (

{t('queue.sections.currentlyPrinting')}

{activeItems.map((item) => ( {}} onCancel={() => {}} onRemove={() => {}} onStop={() => setConfirmAction({ type: 'stop', item })} onRequeue={() => {}} onStart={() => {}} timeFormat={timeFormat} hasPermission={hasPermission} canModify={canModify} printerState={item.printer_id ? printerStateMap[item.printer_id] : null} t={t} /> ))}
)} {/* Pending Queue */} {pendingItems.length > 0 && (

{t('queue.sections.queued')} ({t('queue.itemCount', { count: pendingItems.length })}) {t('queue.dragToReorder')}

{/* Bulk action toolbar */}
{selectedItems.length > 0 && ( <> {t('queue.bulkEdit.selected', { count: selectedItems.length })}
)}
i.id)} strategy={verticalListSortingStrategy} >
{pendingItems.map((item, index) => ( setEditItem(item)} onCancel={() => setConfirmAction({ type: 'cancel', item })} onRemove={() => {}} onStop={() => {}} onRequeue={() => {}} onStart={() => startMutation.mutate(item.id)} timeFormat={timeFormat} isSelected={selectedItems.includes(item.id)} onToggleSelect={() => handleToggleSelect(item.id)} hasPermission={hasPermission} canModify={canModify} t={t} /> ))}
)} {/* History */} {historyItems.length > 0 && (

{t('queue.sections.history')} ({t('queue.itemCount', { count: historyItems.length })})

{historyItems.slice(0, 20).map((item, index) => ( {}} onCancel={() => {}} onRemove={() => setConfirmAction({ type: 'remove', item })} onStop={() => {}} onRequeue={() => setRequeueItem(item)} onStart={() => {}} timeFormat={timeFormat} hasPermission={hasPermission} canModify={canModify} t={t} /> ))}
)}
)} {/* Edit Modal */} {editItem && ( setEditItem(null)} /> )} {/* Re-queue Modal */} {requeueItem && ( setRequeueItem(null)} /> )} {/* Confirm Action Modal */} {confirmAction && ( { if (confirmAction.type === 'cancel') { cancelMutation.mutate(confirmAction.item.id); } else if (confirmAction.type === 'stop') { stopMutation.mutate(confirmAction.item.id); } else { removeMutation.mutate(confirmAction.item.id); } setConfirmAction(null); }} onCancel={() => setConfirmAction(null)} /> )} {/* Clear History Confirm Modal */} {showClearHistoryConfirm && ( { clearHistoryMutation.mutate(); setShowClearHistoryConfirm(false); }} onCancel={() => setShowClearHistoryConfirm(false)} /> )} {/* Bulk Edit Modal */} {showBulkEditModal && ( ({ id: p.id, name: p.name })) || []} onSave={(data) => { if (Object.keys(data).length > 0) { bulkUpdateMutation.mutate({ item_ids: selectedItems, ...data }); } }} onClose={() => setShowBulkEditModal(false)} isSaving={bulkUpdateMutation.isPending} t={t} /> )}
); }