feat: Add dark mode support to dashboard components and filter production timeline

- Replace Tailwind color classes with CSS variables in all dashboard components
- HealthStatusCard: Use CSS variables for success/warning/error colors
- ActionQueueCard: Full dark mode support with CSS variable-based colors
- ProductionTimelineCard: Dark mode colors + filter for PENDING/ON_HOLD with today's start date
- All skeleton loaders now use theme-aware background colors
- Components adapt automatically to light/dark theme changes
This commit is contained in:
Claude
2025-11-08 07:27:37 +00:00
parent f2cb2448b7
commit 09e21d0967
3 changed files with 229 additions and 117 deletions

View File

@@ -34,22 +34,28 @@ interface ActionQueueCardProps {
const urgencyConfig = {
critical: {
bg: 'bg-red-50',
border: 'border-red-300',
badge: 'bg-red-100 text-red-800',
bgColor: 'var(--color-error-50)',
borderColor: 'var(--color-error-300)',
badgeBgColor: 'var(--color-error-100)',
badgeTextColor: 'var(--color-error-800)',
icon: AlertCircle,
iconColor: 'var(--color-error-700)',
},
important: {
bg: 'bg-amber-50',
border: 'border-amber-300',
badge: 'bg-amber-100 text-amber-800',
bgColor: 'var(--color-warning-50)',
borderColor: 'var(--color-warning-300)',
badgeBgColor: 'var(--color-warning-100)',
badgeTextColor: 'var(--color-warning-900)',
icon: AlertCircle,
iconColor: 'var(--color-warning-700)',
},
normal: {
bg: 'bg-blue-50',
border: 'border-blue-300',
badge: 'bg-blue-100 text-blue-800',
bgColor: 'var(--color-info-50)',
borderColor: 'var(--color-info-300)',
badgeBgColor: 'var(--color-info-100)',
badgeTextColor: 'var(--color-info-800)',
icon: FileText,
iconColor: 'var(--color-info-700)',
},
};
@@ -85,25 +91,35 @@ function ActionItemCard({
return (
<div
className={`${config.bg} ${config.border} border-2 rounded-lg p-4 md:p-5 transition-all duration-200 hover:shadow-md`}
className="border-2 rounded-lg p-4 md:p-5 transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: config.bgColor,
borderColor: config.borderColor,
}}
>
{/* Header */}
<div className="flex items-start gap-3 mb-3">
<UrgencyIcon className={`w-6 h-6 flex-shrink-0 ${config.badge.split(' ')[1]}`} />
<UrgencyIcon className="w-6 h-6 flex-shrink-0" style={{ color: config.iconColor }} />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<h3 className="font-bold text-lg">{action.title || 'Action Required'}</h3>
<span className={`${config.badge} px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0`}>
<h3 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}>{action.title || 'Action Required'}</h3>
<span
className="px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0"
style={{
backgroundColor: config.badgeBgColor,
color: config.badgeTextColor,
}}
>
{action.urgency || 'normal'}
</span>
</div>
<p className="text-sm text-gray-600">{action.subtitle || ''}</p>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{action.subtitle || ''}</p>
</div>
</div>
{/* Amount (for POs) */}
{action.amount && (
<div className="flex items-center gap-2 mb-3 text-lg font-bold">
<div className="flex items-center gap-2 mb-3 text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
<Euro className="w-5 h-5" />
<span>
{action.amount.toFixed(2)} {action.currency}
@@ -112,11 +128,11 @@ function ActionItemCard({
)}
{/* Reasoning (always visible) */}
<div className="bg-white rounded-md p-3 mb-3">
<p className="text-sm font-medium text-gray-700 mb-1">
<div className="rounded-md p-3 mb-3" style={{ backgroundColor: 'var(--bg-primary)' }}>
<p className="text-sm font-medium mb-1" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.action_queue.why_needed')}
</p>
<p className="text-sm text-gray-600">{reasoning}</p>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{reasoning}</p>
</div>
{/* Consequence (expandable) */}
@@ -124,17 +140,24 @@ function ActionItemCard({
<>
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-3 w-full"
className="flex items-center gap-2 text-sm transition-colors mb-3 w-full"
style={{ color: 'var(--text-secondary)' }}
>
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
<span className="font-medium">{t('jtbd.action_queue.what_if_not')}</span>
</button>
{expanded && (
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 mb-3">
<p className="text-sm text-amber-900">{consequence}</p>
<div
className="border rounded-md p-3 mb-3"
style={{
backgroundColor: 'var(--color-warning-50)',
borderColor: 'var(--color-warning-200)',
}}
>
<p className="text-sm" style={{ color: 'var(--color-warning-900)' }}>{consequence}</p>
{severity && (
<span className="text-xs font-semibold text-amber-900 mt-1 block">
<span className="text-xs font-semibold mt-1 block" style={{ color: 'var(--color-warning-900)' }}>
{severity}
</span>
)}
@@ -144,7 +167,7 @@ function ActionItemCard({
)}
{/* Time Estimate */}
<div className="flex items-center gap-2 text-xs text-gray-500 mb-4">
<div className="flex items-center gap-2 text-xs mb-4" style={{ color: 'var(--text-tertiary)' }}>
<Clock className="w-4 h-4" />
<span>
{t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes || 5} min
@@ -200,11 +223,11 @@ export function ActionQueueCard({
if (loading || !actionQueue) {
return (
<div className="bg-white rounded-xl shadow-md p-6">
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="animate-pulse space-y-4">
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
<div className="h-32 bg-gray-200 rounded"></div>
<div className="h-32 bg-gray-200 rounded"></div>
<div className="h-6 rounded w-1/2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-32 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-32 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
</div>
</div>
);
@@ -212,12 +235,18 @@ export function ActionQueueCard({
if (!actionQueue.actions || actionQueue.actions.length === 0) {
return (
<div className="bg-green-50 border-2 border-green-200 rounded-xl p-8 text-center">
<CheckCircle2 className="w-16 h-16 text-green-600 mx-auto mb-4" />
<h3 className="text-xl font-bold text-green-900 mb-2">
<div
className="border-2 rounded-xl p-8 text-center"
style={{
backgroundColor: 'var(--color-success-50)',
borderColor: 'var(--color-success-200)',
}}
>
<CheckCircle2 className="w-16 h-16 mx-auto mb-4" style={{ color: 'var(--color-success-600)' }} />
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--color-success-900)' }}>
{t('jtbd.action_queue.all_caught_up')}
</h3>
<p className="text-green-700">{t('jtbd.action_queue.no_actions')}</p>
<p style={{ color: 'var(--color-success-700)' }}>{t('jtbd.action_queue.no_actions')}</p>
</div>
);
}
@@ -225,12 +254,18 @@ export function ActionQueueCard({
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
return (
<div className="bg-white rounded-xl shadow-md p-6">
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-900">{t('jtbd.action_queue.title')}</h2>
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('jtbd.action_queue.title')}</h2>
{(actionQueue.totalActions || 0) > 3 && (
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
<span
className="px-3 py-1 rounded-full text-sm font-semibold"
style={{
backgroundColor: 'var(--color-error-100)',
color: 'var(--color-error-800)',
}}
>
{actionQueue.totalActions || 0} {t('jtbd.action_queue.total')}
</span>
)}
@@ -240,12 +275,24 @@ export function ActionQueueCard({
{((actionQueue.criticalCount || 0) > 0 || (actionQueue.importantCount || 0) > 0) && (
<div className="flex flex-wrap gap-2 mb-6">
{(actionQueue.criticalCount || 0) > 0 && (
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
<span
className="px-3 py-1 rounded-full text-sm font-semibold"
style={{
backgroundColor: 'var(--color-error-100)',
color: 'var(--color-error-800)',
}}
>
{actionQueue.criticalCount || 0} {t('jtbd.action_queue.critical')}
</span>
)}
{(actionQueue.importantCount || 0) > 0 && (
<span className="bg-amber-100 text-amber-800 px-3 py-1 rounded-full text-sm font-semibold">
<span
className="px-3 py-1 rounded-full text-sm font-semibold"
style={{
backgroundColor: 'var(--color-warning-100)',
color: 'var(--color-warning-800)',
}}
>
{actionQueue.importantCount || 0} {t('jtbd.action_queue.important')}
</span>
)}
@@ -269,7 +316,11 @@ export function ActionQueueCard({
{(actionQueue.totalActions || 0) > 3 && (
<button
onClick={() => setShowAll(!showAll)}
className="w-full mt-4 py-3 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold text-gray-700 transition-colors duration-200"
className="w-full mt-4 py-3 rounded-lg font-semibold transition-colors duration-200"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
}}
>
{showAll
? t('jtbd.action_queue.show_less')

View File

@@ -21,25 +21,25 @@ interface HealthStatusCardProps {
const statusConfig = {
green: {
bg: 'bg-green-50',
border: 'border-green-200',
text: 'text-green-800',
bgColor: 'var(--color-success-50)',
borderColor: 'var(--color-success-200)',
textColor: 'var(--color-success-800)',
icon: CheckCircle,
iconColor: 'text-green-600',
iconColor: 'var(--color-success-600)',
},
yellow: {
bg: 'bg-amber-50',
border: 'border-amber-200',
text: 'text-amber-900',
bgColor: 'var(--color-warning-50)',
borderColor: 'var(--color-warning-200)',
textColor: 'var(--color-warning-900)',
icon: AlertTriangle,
iconColor: 'text-amber-600',
iconColor: 'var(--color-warning-600)',
},
red: {
bg: 'bg-red-50',
border: 'border-red-200',
text: 'text-red-900',
bgColor: 'var(--color-error-50)',
borderColor: 'var(--color-error-200)',
textColor: 'var(--color-error-900)',
icon: AlertCircle,
iconColor: 'text-red-600',
iconColor: 'var(--color-error-600)',
},
};
@@ -54,9 +54,9 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
if (loading || !healthStatus) {
return (
<div className="animate-pulse bg-white rounded-lg shadow-md p-6">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="animate-pulse rounded-lg shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="h-8 rounded w-3/4 mb-4" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-4 rounded w-1/2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
</div>
);
}
@@ -67,20 +67,24 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
return (
<div
className={`${config.bg} ${config.border} border-2 rounded-xl p-6 shadow-lg transition-all duration-300 hover:shadow-xl`}
className="border-2 rounded-xl p-6 shadow-lg transition-all duration-300 hover:shadow-xl"
style={{
backgroundColor: config.bgColor,
borderColor: config.borderColor,
}}
>
{/* Header with Status Icon */}
<div className="flex items-start gap-4 mb-4">
<div className={`${config.iconColor} flex-shrink-0`}>
<StatusIcon className="w-10 h-10 md:w-12 md:h-12" strokeWidth={2} />
<div className="flex-shrink-0">
<StatusIcon className="w-10 h-10 md:w-12 md:h-12" strokeWidth={2} style={{ color: config.iconColor }} />
</div>
<div className="flex-1 min-w-0">
<h2 className={`text-xl md:text-2xl font-bold ${config.text} mb-2`}>
<h2 className="text-xl md:text-2xl font-bold mb-2" style={{ color: config.textColor }}>
{healthStatus.headline || t(`jtbd.health_status.${status}`)}
</h2>
{/* Last Update */}
<div className="flex items-center gap-2 text-sm text-gray-600">
<div className="flex items-center gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
<Clock className="w-4 h-4" />
<span>
{t('jtbd.health_status.last_updated')}:{' '}
@@ -94,7 +98,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
{/* Next Check */}
{healthStatus.nextScheduledRun && (
<div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
<div className="flex items-center gap-2 text-sm mt-1" style={{ color: 'var(--text-secondary)' }}>
<RefreshCw className="w-4 h-4" />
<span>
{t('jtbd.health_status.next_check')}:{' '}
@@ -110,17 +114,20 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="space-y-3 mt-6">
{healthStatus.checklistItems.map((item, index) => {
const ItemIcon = iconMap[item.icon];
const iconColorClass = item.actionRequired ? 'text-amber-600' : 'text-green-600';
const iconColor = item.actionRequired ? 'var(--color-warning-600)' : 'var(--color-success-600)';
const bgColor = item.actionRequired ? 'var(--bg-primary)' : 'rgba(255, 255, 255, 0.5)';
return (
<div
key={index}
className={`flex items-center gap-3 p-3 rounded-lg ${
item.actionRequired ? 'bg-white' : 'bg-white/50'
}`}
className="flex items-center gap-3 p-3 rounded-lg"
style={{ backgroundColor: bgColor }}
>
<ItemIcon className={`w-5 h-5 flex-shrink-0 ${iconColorClass}`} />
<span className={`text-sm md:text-base ${item.actionRequired ? 'font-semibold' : ''}`}>
<ItemIcon className="w-5 h-5 flex-shrink-0" style={{ color: iconColor }} />
<span
className={`text-sm md:text-base ${item.actionRequired ? 'font-semibold' : ''}`}
style={{ color: 'var(--text-primary)' }}
>
{item.text || ''}
</span>
</div>
@@ -131,20 +138,20 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
{/* Summary Footer */}
{(healthStatus.criticalIssues > 0 || healthStatus.pendingActions > 0) && (
<div className="mt-6 pt-4 border-t border-gray-200">
<div className="mt-6 pt-4" style={{ borderTop: '1px solid var(--border-primary)' }}>
<div className="flex flex-wrap gap-4 text-sm">
{healthStatus.criticalIssues > 0 && (
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-600" />
<span className="font-semibold text-red-800">
<AlertCircle className="w-4 h-4" style={{ color: 'var(--color-error-600)' }} />
<span className="font-semibold" style={{ color: 'var(--color-error-800)' }}>
{t('jtbd.health_status.critical_issues', { count: healthStatus.criticalIssues })}
</span>
</div>
)}
{healthStatus.pendingActions > 0 && (
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-amber-600" />
<span className="font-semibold text-amber-800">
<AlertTriangle className="w-4 h-4" style={{ color: 'var(--color-warning-600)' }} />
<span className="font-semibold" style={{ color: 'var(--color-warning-800)' }}>
{t('jtbd.health_status.actions_needed', { count: healthStatus.pendingActions })}
</span>
</div>

View File

@@ -21,10 +21,10 @@ interface ProductionTimelineCardProps {
}
const priorityColors = {
URGENT: 'text-red-600',
HIGH: 'text-orange-600',
MEDIUM: 'text-blue-600',
LOW: 'text-gray-600',
URGENT: 'var(--color-error-600)',
HIGH: 'var(--color-warning-600)',
MEDIUM: 'var(--color-info-600)',
LOW: 'var(--text-tertiary)',
};
function TimelineItemCard({
@@ -36,7 +36,7 @@ function TimelineItemCard({
onStart?: (id: string) => void;
onPause?: (id: string) => void;
}) {
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'text-gray-600';
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'var(--text-tertiary)';
const { formatBatchAction } = useReasoningFormatter();
const { t } = useTranslation('reasoning');
@@ -66,11 +66,17 @@ function TimelineItemCard({
: 'N/A';
return (
<div className="flex gap-4 p-4 bg-white rounded-lg border border-gray-200 hover:shadow-md transition-shadow duration-200">
<div
className="flex gap-4 p-4 rounded-lg border hover:shadow-md transition-shadow duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border-primary)',
}}
>
{/* Timeline icon and connector */}
<div className="flex flex-col items-center flex-shrink-0">
<div className="text-2xl">{item.statusIcon || '🔵'}</div>
<div className="text-xs text-gray-500 font-mono mt-1">{startTime}</div>
<div className="text-xs font-mono mt-1" style={{ color: 'var(--text-tertiary)' }}>{startTime}</div>
</div>
{/* Content */}
@@ -78,12 +84,12 @@ function TimelineItemCard({
{/* Header */}
<div className="flex items-start justify-between gap-2 mb-2">
<div>
<h3 className="font-bold text-lg text-gray-900">{item.productName || 'Product'}</h3>
<p className="text-sm text-gray-600">
<h3 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}>{item.productName || 'Product'}</h3>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{item.quantity || 0} {item.unit || 'units'} Batch #{item.batchNumber || 'N/A'}
</p>
</div>
<span className={`text-xs font-semibold uppercase ${priorityColor}`}>
<span className="text-xs font-semibold uppercase" style={{ color: priorityColor }}>
{item.priority || 'NORMAL'}
</span>
</div>
@@ -91,18 +97,18 @@ function TimelineItemCard({
{/* Status and Progress */}
<div className="mb-3">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-gray-700">{item.statusText || 'Status'}</span>
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{item.statusText || 'Status'}</span>
{item.status === 'IN_PROGRESS' && (
<span className="text-sm text-gray-600">{item.progress || 0}%</span>
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>{item.progress || 0}%</span>
)}
</div>
{/* Progress Bar */}
{item.status === 'IN_PROGRESS' && (
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="w-full rounded-full h-2" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${item.progress || 0}%` }}
className="h-2 rounded-full transition-all duration-500"
style={{ width: `${item.progress || 0}%`, backgroundColor: 'var(--color-info-600)' }}
/>
</div>
)}
@@ -110,7 +116,7 @@ function TimelineItemCard({
{/* Ready By Time */}
{item.status !== 'COMPLETED' && (
<div className="flex items-center gap-2 text-sm text-gray-600 mb-2">
<div className="flex items-center gap-2 text-sm mb-2" style={{ color: 'var(--text-secondary)' }}>
<Clock className="w-4 h-4" />
<span>
{t('jtbd.production_timeline.ready_by')}: {readyByTime}
@@ -120,14 +126,18 @@ function TimelineItemCard({
{/* Reasoning */}
{reasoning && (
<p className="text-sm text-gray-600 italic mb-3">"{reasoning}"</p>
<p className="text-sm italic mb-3" style={{ color: 'var(--text-secondary)' }}>"{reasoning}"</p>
)}
{/* Actions */}
{item.status === 'PENDING' && onStart && (
<button
onClick={() => onStart(item.id)}
className="flex items-center gap-2 px-3 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-semibold transition-colors duration-200"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-semibold transition-colors duration-200"
style={{
backgroundColor: 'var(--color-success-600)',
color: 'var(--text-inverse)',
}}
>
<Play className="w-4 h-4" />
{t('jtbd.production_timeline.start_batch')}
@@ -137,7 +147,11 @@ function TimelineItemCard({
{item.status === 'IN_PROGRESS' && onPause && (
<button
onClick={() => onPause(item.id)}
className="flex items-center gap-2 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg text-sm font-semibold transition-colors duration-200"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-semibold transition-colors duration-200"
style={{
backgroundColor: 'var(--color-warning-600)',
color: 'var(--text-inverse)',
}}
>
<Pause className="w-4 h-4" />
{t('jtbd.production_timeline.pause_batch')}
@@ -145,7 +159,7 @@ function TimelineItemCard({
)}
{item.status === 'COMPLETED' && (
<div className="flex items-center gap-2 text-sm text-green-600 font-medium">
<div className="flex items-center gap-2 text-sm font-medium" style={{ color: 'var(--color-success-600)' }}>
<CheckCircle2 className="w-4 h-4" />
{t('jtbd.production_timeline.completed')}
</div>
@@ -163,65 +177,99 @@ export function ProductionTimelineCard({
}: ProductionTimelineCardProps) {
const { t } = useTranslation('reasoning');
if (loading || !timeline) {
// Filter for today's PENDING/ON_HOLD batches only
const filteredTimeline = useMemo(() => {
if (!timeline?.timeline) return null;
const today = new Date();
today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const filteredItems = timeline.timeline.filter(item => {
// Only show PENDING or ON_HOLD status
if (item.status !== 'PENDING' && item.status !== 'ON_HOLD') {
return false;
}
// Check if plannedStartTime is today
if (item.plannedStartTime) {
const startTime = new Date(item.plannedStartTime);
return startTime >= today && startTime < tomorrow;
}
return false;
});
return {
...timeline,
timeline: filteredItems,
totalBatches: filteredItems.length,
pendingBatches: filteredItems.filter(item => item.status === 'PENDING').length,
inProgressBatches: 0, // Filtered out
completedBatches: 0, // Filtered out
};
}, [timeline]);
if (loading || !filteredTimeline) {
return (
<div className="bg-white rounded-xl shadow-md p-6">
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="animate-pulse space-y-4">
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
<div className="h-6 rounded w-1/2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="space-y-3">
<div className="h-24 bg-gray-200 rounded"></div>
<div className="h-24 bg-gray-200 rounded"></div>
<div className="h-24 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-24 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
</div>
</div>
</div>
);
}
if (!timeline.timeline || timeline.timeline.length === 0) {
if (!filteredTimeline.timeline || filteredTimeline.timeline.length === 0) {
return (
<div className="bg-white rounded-xl shadow-md p-8 text-center">
<Factory className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">
<div className="rounded-xl shadow-md p-8 text-center" style={{ backgroundColor: 'var(--bg-primary)' }}>
<Factory className="w-16 h-16 mx-auto mb-4" style={{ color: 'var(--text-tertiary)' }} />
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.production_timeline.no_production')}
</h3>
<p className="text-gray-600">{t('jtbd.production_timeline.no_batches')}</p>
<p style={{ color: 'var(--text-secondary)' }}>{t('jtbd.production_timeline.no_batches')}</p>
</div>
);
}
return (
<div className="bg-white rounded-xl shadow-md p-6">
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Factory className="w-8 h-8 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-900">{t('jtbd.production_timeline.title')}</h2>
<Factory className="w-8 h-8" style={{ color: 'var(--color-info-600)' }} />
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('jtbd.production_timeline.title')}</h2>
</div>
</div>
{/* Summary Stats */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-900">{timeline.totalBatches}</div>
<div className="text-xs text-gray-600 uppercase">{t('jtbd.production_timeline.total')}</div>
<div className="rounded-lg p-3 text-center" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{filteredTimeline.totalBatches}</div>
<div className="text-xs uppercase" style={{ color: 'var(--text-secondary)' }}>{t('jtbd.production_timeline.total')}</div>
</div>
<div className="bg-green-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-green-600">{timeline.completedBatches}</div>
<div className="text-xs text-green-700 uppercase">{t('jtbd.production_timeline.done')}</div>
<div className="rounded-lg p-3 text-center" style={{ backgroundColor: 'var(--color-success-50)' }}>
<div className="text-2xl font-bold" style={{ color: 'var(--color-success-600)' }}>{filteredTimeline.completedBatches}</div>
<div className="text-xs uppercase" style={{ color: 'var(--color-success-700)' }}>{t('jtbd.production_timeline.done')}</div>
</div>
<div className="bg-blue-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-blue-600">{timeline.inProgressBatches}</div>
<div className="text-xs text-blue-700 uppercase">{t('jtbd.production_timeline.active')}</div>
<div className="rounded-lg p-3 text-center" style={{ backgroundColor: 'var(--color-info-50)' }}>
<div className="text-2xl font-bold" style={{ color: 'var(--color-info-600)' }}>{filteredTimeline.inProgressBatches}</div>
<div className="text-xs uppercase" style={{ color: 'var(--color-info-700)' }}>{t('jtbd.production_timeline.active')}</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-600">{timeline.pendingBatches}</div>
<div className="text-xs text-gray-600 uppercase">{t('jtbd.production_timeline.pending')}</div>
<div className="rounded-lg p-3 text-center" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<div className="text-2xl font-bold" style={{ color: 'var(--text-secondary)' }}>{filteredTimeline.pendingBatches}</div>
<div className="text-xs uppercase" style={{ color: 'var(--text-secondary)' }}>{t('jtbd.production_timeline.pending')}</div>
</div>
</div>
{/* Timeline */}
<div className="space-y-4">
{timeline.timeline.map((item) => (
{filteredTimeline.timeline.map((item) => (
<TimelineItemCard
key={item.id}
item={item}
@@ -232,8 +280,14 @@ export function ProductionTimelineCard({
</div>
{/* View Full Schedule Link */}
{timeline.totalBatches > 5 && (
<button className="w-full mt-6 py-3 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold text-gray-700 transition-colors duration-200">
{filteredTimeline.totalBatches > 5 && (
<button
className="w-full mt-6 py-3 rounded-lg font-semibold transition-colors duration-200"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
}}
>
{t('jtbd.production_timeline.view_full_schedule')}
</button>
)}