import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useParams, useNavigate, Link } from 'react-router-dom' import { useState, useMemo, Fragment } from 'react' import { ArrowLeft, Send, Trash2, FileBox, AlertTriangle, CheckCircle2, Image as ImageIcon, Unlink, RotateCcw, LayoutList, LayoutGrid, X, ChevronDown, ChevronUp, ChevronsUpDown, Search, SlidersHorizontal, FileSpreadsheet, Box, Loader2, Play, RefreshCw, ExternalLink, Ban, StopCircle, Scissors, Plus, Wand2, Download, } from 'lucide-react' import { toast } from 'sonner' import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbnail, patchOrderItem, removeOrderLine, dispatchRenders, cancelLineRender, cancelOrderRenders, splitMissingStep, generateLinesFromItems, downloadOrderRenders } from '../api/orders' import type { OrderItem, OrderLine } from '../api/orders' import { listOutputTypes } from '../api/outputTypes' import type { OutputType } from '../api/outputTypes' import { useAuthStore } from '../store/auth' import StepDropzone from '../components/upload/StepDropzone' import CadPartMaterials from '../components/orders/CadPartMaterials' import LiveRenderLog from '../components/LiveRenderLog' // ── Filter / sort types ─────────────────────────────────────────────────────── type SortKey = 'row_index' | 'name_cad_modell' | 'baureihe' | 'item_status' type SortDir = 'asc' | 'desc' interface TableFilters { search: string itemStatus: '' | 'pending' | 'approved' | 'rejected' stepStatus: '' | 'with_step' | 'missing_step' rendering: '' | 'rendering' | 'non_rendering' } const EMPTY_FILTERS: TableFilters = { search: '', itemStatus: '', stepStatus: '', rendering: '', } function countActiveFilters(f: TableFilters): number { return ( (f.search.trim() !== '' ? 1 : 0) + (f.itemStatus !== '' ? 1 : 0) + (f.stepStatus !== '' ? 1 : 0) + (f.rendering !== '' ? 1 : 0) ) } // ── Page ───────────────────────────────────────────────────────────────────── export default function OrderDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const qc = useQueryClient() const user = useAuthStore((s) => s.user) const [galleryView, setGalleryView] = useState(false) const [gallerySelected, setGallerySelected] = useState(null) const [genLinesOpen, setGenLinesOpen] = useState(false) const [genLinesSelected, setGenLinesSelected] = useState>({}) const [isDownloading, setIsDownloading] = useState(false) // Table state const [filters, setFilters] = useState(EMPTY_FILTERS) const [sortKey, setSortKey] = useState('row_index') const [sortDir, setSortDir] = useState('asc') const [expandedId, setExpandedId] = useState(null) const { data: order, isLoading } = useQuery({ queryKey: ['order', id], queryFn: () => getOrder(id!), }) const submitMut = useMutation({ mutationFn: () => submitOrder(id!), onSuccess: () => { toast.success('Order submitted') qc.invalidateQueries({ queryKey: ['order', id] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Submit failed'), }) const deleteMut = useMutation({ mutationFn: () => deleteOrder(id!), onSuccess: () => { toast.success('Order deleted') navigate('/orders') }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Delete failed'), }) const dispatchMut = useMutation({ mutationFn: () => dispatchRenders(id!), onSuccess: (data) => { toast.success(`${data.dispatched} render${data.dispatched !== 1 ? 's' : ''} dispatched`) qc.invalidateQueries({ queryKey: ['order', id] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Dispatch failed'), }) const cancelAllMut = useMutation({ mutationFn: () => cancelOrderRenders(id!), onSuccess: (data) => { toast.success(`${data.cancelled} render${data.cancelled !== 1 ? 's' : ''} cancelled`) if (data.errors?.length) toast.warning(`Some cancellations had issues: ${data.errors.join('; ')}`) qc.invalidateQueries({ queryKey: ['order', id] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Cancel failed'), }) const { data: allOutputTypes = [] } = useQuery({ queryKey: ['output-types'], queryFn: () => listOutputTypes(false), enabled: genLinesOpen, }) const generateLinesMut = useMutation({ mutationFn: () => { const typeIds = Object.entries(genLinesSelected).filter(([, v]) => v).map(([k]) => k) return generateLinesFromItems(id!, typeIds) }, onSuccess: (data) => { toast.success(`${data.created} line${data.created !== 1 ? 's' : ''} created`) if (data.no_product_count > 0) { toast.warning(`${data.no_product_count} item${data.no_product_count !== 1 ? 's' : ''} could not be matched to a product`) } if (data.no_step_count > 0) { toast.warning(`${data.no_step_count} product${data.no_step_count !== 1 ? 's' : ''} have no STEP file — upload STEP files before dispatching renders`) } setGenLinesOpen(false) setGenLinesSelected({}) qc.invalidateQueries({ queryKey: ['order', id] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to generate lines'), }) const splitMut = useMutation({ mutationFn: () => splitMissingStep(id!), onSuccess: (data) => { const total = data.moved_item_count + data.moved_line_count toast.success( `Draft order ${data.new_order_number} created — ${total} item${total !== 1 ? 's' : ''} without STEP moved there`, ) qc.invalidateQueries({ queryKey: ['order', id] }) qc.invalidateQueries({ queryKey: ['orders'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Split failed'), }) if (isLoading) return
Loading…
if (!order) return
Order not found
const canSubmit = order.status === 'draft' const canDelete = order.status === 'draft' || order.status === 'rejected' const isDraft = order.status === 'draft' const isPrivileged = user?.role === 'admin' || user?.role === 'project_manager' const rp = order.render_progress const hasRetryable = rp && (rp.pending > 0 || rp.failed > 0 || (rp as any).cancelled > 0) const canDispatch = isPrivileged && (order.status === 'processing' || order.status === 'submitted' || order.status === 'completed') const hasActiveRenders = rp && (rp.processing > 0 || rp.pending > 0) const canCancelRenders = isPrivileged && order.status === 'processing' && hasActiveRenders const hasCompletedRenders = rp && rp.completed > 0 async function handleDownloadRenders() { setIsDownloading(true) try { await downloadOrderRenders(id!, order!.order_number) } catch (e: any) { toast.error(e.response?.data?.detail || 'Download failed') } finally { setIsDownloading(false) } } // Items marked for rendering require a STEP file before submission const renderingItems = order.items.filter((i: any) => i.medias_rendering) const missingStep = renderingItems.filter((i: any) => !i.cad_file_id) // Order lines with an output type also require the product to have a STEP file const missingStepLines = (order.lines ?? []).filter( (l: any) => l.output_type_id && !l.product?.cad_file_id, ) const allRenderingHaveStep = missingStep.length === 0 && missingStepLines.length === 0 const totalMissingStep = missingStep.length + missingStepLines.length // Allow admin/PM to generate lines for orders that have items but no lines const canGenerateLines = isPrivileged && (isDraft || order.status === 'submitted') && (order.lines?.length ?? 0) === 0 && (order.items?.length ?? 0) > 0 function handleSort(key: SortKey) { if (sortKey === key) { setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) } else { setSortKey(key) setSortDir('asc') } } function updateFilter(key: K, value: TableFilters[K]) { setFilters((prev) => ({ ...prev, [key]: value })) } const activeFilterCount = countActiveFilters(filters) return (
{/* Header */}
Back

