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

View File

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

View File

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