/ src / components / ops / WorkflowTimeline.tsx
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  }