{order.order_number}

{order.items.length} items · Created {new Date(order.created_at).toLocaleString('de-DE')}

{order.estimated_price != null && (
Estimated: {Number(order.estimated_price).toFixed(2)}
)} {canDelete && ( )} {canCancelRenders && ( )} {hasCompletedRenders && ( )} {canDispatch && ( )} {canSubmit && ( )}
{/* Draft action banner */} {isDraft && (
{allRenderingHaveStep ? (

This order is a draft — all rendering items have STEP files attached.

Ready to submit.

) : (

{totalMissingStep} item{totalMissingStep !== 1 ? 's' : ''} still need a STEP file

Upload the missing STEP files below, or move these items to a separate draft order so you can submit the rest now.

)}
)} {/* Missing STEP file warning — only for rendering items */} {isDraft && !allRenderingHaveStep && (

{missingStep.length} rendering item{missingStep.length > 1 ? 's' : ''} still need a STEP file

Upload the corresponding .stp files below — matched automatically by filename. Submission is blocked until all items marked for rendering have a CAD file.

{missingStep.slice(0, 8).map((item: any) => ( {item.name_cad_modell || `row ${item.row_index}`} ))} {missingStep.length > 8 && ( +{missingStep.length - 8} more )}
)} {/* All rendering items have STEP files */} {isDraft && allRenderingHaveStep && renderingItems.length > 0 && (

All {renderingItems.length} rendering item{renderingItems.length > 1 ? 's' : ''} have a STEP file attached — ready to submit.

)} {order.notes && (

{order.notes}

)} {/* Render progress bar */} {rp && rp.total > 0 && (order.status === 'processing' || order.status === 'completed') && (

Render Progress

{rp.completed}/{rp.total} completed {rp.processing > 0 && ({rp.processing} rendering)} {rp.failed > 0 && ({rp.failed} failed)} {(rp as any).cancelled > 0 && ({(rp as any).cancelled} cancelled)} {order.status === 'processing' && rp.processing > 0 && ( )}
{rp.completed > 0 && (
)} {rp.processing > 0 && (
)} {rp.failed > 0 && (
)} {(rp as any).cancelled > 0 && (
)}
)} {/* STEP upload section — only for draft orders */} {isDraft && (

Upload STEP Files

Drop one or multiple .stp / .step files — they are matched to items by filename stem (case-insensitive).

qc.invalidateQueries({ queryKey: ['order', id] })} />
)} {/* Source spreadsheet — reference data, collapsed by default after submission */} {order.source_excel != null && ( qc.invalidateQueries({ queryKey: ['order', id] })} /> )} {/* ── Product Output Lines ───────────────────────────────────────────────── Shown ABOVE the Order Items table so the primary "what was ordered" view (product + output type + render status) is immediately visible */} {((order.lines?.length ?? 0) > 0 || canGenerateLines) && (

Product Output Lines ({order.lines?.length ?? 0})

{canGenerateLines && ( )}
{/* Generate lines panel */} {canGenerateLines && genLinesOpen && (

This order has {order.items?.length ?? 0} items but no output lines. Select one or more output types to generate renderable lines for all matching products:

{allOutputTypes.length === 0 ? (

Loading output types…

) : (
{allOutputTypes.map((ot: OutputType) => ( ))}
)}
)} {(order.lines?.length ?? 0) > 0 && (
{isDraft && {order.lines.map((line: OrderLine) => ( qc.invalidateQueries({ queryKey: ['order', id] })} /> ))}
Thumb Product Output Type Backend Price Render Status}
)}
)} {/* Order items */}
{/* Toolbar */}
{/* Top row: title + view toggle */}

