import { CheckCircle, XCircle, SkipForward, Ban, RefreshCw, Trash2, Printer, Timer, Layers, User, AlertCircle, Weight, } from 'lucide-react'; import { api } from '../api/client'; import { type TimeFormat, formatDuration, formatRelativeTime } from '../utils/date'; import type { PrintQueueItem, Permission } from '../api/client'; import { Button } from './Button'; import { queueItemDisplayName } from '../utils/queueItemName'; const STATUS_CONFIG = { completed: { icon: CheckCircle, color: 'text-emerald-600 dark:text-emerald-400', border: 'border-l-emerald-500' }, failed: { icon: XCircle, color: 'text-red-600 dark:text-red-400', border: 'border-l-red-500' }, skipped: { icon: SkipForward, color: 'text-orange-600 dark:text-orange-400', border: 'border-l-gray-500' }, cancelled: { icon: Ban, color: 'text-gray-400', border: 'border-l-gray-500' }, } as const; /** Bambu encodes "no filament" as transparent/zeroed RGBA. The slicer's * filament_color is a hex string like "RRGGBBAA" or "RRGGBB"; treat all-zero * / unparsable as no swatch. */ function normalizeFilamentColor(raw: string | null | undefined): string | null { if (!raw) return null; const clean = raw.startsWith('#') ? raw.slice(1) : raw; if (/^0{6,8}$/.test(clean)) return null; if (!/^[0-9a-fA-F]{6,8}$/.test(clean)) return null; return `#${clean.slice(0, 6)}`; } export function CompactHistoryRow({ item, onRequeue, onRemove, timeFormat = 'system', hasPermission, canModify, t, }: { item: PrintQueueItem; onRequeue: () => void; onRemove: () => void; timeFormat?: TimeFormat; hasPermission: (permission: Permission) => boolean; canModify: (resource: 'queue' | 'archives' | 'library', action: 'update' | 'delete' | 'reprint', createdById: number | null | undefined) => boolean; t: (key: string, options?: Record) => string; }) { const config = STATUS_CONFIG[item.status as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.cancelled; const StatusIcon = config.icon; const displayName = queueItemDisplayName(item); const thumbnailUrl = item.archive_thumbnail ? api.getArchiveThumbnail(item.archive_id!) : item.library_file_thumbnail ? api.getLibraryFileThumbnailUrl(item.library_file_id!) : null; const completedTime = item.completed_at || item.created_at; const filamentColor = normalizeFilamentColor(item.filament_color); const filamentMass = item.filament_used_grams ? Math.round(item.filament_used_grams) : null; // Failed and skipped prints carry the diagnostic in error_message; surface // it inline so the user doesn't have to reopen the row to see why. const showErrorMessage = !!item.error_message && (item.status === 'failed' || item.status === 'skipped'); return (
{/* Top row — status / thumb / name / time / actions */}
{thumbnailUrl ? ( ) : (
)}
{/* Hover preview — desktop only via CSS @media. Positioned to the right of the thumbnail; the parent card has no overflow:hidden so the popup escapes its rounded border. pointer-events-none so it doesn't interfere with clicks on rows below. */} {thumbnailUrl && (
)}
{displayName} {formatRelativeTime(completedTime, timeFormat, t)}
{/* Meta row — printer / filament / duration / user. Indented under the thumbnail so it lines up with the name. */}
{item.printer_name && ( {item.printer_name} )} {(filamentColor || filamentMass) && ( {filamentColor ? ( ) : ( )} {filamentMass ? `${filamentMass}g` : null} {filamentMass && item.filament_type ? ` ${item.filament_type}` : null} {!filamentMass && item.filament_type ? item.filament_type : null} )} {item.print_time_seconds && ( {formatDuration(item.print_time_seconds)} )} {item.created_by_username && ( {item.created_by_username} )}
{/* Error message — only rendered on failed/skipped rows. */} {showErrorMessage && (
{item.error_message}
)}
); }