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:
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user