Order Items ({order.items.length})

{/* Filter bar — only in table view */} {!galleryView && (
{/* Text search */}
updateFilter('search', e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-accent" />
{/* Item status filter */} {/* STEP status filter */} {/* Rendering filter */} {/* Active filter count + clear */} {activeFilterCount > 0 && ( )} {activeFilterCount === 0 && (
Filter
)}
)}
{galleryView ? ( <>
{order.items.map((item: any) => ( setGallerySelected(item)} /> ))}
{gallerySelected && ( setGallerySelected(null)} onUnlink={() => { setGallerySelected(null); qc.invalidateQueries({ queryKey: ['order', id] }) }} onRerender={() => qc.invalidateQueries({ queryKey: ['order', id] })} /> )} ) : ( qc.invalidateQueries({ queryKey: ['order', id] })} /> )}
) } // ── Order Line Row ──────────────────────────────────────────────────────────── function OrderLineRow({ line, orderId, isDraft, isPrivileged, onRemoved, }: { line: OrderLine orderId: string isDraft: boolean isPrivileged: boolean onRemoved: () => void }) { const qc = useQueryClient() const removeMut = useMutation({ mutationFn: () => removeOrderLine(orderId, line.id), onSuccess: onRemoved, onError: (e: any) => toast.error(e.response?.data?.detail || 'Remove failed'), }) const cancelMut = useMutation({ mutationFn: () => cancelLineRender(orderId, line.id), onSuccess: () => { toast.success('Render cancelled') qc.invalidateQueries({ queryKey: ['order', orderId] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Cancel failed'), }) const canCancel = isPrivileged && (line.render_status === 'processing' || line.render_status === 'pending') && line.output_type_id const renderStatusColor: Record = { pending: 'bg-surface-muted text-content-muted', processing: 'bg-status-info-bg text-status-info-text', completed: 'bg-status-success-bg text-status-success-text', failed: 'bg-status-error-bg text-status-error-text', cancelled: 'bg-status-warning-bg text-status-warning-text', } return ( {/* Thumbnail */} {line.thumbnail_url ? ( {line.product.name ) : (
)} {/* Product */} e.stopPropagation()} > {line.product.name || line.product.pim_id} {line.product.pim_id} {/* Output Type + Position */}
{line.output_type ? ( {line.output_type.name} ) : ( tracking only )} {line.render_position_name && ( {line.render_position_name} )}
{/* Backend */} {line.render_backend_used ? ( line.render_backend_used === 'flamenco' && line.flamenco_job_id ? ( e.stopPropagation()} className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-status-warning-bg text-status-warning-text font-medium hover:bg-surface-hover transition-colors" > Flamenco ) : line.render_backend_used === 'celery' ? ( Celery ) : ( {line.render_backend_used} ) ) : ( )} {/* Price */} {line.unit_price != null ? ( {line.unit_price.toFixed(2)} ) : ( )} {/* Render Status */} {line.output_type_id ? (
{line.render_status === 'processing' && } {line.render_status} {canCancel && ( )}
) : ( )} {/* Item Status */} {/* Remove (draft only) */} {isDraft && ( )} ) } // ── Table component ─────────────────────────────────────────────────────────── interface OrderItemsTableProps { items: OrderItem[] orderId: string isDraft: boolean filters: TableFilters sortKey: SortKey sortDir: SortDir onSort: (key: SortKey) => void expandedId: string | null onExpand: (id: string | null) => void onUnlink: () => void } function OrderItemsTable({ items, orderId, isDraft, filters, sortKey, sortDir, onSort, expandedId, onExpand, onUnlink, }: OrderItemsTableProps) { const filtered = useMemo(() => { let result = [...items] // Text search if (filters.search.trim()) { const q = filters.search.trim().toLowerCase() result = result.filter((item) => [item.name_cad_modell, item.baureihe, item.ebene1, item.ebene2] .some((v) => v?.toLowerCase().includes(q)) ) } // Item status if (filters.itemStatus) { result = result.filter((item) => item.item_status === filters.itemStatus) } // STEP status if (filters.stepStatus === 'with_step') { result = result.filter((item) => !!item.cad_file_id) } else if (filters.stepStatus === 'missing_step') { result = result.filter((item) => !item.cad_file_id) } // Rendering filter if (filters.rendering === 'rendering') { result = result.filter((item) => item.medias_rendering === true) } else if (filters.rendering === 'non_rendering') { result = result.filter((item) => item.medias_rendering !== true) } // Sort result.sort((a, b) => { let av: string | number = '' let bv: string | number = '' if (sortKey === 'row_index') { av = a.row_index; bv = b.row_index } else if (sortKey === 'name_cad_modell') { av = a.name_cad_modell ?? ''; bv = b.name_cad_modell ?? '' } else if (sortKey === 'baureihe') { av = a.baureihe ?? ''; bv = b.baureihe ?? '' } else if (sortKey === 'item_status') { av = a.item_status ?? ''; bv = b.item_status ?? '' } if (av < bv) return sortDir === 'asc' ? -1 : 1 if (av > bv) return sortDir === 'asc' ? 1 : -1 return 0 }) return result }, [items, filters, sortKey, sortDir]) if (filtered.length === 0) { return (
No items match the current filters.
) } return (
{filtered.map((item) => ( onExpand(expandedId === item.id ? null : item.id)} onUnlink={onUnlink} /> ))}
Img Ebene 1 Ebene 2 Lagertyp Rendering Parts STEP
) } // ── Sortable table header ───────────────────────────────────────────────────── function SortableTh({ label, sortKey: key, current, dir, onSort, className = '', }: { label: string sortKey: SortKey current: SortKey dir: SortDir onSort: (k: SortKey) => void className?: string }) { const active = current === key return ( onSort(key)} > {label} {active ? ( dir === 'asc' ? : ) : ( )} ) } // ── Table row + expand panel ────────────────────────────────────────────────── function ItemTableRow({ item, orderId, isDraft, expanded, onExpand, onUnlink, }: { item: OrderItem orderId: string isDraft: boolean expanded: boolean onExpand: () => void onUnlink: () => void }) { const hasStep = !!item.cad_file_id const [imgError, setImgError] = useState(false) const [thumbVersion, setThumbVersion] = useState(0) const unlinkMut = useMutation({ mutationFn: () => unlinkCadFile(orderId, item.id), onSuccess: () => { toast.success('CAD file removed'); onUnlink() }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Remove failed'), }) const rerenderMut = useMutation({ mutationFn: () => regenerateItemThumbnail(orderId, item.id), onSuccess: () => { toast.success('Thumbnail re-render queued — refresh in a moment') setTimeout(() => setThumbVersion((v) => v + 1), 8000) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Re-render failed'), }) const thumbSrc = `/api/cad/${item.cad_file_id}/thumbnail${thumbVersion > 0 ? `?v=${thumbVersion}` : ''}` return ( <> {/* Row # */} {item.row_index} {/* Thumbnail 40px */}
{hasStep && !imgError ? ( thumb setImgError(true)} /> ) : ( )}
{/* CAD Model */}

