Files
Ajjj 936ae1c6b2
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: implement comprehensive dashboard controller and modular administrative and project management interfaces
2026-08-03 18:28:52 +08:00

69 lines
3.0 KiB
TypeScript

import { Badge } from '@/Components/ui/badge';
import { CheckCircle2, Circle, XCircle, SkipForward } from 'lucide-react';
interface Step {
id: number; ulid: string;
order: number;
status: string;
notes?: string;
acted_at?: string;
approver: { id: number; ulid: string; name: string };
}
interface Props {
steps: Step[];
}
const statusConfig: Record<string, { icon: React.ComponentType<{ className?: string }>; color: string; bgColor: string }> = {
pending: { icon: Circle, color: 'text-gray-400', bgColor: 'bg-gray-100' },
approved: { icon: CheckCircle2, color: 'text-green-600', bgColor: 'bg-green-50' },
rejected: { icon: XCircle, color: 'text-red-600', bgColor: 'bg-red-50' },
skipped: { icon: SkipForward, color: 'text-gray-400', bgColor: 'bg-gray-50' },
};
export default function ApprovalTimeline({ steps }: Props) {
const visibleSteps = steps.filter(s => s.status !== 'skipped');
return (
<div className="space-y-0">
{visibleSteps.map((step, idx) => {
const config = statusConfig[step.status] || statusConfig.pending;
const Icon = config.icon;
const isLast = idx === visibleSteps.length - 1;
return (
<div key={step.id} className="flex gap-3">
{/* Timeline line + icon */}
<div className="flex flex-col items-center">
<div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${config.bgColor}`}>
<Icon className={`h-4 w-4 ${config.color}`} />
</div>
{!isLast && (
<div className="w-px flex-1 bg-gray-200 min-h-[24px]" />
)}
</div>
{/* Content */}
<div className="pb-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{step.approver.name}</span>
<Badge variant={step.status === 'approved' ? 'default' : step.status === 'rejected' ? 'destructive' : 'outline'} className="text-xs">
{step.status.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
</Badge>
</div>
{step.notes && (
<p className="mt-1 text-xs text-gray-500 italic">"{step.notes}"</p>
)}
{step.acted_at && (
<p className="mt-0.5 text-xs text-gray-400">
{new Date(step.acted_at).toLocaleString()}
</p>
)}
</div>
</div>
);
})}
</div>
);
}