WorkflowTimeline.tsx
1 // Copyright (c) 2026 VPL Solutions. All rights reserved. 2 // Licensed under the MIT License. See LICENSE for details. 3 4 import { Check, Shield, Lock } from 'lucide-react'; 5 import type { InvestigationStatus } from '../../api/investigation'; 6 import { STATUS_META, TIMELINE_STATES, APPROVAL_GATE_INDEX } from '../../data/investigationStates'; 7 8 interface WorkflowTimelineProps { 9 currentStatus: InvestigationStatus; 10 } 11 12 export function WorkflowTimeline({ currentStatus }: WorkflowTimelineProps) { 13 const currentMeta = STATUS_META[currentStatus]; 14 const currentOrder = currentMeta.order; 15 16 return ( 17 <div className="w-full"> 18 {/* Desktop: horizontal timeline */} 19 <div className="hidden md:block"> 20 <div className="flex items-center"> 21 {TIMELINE_STATES.map((status, i) => { 22 const meta = STATUS_META[status]; 23 const isComplete = meta.order < currentOrder; 24 const isCurrent = status === currentStatus; 25 const isFuture = meta.order > currentOrder; 26 const isApprovalGate = i === APPROVAL_GATE_INDEX; 27 28 return ( 29 <div key={status} className="flex items-center flex-1 last:flex-none"> 30 {/* Approval gate divider */} 31 {isApprovalGate && ( 32 <div className="flex flex-col items-center mx-1 shrink-0"> 33 <div className="flex items-center gap-1 px-2 py-0.5 rounded bg-amber-100 dark:bg-amber-900/30 border border-amber-300 dark:border-amber-700"> 34 <Lock className="w-3 h-3 text-amber-600 dark:text-amber-400" /> 35 <span className="text-[9px] font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400"> 36 Approval Gate 37 </span> 38 </div> 39 </div> 40 )} 41 42 {/* Node */} 43 <div className="flex flex-col items-center gap-1 shrink-0"> 44 <div className={` 45 w-8 h-8 rounded-full flex items-center justify-center border-2 transition-all 46 ${isComplete 47 ? 'bg-emerald-500 border-emerald-500 text-white' 48 : isCurrent 49 ? `${meta.dotColor} border-current ring-4 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 ${meta.dotColor}/30 text-white` 50 : 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-400' 51 } 52 `}> 53 {isComplete ? ( 54 <Check className="w-4 h-4" /> 55 ) : isCurrent ? ( 56 <span className="w-2 h-2 rounded-full bg-white" /> 57 ) : ( 58 <span className="w-2 h-2 rounded-full bg-current opacity-40" /> 59 )} 60 </div> 61 <span className={`text-[10px] font-medium text-center leading-tight max-w-[70px] ${ 62 isCurrent ? meta.color : isFuture ? 'text-gray-400 dark:text-gray-500' : 'text-gray-600 dark:text-gray-300' 63 }`}> 64 {meta.shortLabel} 65 </span> 66 </div> 67 68 {/* Connector line */} 69 {i < TIMELINE_STATES.length - 1 && ( 70 <div className={`flex-1 h-0.5 mx-1 ${ 71 isComplete ? 'bg-emerald-500' : 'bg-gray-200 dark:bg-gray-700' 72 }`} /> 73 )} 74 </div> 75 ); 76 })} 77 </div> 78 </div> 79 80 {/* Mobile: vertical compact */} 81 <div className="md:hidden space-y-1"> 82 {TIMELINE_STATES.map((status, i) => { 83 const meta = STATUS_META[status]; 84 const isComplete = meta.order < currentOrder; 85 const isCurrent = status === currentStatus; 86 const isApprovalGate = i === APPROVAL_GATE_INDEX; 87 88 return ( 89 <div key={status}> 90 {isApprovalGate && ( 91 <div className="flex items-center gap-2 py-1 pl-4"> 92 <Shield className="w-3 h-3 text-amber-500" /> 93 <span className="text-[10px] font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400"> 94 No execution without approval 95 </span> 96 </div> 97 )} 98 <div className="flex items-center gap-3"> 99 <div className={`w-2.5 h-2.5 rounded-full shrink-0 ${ 100 isComplete ? 'bg-emerald-500' : isCurrent ? meta.dotColor : 'bg-gray-300 dark:bg-gray-600' 101 }`} /> 102 <span className={`text-xs ${ 103 isCurrent ? `font-semibold ${meta.color}` : isComplete ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400' 104 }`}> 105 {meta.label} 106 </span> 107 {isComplete && <Check className="w-3 h-3 text-emerald-500" />} 108 </div> 109 </div> 110 ); 111 })} 112 </div> 113 </div> 114 ); 115 }