{item.name_cad_modell || '—'}

{/* Baureihe */}

{item.baureihe || '—'}

{/* Ebene 1 */}

{item.ebene1 || '—'}

{/* Ebene 2 */}

{item.ebene2 || '—'}

{/* Lagertyp */}

{item.lagertyp || '—'}

{/* Rendering */} {item.medias_rendering ? Yes : No} {/* Components count */} {(item.components?.length ?? 0) || '—'} {/* STEP status */} {hasStep ? ( STEP ) : ( )} {/* Item status */} {/* Expanded detail panel */} {expanded && (
{/* Thumbnail */}

CAD Thumbnail

{hasStep ? (
{isDraft && ( )}
) : (
No STEP file yet
)}
{item.components && item.components.length > 0 && (

Components ({item.components.length})

{item.components.map((c: any, i: number) => ( ))}
# Type Part Name Material
{i + 1} {c.component_type || '—'} {c.part_name || '—'} {c.material || '—'}
)} {/* CAD part material assignments */} {(item as any).cad_parsed_objects && (item as any).cad_parsed_objects.length > 0 && ( )}
)} ) } // ── Reusable display helpers ────────────────────────────────────────────────── function ThumbnailPreview({ cadFileId, version = 0 }: { cadFileId: string; version?: number }) { const [imgError, setImgError] = useState(false) const src = `/api/cad/${cadFileId}/thumbnail${version > 0 ? `?v=${version}` : ''}` if (imgError) { return (
No preview
) } return (
{/* Small thumbnail */} CAD thumbnail setImgError(true)} /> {/* Full-res hover overlay */}
{/* Arrow pointing left */}
CAD preview

