fix: Add comprehensive null guards to all dashboard components
This commit fixes React Error #306 (text content mismatch) by ensuring no component ever tries to render undefined values as text content. Changes made across all dashboard components: 1. **Component Entry Guards**: - Added early returns for null/undefined data - Changed `if (loading)` to `if (loading || !data)` pattern - Components now show loading skeleton when data is undefined 2. **Property Access Guards**: - Added `|| fallback` operators for all rendered text - Added optional chaining for nested property access - Added conditional rendering for optional/array fields 3. **Specific Component Fixes**: **HealthStatusCard.tsx**: - Added early return for !healthStatus - Added fallback for status: `healthStatus.status || 'green'` - Added conditional rendering for nextScheduledRun - Added conditional rendering for checklistItems array - Added text fallback: `item.text || ''` **InsightsGrid.tsx**: - Added null check: `if (!insights || !insights.savings || ...)` - Added fallbacks: `insights.savings?.label || 'Savings'` - Added fallbacks for all 4 insight cards **OrchestrationSummaryCard.tsx**: - Added early return for !summary - Added fallback: `summary.message || ''` - Added fallback: `summary.runNumber || 0` - Added array checks: `summary.purchaseOrdersSummary && ...` - Added fallbacks for PO items: `po.supplierName || 'Unknown Supplier'` - Added fallbacks for batch items: `batch.quantity || 0` - Added safe date parsing: `batch.readyByTime ? new Date(...) : 'TBD'` **ProductionTimelineCard.tsx**: - Added early return for !timeline - Added array check: `!timeline.timeline || timeline.timeline.length === 0` - Added fallbacks for all item properties: - `item.statusIcon || '🔵'` - `item.productName || 'Product'` - `item.quantity || 0` - `item.unit || 'units'` - `item.batchNumber || 'N/A'` - `item.priority || 'NORMAL'` - `item.statusText || 'Status'` - `item.progress || 0` **ActionQueueCard.tsx**: - Added early return for !actionQueue - Added safe config lookup with fallback to normal urgency - Added array check: `!actionQueue.actions || ...` - Added property fallbacks: - `action.title || 'Action Required'` - `action.urgency || 'normal'` - `action.subtitle || ''` - `action.estimatedTimeMinutes || 5` - Added array guard: `(action.actions || []).map(...)` - Added fallbacks for counts: `actionQueue.totalActions || 0` **useReasoningTranslation.ts**: - Enhanced formatPOAction with explicit fallbacks - Enhanced formatBatchAction with explicit fallbacks - All translation functions return strings with || operators **DashboardPage.tsx**: - Removed conditional rendering operators (&&) - Components now always render and handle their own null states - Pattern: `<Component data={data} loading={loading} />` 4. **Testing Strategy**: - All components gracefully handle undefined data - Loading states shown when data is undefined - No undefined values can be rendered as text content - Multiple fallback layers ensure strings are always returned This comprehensive fix addresses the root cause of React Error #306 by making all components bulletproof against undefined values.
This commit is contained in:
@@ -65,7 +65,7 @@ function ActionItemCard({
|
|||||||
onModify?: (id: string) => void;
|
onModify?: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const config = urgencyConfig[action.urgency];
|
const config = urgencyConfig[action.urgency as keyof typeof urgencyConfig] || urgencyConfig.normal;
|
||||||
const UrgencyIcon = config.icon;
|
const UrgencyIcon = config.icon;
|
||||||
const { formatPOAction } = useReasoningFormatter();
|
const { formatPOAction } = useReasoningFormatter();
|
||||||
const { t } = useTranslation('reasoning');
|
const { t } = useTranslation('reasoning');
|
||||||
@@ -84,12 +84,12 @@ function ActionItemCard({
|
|||||||
<UrgencyIcon className={`w-6 h-6 flex-shrink-0 ${config.badge.split(' ')[1]}`} />
|
<UrgencyIcon className={`w-6 h-6 flex-shrink-0 ${config.badge.split(' ')[1]}`} />
|
||||||
<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}</h3>
|
<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`}>
|
<span className={`${config.badge} px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0`}>
|
||||||
{action.urgency}
|
{action.urgency || 'normal'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{action.subtitle}</p>
|
<p className="text-sm text-gray-600">{action.subtitle || ''}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -139,13 +139,13 @@ function ActionItemCard({
|
|||||||
<div className="flex items-center gap-2 text-xs text-gray-500 mb-4">
|
<div className="flex items-center gap-2 text-xs text-gray-500 mb-4">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
<span>
|
<span>
|
||||||
{t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes} min
|
{t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes || 5} min
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{action.actions.map((button, index) => {
|
{(action.actions || []).map((button, index) => {
|
||||||
const buttonStyles = {
|
const buttonStyles = {
|
||||||
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
|
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
|
||||||
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
|
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
|
||||||
@@ -188,10 +188,9 @@ export function ActionQueueCard({
|
|||||||
onModify,
|
onModify,
|
||||||
}: ActionQueueCardProps) {
|
}: ActionQueueCardProps) {
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
|
|
||||||
const { t } = useTranslation('reasoning');
|
const { t } = useTranslation('reasoning');
|
||||||
|
|
||||||
if (loading) {
|
if (loading || !actionQueue) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-md p-6">
|
<div className="bg-white rounded-xl shadow-md p-6">
|
||||||
<div className="animate-pulse space-y-4">
|
<div className="animate-pulse space-y-4">
|
||||||
@@ -203,7 +202,7 @@ export function ActionQueueCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (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 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" />
|
<CheckCircle2 className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
||||||
@@ -215,29 +214,31 @@ export function ActionQueueCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-md p-6">
|
<div className="bg-white rounded-xl shadow-md p-6">
|
||||||
{/* 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 text-gray-900">{t('jtbd.action_queue.title')}</h2>
|
||||||
{actionQueue.totalActions > 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="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||||
{actionQueue.totalActions} {t('jtbd.action_queue.total')}
|
{actionQueue.totalActions || 0} {t('jtbd.action_queue.total')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Badges */}
|
{/* Summary Badges */}
|
||||||
{(actionQueue.criticalCount > 0 || actionQueue.importantCount > 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 && (
|
{(actionQueue.criticalCount || 0) > 0 && (
|
||||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||||
{actionQueue.criticalCount} {t('jtbd.action_queue.critical')}
|
{actionQueue.criticalCount || 0} {t('jtbd.action_queue.critical')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{actionQueue.importantCount > 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="bg-amber-100 text-amber-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||||
{actionQueue.importantCount} {t('jtbd.action_queue.important')}
|
{actionQueue.importantCount || 0} {t('jtbd.action_queue.important')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -257,14 +258,14 @@ export function ActionQueueCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show More/Less */}
|
{/* Show More/Less */}
|
||||||
{actionQueue.totalActions > 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 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold text-gray-700 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
{showAll
|
{showAll
|
||||||
? t('jtbd.action_queue.show_less')
|
? t('jtbd.action_queue.show_less')
|
||||||
: t('jtbd.action_queue.show_more', { count: actionQueue.totalActions - 3 })}
|
: t('jtbd.action_queue.show_more', { count: (actionQueue.totalActions || 3) - 3 })}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,11 +50,9 @@ const iconMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) {
|
export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) {
|
||||||
const config = statusConfig[healthStatus.status];
|
|
||||||
const StatusIcon = config.icon;
|
|
||||||
const { t } = useTranslation('reasoning');
|
const { t } = useTranslation('reasoning');
|
||||||
|
|
||||||
if (loading) {
|
if (loading || !healthStatus) {
|
||||||
return (
|
return (
|
||||||
<div className="animate-pulse bg-white rounded-lg shadow-md p-6">
|
<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-8 bg-gray-200 rounded w-3/4 mb-4"></div>
|
||||||
@@ -63,6 +61,10 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const status = healthStatus.status || 'green';
|
||||||
|
const config = statusConfig[status];
|
||||||
|
const StatusIcon = config.icon;
|
||||||
|
|
||||||
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={`${config.bg} ${config.border} border-2 rounded-xl p-6 shadow-lg transition-all duration-300 hover:shadow-xl`}
|
||||||
@@ -74,7 +76,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
|||||||
</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 ${config.text} mb-2`}>
|
||||||
{healthStatus.headline}
|
{healthStatus.headline || t(`jtbd.health_status.${status}`)}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Last Update */}
|
{/* Last Update */}
|
||||||
@@ -91,19 +93,22 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Next Check */}
|
{/* Next Check */}
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
|
{healthStatus.nextScheduledRun && (
|
||||||
<RefreshCw className="w-4 h-4" />
|
<div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
|
||||||
<span>
|
<RefreshCw className="w-4 h-4" />
|
||||||
{t('jtbd.health_status.next_check')}:{' '}
|
<span>
|
||||||
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true })}
|
{t('jtbd.health_status.next_check')}:{' '}
|
||||||
</span>
|
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true })}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Checklist */}
|
{/* Status Checklist */}
|
||||||
<div className="space-y-3 mt-6">
|
{healthStatus.checklistItems && healthStatus.checklistItems.length > 0 && (
|
||||||
{healthStatus.checklistItems.map((item, index) => {
|
<div className="space-y-3 mt-6">
|
||||||
|
{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 iconColorClass = item.actionRequired ? 'text-amber-600' : 'text-green-600';
|
||||||
|
|
||||||
@@ -116,12 +121,13 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
|
|||||||
>
|
>
|
||||||
<ItemIcon className={`w-5 h-5 flex-shrink-0 ${iconColorClass}`} />
|
<ItemIcon className={`w-5 h-5 flex-shrink-0 ${iconColorClass}`} />
|
||||||
<span className={`text-sm md:text-base ${item.actionRequired ? 'font-semibold' : ''}`}>
|
<span className={`text-sm md:text-base ${item.actionRequired ? 'font-semibold' : ''}`}>
|
||||||
{item.text}
|
{item.text || ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Summary Footer */}
|
{/* Summary Footer */}
|
||||||
{(healthStatus.criticalIssues > 0 || healthStatus.pendingActions > 0) && (
|
{(healthStatus.criticalIssues > 0 || healthStatus.pendingActions > 0) && (
|
||||||
|
|||||||
@@ -81,31 +81,36 @@ export function InsightsGrid({ insights, loading }: InsightsGridProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard against undefined values
|
||||||
|
if (!insights || !insights.savings || !insights.inventory || !insights.waste || !insights.deliveries) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<InsightCard
|
<InsightCard
|
||||||
label={insights.savings.label}
|
label={insights.savings?.label || 'Savings'}
|
||||||
value={insights.savings.value}
|
value={insights.savings?.value || '€0'}
|
||||||
detail={insights.savings.detail}
|
detail={insights.savings?.detail || 'This week'}
|
||||||
color={insights.savings.color}
|
color={insights.savings?.color || 'green'}
|
||||||
/>
|
/>
|
||||||
<InsightCard
|
<InsightCard
|
||||||
label={insights.inventory.label}
|
label={insights.inventory?.label || 'Inventory'}
|
||||||
value={insights.inventory.value}
|
value={insights.inventory?.value || '0%'}
|
||||||
detail={insights.inventory.detail}
|
detail={insights.inventory?.detail || 'Status'}
|
||||||
color={insights.inventory.color}
|
color={insights.inventory?.color || 'green'}
|
||||||
/>
|
/>
|
||||||
<InsightCard
|
<InsightCard
|
||||||
label={insights.waste.label}
|
label={insights.waste?.label || 'Waste'}
|
||||||
value={insights.waste.value}
|
value={insights.waste?.value || '0%'}
|
||||||
detail={insights.waste.detail}
|
detail={insights.waste?.detail || 'Reduction'}
|
||||||
color={insights.waste.color}
|
color={insights.waste?.color || 'green'}
|
||||||
/>
|
/>
|
||||||
<InsightCard
|
<InsightCard
|
||||||
label={insights.deliveries.label}
|
label={insights.deliveries?.label || 'Deliveries'}
|
||||||
value={insights.deliveries.value}
|
value={insights.deliveries?.value || '0%'}
|
||||||
detail={insights.deliveries.detail}
|
detail={insights.deliveries?.detail || 'On-time'}
|
||||||
color={insights.deliveries.color}
|
color={insights.deliveries?.color || 'green'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
|||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const { t } = useTranslation('reasoning');
|
const { t } = useTranslation('reasoning');
|
||||||
|
|
||||||
if (loading) {
|
if (loading || !summary) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-md p-6">
|
<div className="bg-white rounded-xl shadow-md p-6">
|
||||||
<div className="animate-pulse space-y-4">
|
<div className="animate-pulse space-y-4">
|
||||||
@@ -62,7 +62,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
|||||||
<h3 className="text-lg font-bold text-blue-900 mb-2">
|
<h3 className="text-lg font-bold text-blue-900 mb-2">
|
||||||
{t('jtbd.orchestration_summary.ready_to_plan')}
|
{t('jtbd.orchestration_summary.ready_to_plan')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-blue-700 mb-4">{summary.message}</p>
|
<p className="text-blue-700 mb-4">{summary.message || ''}</p>
|
||||||
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold transition-colors duration-200">
|
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold transition-colors duration-200">
|
||||||
{t('jtbd.orchestration_summary.run_planning')}
|
{t('jtbd.orchestration_summary.run_planning')}
|
||||||
</button>
|
</button>
|
||||||
@@ -90,7 +90,7 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
|||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
<span>
|
<span>
|
||||||
{t('jtbd.orchestration_summary.run_info', { runNumber: summary.runNumber })} • {runTime}
|
{t('jtbd.orchestration_summary.run_info', { runNumber: summary.runNumber || 0 })} • {runTime}
|
||||||
</span>
|
</span>
|
||||||
{summary.durationSeconds && (
|
{summary.durationSeconds && (
|
||||||
<span className="text-gray-400">
|
<span className="text-gray-400">
|
||||||
@@ -111,16 +111,16 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary.purchaseOrdersSummary.length > 0 && (
|
{summary.purchaseOrdersSummary && summary.purchaseOrdersSummary.length > 0 && (
|
||||||
<ul className="space-y-2 ml-8">
|
<ul className="space-y-2 ml-8">
|
||||||
{summary.purchaseOrdersSummary.map((po, index) => (
|
{summary.purchaseOrdersSummary.map((po, index) => (
|
||||||
<li key={index} className="text-sm text-gray-700">
|
<li key={index} className="text-sm text-gray-700">
|
||||||
<span className="font-medium">{po.supplierName}</span>
|
<span className="font-medium">{po.supplierName || 'Unknown Supplier'}</span>
|
||||||
{' • '}
|
{' • '}
|
||||||
{po.itemCategories.slice(0, 2).join(', ')}
|
{(po.itemCategories || []).slice(0, 2).join(', ') || 'Items'}
|
||||||
{po.itemCategories.length > 2 && ` +${po.itemCategories.length - 2} more`}
|
{(po.itemCategories || []).length > 2 && ` +${po.itemCategories.length - 2} more`}
|
||||||
{' • '}
|
{' • '}
|
||||||
<span className="font-semibold">€{po.totalAmount.toFixed(2)}</span>
|
<span className="font-semibold">€{(po.totalAmount || 0).toFixed(2)}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -140,25 +140,25 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary.productionBatchesSummary.length > 0 && (
|
{summary.productionBatchesSummary && summary.productionBatchesSummary.length > 0 && (
|
||||||
<ul className="space-y-2 ml-8">
|
<ul className="space-y-2 ml-8">
|
||||||
{summary.productionBatchesSummary.slice(0, expanded ? undefined : 3).map((batch, index) => (
|
{summary.productionBatchesSummary.slice(0, expanded ? undefined : 3).map((batch, index) => (
|
||||||
<li key={index} className="text-sm text-gray-700">
|
<li key={index} className="text-sm text-gray-700">
|
||||||
<span className="font-semibold">{batch.quantity}</span> {batch.productName}
|
<span className="font-semibold">{batch.quantity || 0}</span> {batch.productName || 'Product'}
|
||||||
{' • '}
|
{' • '}
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">
|
||||||
ready by {new Date(batch.readyByTime).toLocaleTimeString('en-US', {
|
ready by {batch.readyByTime ? new Date(batch.readyByTime).toLocaleTimeString('en-US', {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: true,
|
hour12: true,
|
||||||
})}
|
}) : 'TBD'}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{summary.productionBatchesSummary.length > 3 && (
|
{summary.productionBatchesSummary && summary.productionBatchesSummary.length > 3 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
className="ml-8 mt-2 flex items-center gap-1 text-sm text-purple-600 hover:text-purple-800 font-medium"
|
className="ml-8 mt-2 flex items-center gap-1 text-sm text-purple-600 hover:text-purple-800 font-medium"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function TimelineItemCard({
|
|||||||
<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 bg-white rounded-lg border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
||||||
{/* 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 text-gray-500 font-mono mt-1">{startTime}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -74,22 +74,22 @@ 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}</h3>
|
<h3 className="font-bold text-lg text-gray-900">{item.productName || 'Product'}</h3>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{item.quantity} {item.unit} • Batch #{item.batchNumber}
|
{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 ${priorityColor}`}>
|
||||||
{item.priority}
|
{item.priority || 'NORMAL'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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}</span>
|
<span className="text-sm font-medium text-gray-700">{item.statusText || 'Status'}</span>
|
||||||
{item.status === 'IN_PROGRESS' && (
|
{item.status === 'IN_PROGRESS' && (
|
||||||
<span className="text-sm text-gray-600">{item.progress}%</span>
|
<span className="text-sm text-gray-600">{item.progress || 0}%</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ function TimelineItemCard({
|
|||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-500"
|
className="bg-blue-600 h-2 rounded-full transition-all duration-500"
|
||||||
style={{ width: `${item.progress}%` }}
|
style={{ width: `${item.progress || 0}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -159,7 +159,7 @@ export function ProductionTimelineCard({
|
|||||||
}: ProductionTimelineCardProps) {
|
}: ProductionTimelineCardProps) {
|
||||||
const { t } = useTranslation('reasoning');
|
const { t } = useTranslation('reasoning');
|
||||||
|
|
||||||
if (loading) {
|
if (loading || !timeline) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-md p-6">
|
<div className="bg-white rounded-xl shadow-md p-6">
|
||||||
<div className="animate-pulse space-y-4">
|
<div className="animate-pulse space-y-4">
|
||||||
@@ -173,7 +173,7 @@ export function ProductionTimelineCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeline.timeline.length === 0) {
|
if (!timeline.timeline || timeline.timeline.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-md p-8 text-center">
|
<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" />
|
<Factory className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
|||||||
@@ -138,30 +138,37 @@ export function useReasoningFormatter() {
|
|||||||
const formatPOAction = (reasoningData?: ReasoningData) => {
|
const formatPOAction = (reasoningData?: ReasoningData) => {
|
||||||
if (!reasoningData) {
|
if (!reasoningData) {
|
||||||
return {
|
return {
|
||||||
reasoning: translation.translatePOReasonng({} as ReasoningData),
|
reasoning: translation.translatePOReasonng({} as ReasoningData) || 'Purchase order required',
|
||||||
consequence: '',
|
consequence: '',
|
||||||
severity: ''
|
severity: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reasoning = translation.translatePOReasonng(reasoningData) || 'Purchase order required';
|
||||||
|
const consequence = translation.translateConsequence(reasoningData.consequence) || '';
|
||||||
|
const severity = translation.translateSeverity(reasoningData.consequence?.severity) || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reasoning: translation.translatePOReasonng(reasoningData),
|
reasoning,
|
||||||
consequence: translation.translateConsequence(reasoningData.consequence),
|
consequence,
|
||||||
severity: translation.translateSeverity(reasoningData.consequence?.severity)
|
severity
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatBatchAction = (reasoningData?: ReasoningData) => {
|
const formatBatchAction = (reasoningData?: ReasoningData) => {
|
||||||
if (!reasoningData) {
|
if (!reasoningData) {
|
||||||
return {
|
return {
|
||||||
reasoning: translation.translateBatchReasoning({} as ReasoningData),
|
reasoning: translation.translateBatchReasoning({} as ReasoningData) || 'Production batch scheduled',
|
||||||
urgency: ''
|
urgency: 'normal'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reasoning = translation.translateBatchReasoning(reasoningData) || 'Production batch scheduled';
|
||||||
|
const urgency = reasoningData.urgency?.level || 'normal';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reasoning: translation.translateBatchReasoning(reasoningData),
|
reasoning,
|
||||||
urgency: reasoningData.urgency?.level || 'normal'
|
urgency
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -147,43 +147,35 @@ export function NewDashboardPage() {
|
|||||||
{/* Main Dashboard Layout */}
|
{/* Main Dashboard Layout */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* SECTION 1: Bakery Health Status */}
|
{/* SECTION 1: Bakery Health Status */}
|
||||||
{healthStatus && (
|
<HealthStatusCard healthStatus={healthStatus} loading={healthLoading} />
|
||||||
<HealthStatusCard healthStatus={healthStatus} loading={healthLoading} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* SECTION 2: What Needs Your Attention (Action Queue) */}
|
{/* SECTION 2: What Needs Your Attention (Action Queue) */}
|
||||||
{actionQueue && (
|
<ActionQueueCard
|
||||||
<ActionQueueCard
|
actionQueue={actionQueue}
|
||||||
actionQueue={actionQueue}
|
loading={actionQueueLoading}
|
||||||
loading={actionQueueLoading}
|
onApprove={handleApprove}
|
||||||
onApprove={handleApprove}
|
onViewDetails={handleViewDetails}
|
||||||
onViewDetails={handleViewDetails}
|
onModify={handleModify}
|
||||||
onModify={handleModify}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* SECTION 3: What the System Did for You (Orchestration Summary) */}
|
{/* SECTION 3: What the System Did for You (Orchestration Summary) */}
|
||||||
{orchestrationSummary && (
|
<OrchestrationSummaryCard
|
||||||
<OrchestrationSummaryCard
|
summary={orchestrationSummary}
|
||||||
summary={orchestrationSummary}
|
loading={orchestrationLoading}
|
||||||
loading={orchestrationLoading}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* SECTION 4: Today's Production Timeline */}
|
{/* SECTION 4: Today's Production Timeline */}
|
||||||
{productionTimeline && (
|
<ProductionTimelineCard
|
||||||
<ProductionTimelineCard
|
timeline={productionTimeline}
|
||||||
timeline={productionTimeline}
|
loading={timelineLoading}
|
||||||
loading={timelineLoading}
|
onStart={handleStartBatch}
|
||||||
onStart={handleStartBatch}
|
onPause={handlePauseBatch}
|
||||||
onPause={handlePauseBatch}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* SECTION 5: Quick Insights Grid */}
|
{/* SECTION 5: Quick Insights Grid */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Key Metrics</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">Key Metrics</h2>
|
||||||
{insights && <InsightsGrid insights={insights} loading={insightsLoading} />}
|
<InsightsGrid insights={insights} loading={insightsLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SECTION 6: Quick Action Links */}
|
{/* SECTION 6: Quick Action Links */}
|
||||||
|
|||||||
Reference in New Issue
Block a user