Full resolution preview

) } function Field({ label, value }: { label: string; value: string | null | undefined }) { return (

{label}

{value || '—'}

) } function StatusBadge({ status }: { status: string }) { const map: Record = { draft: 'badge-gray', submitted: 'badge-blue', processing: 'badge-yellow', completed: 'badge-green', rejected: 'badge-red', } return {status} } function ItemStatusBadge({ status }: { status: string }) { const map: Record = { pending: 'badge-gray', approved: 'badge-green', rejected: 'badge-red', } return {status} } // ── Gallery view ───────────────────────────────────────────────────────────── function GalleryCard({ item, isDraft, onClick }: { item: any; isDraft: boolean; onClick: () => void }) { const hasStep = !!item.cad_file_id const [imgError, setImgError] = useState(false) return (
{hasStep && !imgError ? ( {item.name_cad_modell setImgError(true)} /> ) : (
)} {/* Status dot */} {isDraft && ( )}

{item.name_cad_modell || '—'}

{hasStep && STEP}
) } // ── Source Spreadsheet ──────────────────────────────────────────────────────── const STD_COLS: { key: keyof OrderItem; label: string; editable?: boolean }[] = [ { key: 'ebene1', label: 'Ebene 1', editable: true }, { key: 'ebene2', label: 'Ebene 2', editable: true }, { key: 'baureihe', label: 'Baureihe', editable: true }, { key: 'pim_id', label: 'PIM-ID', editable: true }, { key: 'produkt_baureihe', label: 'Produkt/Baureihe',editable: true }, { key: 'gewaehltes_produkt', label: 'Gew. Produkt', editable: true }, { key: 'name_cad_modell', label: 'CAD-Modell', editable: true }, { key: 'gewuenschte_bildnummer', label: 'Bildnummer', editable: true }, { key: 'lagertyp', label: 'Lagertyp', editable: true }, ] function SourceSpreadsheet({ items, sourceExcel, isDraft, defaultOpen = true, orderId, onItemUpdated, }: { items: OrderItem[] sourceExcel: string | null | undefined isDraft?: boolean defaultOpen?: boolean orderId?: string onItemUpdated?: () => void }) { const [open, setOpen] = useState(defaultOpen) const [saving, setSaving] = useState(null) // itemId being saved const sorted = useMemo(() => [...items].sort((a, b) => a.row_index - b.row_index), [items]) const maxComponents = Math.max(0, ...sorted.map((i) => i.components?.length ?? 0)) const fileName = sourceExcel ? sourceExcel.replace(/\\/g, '/').split('/').pop() ?? sourceExcel : null async function saveField(itemId: string, field: keyof OrderItem, value: string | boolean | null) { if (!orderId) return setSaving(itemId) try { await patchOrderItem(orderId, itemId, { [field]: value }) onItemUpdated?.() } catch { toast.error('Failed to save change') } finally { setSaving(null) } } return (
{/* Collapsible header */} {open && (
{/* Rendering toggle column */} {STD_COLS.map((c) => ( ))} {Array.from({ length: maxComponents }, (_, i) => ( ))} {sorted.map((item) => ( {/* Row # */} {/* Rendering toggle */} {/* Text fields */} {STD_COLS.map((c) => { const raw = (item as any)[c.key] const display = raw ?? '' return ( ) })} {/* Component columns (read-only) */} {Array.from({ length: maxComponents }, (_, i) => { const comp = (item.components as any[])?.[i] return ( ) })} ))}
# Rendering {c.label} Bauteil {i + 1} Material {i + 1}
{item.row_index} {isDraft ? ( ) : ( {item.medias_rendering ? 'Yes' : 'No'} )} {isDraft && c.editable ? ( { const newVal = e.target.value.trim() || null if (newVal !== (raw ?? null)) { saveField(item.id, c.key, newVal) } }} onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur() }} className="w-full min-w-[120px] max-w-[200px] px-1.5 py-0.5 text-xs text-content-secondary rounded border border-transparent hover:border-border-default focus:border-accent focus:outline-none focus:bg-surface bg-transparent" /> ) : ( {display || '—'} )} {comp?.part_name || '—'} {comp?.material || '—'}
)}
) } // ── Gallery modal ───────────────────────────────────────────────────────────── function GalleryModal({ item, orderId, isDraft, onClose, onUnlink, onRerender, }: { item: any; orderId: string; isDraft: boolean onClose: () => void; onUnlink: () => void; onRerender: () => void }) { const hasStep = !!item.cad_file_id const [thumbVersion, setThumbVersion] = useState(0) const unlinkMut = useMutation({ mutationFn: () => unlinkCadFile(orderId, item.id), onSuccess: () => { toast.success('CAD file removed'); onUnlink() }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Remove failed'), }) const rerenderMut = useMutation({ mutationFn: () => regenerateItemThumbnail(orderId, item.id), onSuccess: () => { toast.success('Re-render queued — refreshing in a moment') setTimeout(() => { setThumbVersion((v) => v + 1); onRerender() }, 8000) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Re-render failed'), }) return (
e.stopPropagation()} > {/* Header */}

{item.name_cad_modell || `Row ${item.row_index}`}

{/* Thumbnail */}
{hasStep ? ( 0 ? `?v=${thumbVersion}` : ''}`} alt="CAD preview" className="max-h-64 object-contain rounded-lg" /> ) : (

No STEP file attached

)}
{/* Fields */}
{/* Actions */} {hasStep && (
{isDraft && ( )}
)}
) }