demo seed change

This commit is contained in:
Urtzi Alfaro
2025-12-13 23:57:54 +01:00
parent f3688dfb04
commit ff830a3415
299 changed files with 20328 additions and 19485 deletions

View File

@@ -0,0 +1,181 @@
/**
* AIInsightsBlock - AI Insights Dashboard Block
*
* Displays AI-generated insights for professional/enterprise tiers
* Shows top 2-3 insights with links to full AI Insights page
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Lightbulb, ArrowRight, BarChart2, TrendingUp, TrendingDown, Shield, AlertTriangle } from 'lucide-react';
interface AIInsight {
id: string;
title: string;
description: string;
type: 'cost_optimization' | 'waste_reduction' | 'safety_stock' | 'demand_forecast' | 'risk_alert';
impact: 'high' | 'medium' | 'low';
impact_value?: string;
impact_currency?: string;
created_at: string;
}
interface AIInsightsBlockProps {
insights: AIInsight[];
loading?: boolean;
onViewAll: () => void;
}
export function AIInsightsBlock({ insights = [], loading = false, onViewAll }: AIInsightsBlockProps) {
const { t } = useTranslation(['dashboard', 'common']);
// Get icon based on insight type
const getInsightIcon = (type: string) => {
switch (type) {
case 'cost_optimization': return <TrendingUp className="w-5 h-5 text-[var(--color-success-600)]" />;
case 'waste_reduction': return <TrendingDown className="w-5 h-5 text-[var(--color-success-600)]" />;
case 'safety_stock': return <Shield className="w-5 h-5 text-[var(--color-info-600)]" />;
case 'demand_forecast': return <BarChart2 className="w-5 h-5 text-[var(--color-primary-600)]" />;
case 'risk_alert': return <AlertTriangle className="w-5 h-5 text-[var(--color-error-600)]" />;
default: return <Lightbulb className="w-5 h-5 text-[var(--color-primary-600)]" />;
}
};
// Get impact color based on level
const getImpactColor = (impact: string) => {
switch (impact) {
case 'high': return 'bg-[var(--color-error-100)] text-[var(--color-error-700)]';
case 'medium': return 'bg-[var(--color-warning-100)] text-[var(--color-warning-700)]';
case 'low': return 'bg-[var(--color-info-100)] text-[var(--color-info-700)]';
default: return 'bg-[var(--bg-secondary)] text-[var(--text-secondary)]';
}
};
// Get impact label
const getImpactLabel = (impact: string) => {
switch (impact) {
case 'high': return t('dashboard:ai_insights.impact_high');
case 'medium': return t('dashboard:ai_insights.impact_medium');
case 'low': return t('dashboard:ai_insights.impact_low');
default: return '';
}
};
if (loading) {
return (
<div className="rounded-xl shadow-lg p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)] animate-pulse">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-[var(--bg-secondary)] rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-5 bg-[var(--bg-secondary)] rounded w-1/3"></div>
<div className="h-4 bg-[var(--bg-secondary)] rounded w-1/4"></div>
</div>
</div>
<div className="space-y-4">
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
<div className="h-16 bg-[var(--bg-secondary)] rounded"></div>
</div>
</div>
);
}
// Show top 3 insights
const topInsights = insights.slice(0, 3);
return (
<div className="rounded-xl shadow-lg border border-[var(--border-primary)] bg-[var(--bg-primary)] overflow-hidden">
{/* Header */}
<div className="p-6 pb-4">
<div className="flex items-center gap-4">
{/* Icon */}
<div className="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 bg-[var(--color-primary-100)]">
<Lightbulb className="w-6 h-6 text-[var(--color-primary-600)]" />
</div>
{/* Title & Description */}
<div className="flex-1">
<h2 className="text-xl font-bold text-[var(--text-primary)]">
{t('dashboard:ai_insights.title')}
</h2>
<p className="text-sm text-[var(--text-secondary)]">
{t('dashboard:ai_insights.subtitle')}
</p>
</div>
{/* View All Button */}
<button
onClick={onViewAll}
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[var(--border-primary)] text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] transition-colors text-sm font-medium"
>
<span>{t('dashboard:ai_insights.view_all')}</span>
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
{/* Insights List */}
{topInsights.length > 0 ? (
<div className="border-t border-[var(--border-primary)]">
{topInsights.map((insight, index) => (
<div
key={insight.id || index}
className={`p-4 ${index < topInsights.length - 1 ? 'border-b border-[var(--border-primary)]' : ''}`}
>
<div className="flex items-start gap-3">
{/* Icon */}
<div className="flex-shrink-0 mt-1">
{getInsightIcon(insight.type)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-[var(--text-primary)] text-sm">
{insight.title}
</h3>
{/* Impact Badge */}
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getImpactColor(insight.impact)}`}>
{getImpactLabel(insight.impact)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-2">
{insight.description}
</p>
{/* Impact Value */}
{insight.impact_value && (
<div className="flex items-center gap-2">
{insight.type === 'cost_optimization' && (
<span className="text-sm font-semibold text-[var(--color-success-600)]">
{insight.impact_currency}{insight.impact_value} {t('dashboard:ai_insights.savings')}
</span>
)}
{insight.type === 'waste_reduction' && (
<span className="text-sm font-semibold text-[var(--color-success-600)]">
{insight.impact_value} {t('dashboard:ai_insights.reduction')}
</span>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
) : (
/* Empty State */
<div className="px-6 pb-6">
<div className="flex items-center gap-3 p-4 rounded-lg bg-[var(--color-info-50)] border border-[var(--color-info-100)]">
<Lightbulb className="w-6 h-6 text-[var(--color-info-600)]" />
<p className="text-sm text-[var(--color-info-700)]">
{t('dashboard:ai_insights.no_insights')}
</p>
</div>
</div>
)}
</div>
);
}
export default AIInsightsBlock;

View File

@@ -26,6 +26,7 @@ interface ProductionStatusBlockProps {
lateToStartBatches?: any[];
runningBatches?: any[];
pendingBatches?: any[];
alerts?: any[]; // Add alerts prop for production-related alerts
onStartBatch?: (batchId: string) => Promise<void>;
onViewBatch?: (batchId: string) => void;
loading?: boolean;
@@ -35,6 +36,7 @@ export function ProductionStatusBlock({
lateToStartBatches = [],
runningBatches = [],
pendingBatches = [],
alerts = [],
onStartBatch,
onViewBatch,
loading,
@@ -43,6 +45,28 @@ export function ProductionStatusBlock({
const [expandedReasoningId, setExpandedReasoningId] = useState<string | null>(null);
const [processingId, setProcessingId] = useState<string | null>(null);
// Filter production-related alerts and deduplicate by ID
const productionAlerts = React.useMemo(() => {
const filtered = alerts.filter((alert: any) => {
const eventType = alert.event_type || '';
return eventType.includes('production.') ||
eventType.includes('equipment_maintenance') ||
eventType.includes('production_delay') ||
eventType.includes('batch_start_delayed');
});
// Deduplicate by alert ID to prevent duplicates from API + SSE
const uniqueAlerts = new Map<string, any>();
filtered.forEach((alert: any) => {
const alertId = alert.id || alert.event_id;
if (alertId && !uniqueAlerts.has(alertId)) {
uniqueAlerts.set(alertId, alert);
}
});
return Array.from(uniqueAlerts.values());
}, [alerts]);
if (loading) {
return (
<div className="rounded-xl shadow-lg p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)] animate-pulse">
@@ -64,11 +88,12 @@ export function ProductionStatusBlock({
const hasLate = lateToStartBatches.length > 0;
const hasRunning = runningBatches.length > 0;
const hasPending = pendingBatches.length > 0;
const hasAnyProduction = hasLate || hasRunning || hasPending;
const hasAlerts = productionAlerts.length > 0;
const hasAnyProduction = hasLate || hasRunning || hasPending || hasAlerts;
const totalCount = lateToStartBatches.length + runningBatches.length + pendingBatches.length;
// Determine header status
const status = hasLate ? 'error' : hasRunning ? 'info' : hasPending ? 'warning' : 'success';
// Determine header status - prioritize alerts and late batches
const status = hasAlerts || hasLate ? 'error' : hasRunning ? 'info' : hasPending ? 'warning' : 'success';
const statusStyles = {
success: {
@@ -115,10 +140,19 @@ export function ProductionStatusBlock({
if (typeof reasoningData === 'string') return reasoningData;
if (reasoningData.type === 'forecast_demand') {
return t('dashboard:new_dashboard.production_status.reasoning.forecast_demand', {
product: reasoningData.parameters?.product_name || batch.product_name,
demand: reasoningData.parameters?.predicted_demand || batch.planned_quantity,
});
// Check if this is enhanced reasoning with factors
if (reasoningData.parameters?.factors && reasoningData.parameters.factors.length > 0) {
return t('dashboard:new_dashboard.production_status.reasoning.forecast_demand_enhanced', {
product: reasoningData.parameters?.product_name || batch.product_name,
demand: reasoningData.parameters?.predicted_demand || batch.planned_quantity,
variance: reasoningData.parameters?.variance_percent || 0,
});
} else {
return t('dashboard:new_dashboard.production_status.reasoning.forecast_demand', {
product: reasoningData.parameters?.product_name || batch.product_name,
demand: reasoningData.parameters?.predicted_demand || batch.planned_quantity,
});
}
}
if (reasoningData.type === 'customer_order') {
@@ -127,11 +161,79 @@ export function ProductionStatusBlock({
});
}
if (reasoningData.summary) return reasoningData.summary;
return null;
};
// Get factor icon based on type (handles both uppercase and lowercase formats)
const getFactorIcon = (factorType: string) => {
const normalizedFactor = factorType?.toLowerCase() || '';
if (normalizedFactor.includes('historical') || normalizedFactor === 'historical_pattern') return '📊';
if (normalizedFactor.includes('sunny') || normalizedFactor === 'weather_sunny') return '☀️';
if (normalizedFactor.includes('rainy') || normalizedFactor === 'weather_rainy') return '🌧️';
if (normalizedFactor.includes('cold') || normalizedFactor === 'weather_cold') return '❄️';
if (normalizedFactor.includes('hot') || normalizedFactor === 'weather_hot') return '🔥';
if (normalizedFactor.includes('weekend') || normalizedFactor === 'weekend_boost') return '📅';
if (normalizedFactor.includes('inventory') || normalizedFactor === 'inventory_level') return '📦';
if (normalizedFactor.includes('seasonal') || normalizedFactor === 'seasonal_trend') return '🍂';
return '';
};
// Get factor translation key (handles both uppercase and lowercase formats)
const getFactorTranslationKey = (factorType: string) => {
const normalizedFactor = factorType?.toLowerCase().replace(/\s+/g, '_') || '';
// Direct mapping for exact matches
const factorMap: Record<string, string> = {
'historical_pattern': 'historical_pattern',
'historical_sales_pattern': 'historical_pattern',
'weather_sunny': 'weather_sunny',
'weather_impact_sunny': 'weather_sunny',
'weather_rainy': 'weather_rainy',
'weather_cold': 'weather_cold',
'weather_hot': 'weather_hot',
'weekend_boost': 'weekend_boost',
'inventory_level': 'inventory_level',
'current_inventory_trigger': 'inventory_level',
'seasonal_trend': 'seasonal_trend',
'seasonal_trend_adjustment': 'seasonal_trend',
};
// Check for direct match
if (factorMap[normalizedFactor]) {
return `dashboard:new_dashboard.production_status.factors.${factorMap[normalizedFactor]}`;
}
// Fallback to partial matching
if (normalizedFactor.includes('historical')) {
return 'dashboard:new_dashboard.production_status.factors.historical_pattern';
}
if (normalizedFactor.includes('sunny')) {
return 'dashboard:new_dashboard.production_status.factors.weather_sunny';
}
if (normalizedFactor.includes('rainy')) {
return 'dashboard:new_dashboard.production_status.factors.weather_rainy';
}
if (normalizedFactor.includes('cold')) {
return 'dashboard:new_dashboard.production_status.factors.weather_cold';
}
if (normalizedFactor.includes('hot')) {
return 'dashboard:new_dashboard.production_status.factors.weather_hot';
}
if (normalizedFactor.includes('weekend')) {
return 'dashboard:new_dashboard.production_status.factors.weekend_boost';
}
if (normalizedFactor.includes('inventory')) {
return 'dashboard:new_dashboard.production_status.factors.inventory_level';
}
if (normalizedFactor.includes('seasonal')) {
return 'dashboard:new_dashboard.production_status.factors.seasonal_trend';
}
return 'dashboard:new_dashboard.production_status.factors.general';
};
// Format time
const formatTime = (isoString: string | null | undefined) => {
if (!isoString) return '--:--';
@@ -153,6 +255,156 @@ export function ProductionStatusBlock({
return Math.round(((now - start) / (end - start)) * 100);
};
// Render an alert item
const renderAlertItem = (alert: any, index: number, total: number) => {
const alertId = alert.id || alert.event_id;
const eventType = alert.event_type || '';
const priorityLevel = alert.priority_level || 'standard';
const businessImpact = alert.business_impact || {};
const urgency = alert.urgency || {};
const metadata = alert.event_metadata || {};
// Determine alert icon and type
let icon = <AlertTriangle className="w-4 h-4 text-[var(--color-error-600)]" />;
let alertTitle = '';
let alertDescription = '';
if (eventType.includes('equipment_maintenance')) {
icon = <AlertTriangle className="w-4 h-4 text-[var(--color-warning-600)]" />;
alertTitle = alert.title || t('dashboard:new_dashboard.production_status.alerts.equipment_maintenance');
alertDescription = alert.description || alert.message || '';
} else if (eventType.includes('production_delay')) {
icon = <Clock className="w-4 h-4 text-[var(--color-error-600)]" />;
alertTitle = alert.title || t('dashboard:new_dashboard.production_status.alerts.production_delay');
alertDescription = alert.description || alert.message || '';
} else if (eventType.includes('batch_start_delayed')) {
icon = <AlertTriangle className="w-4 h-4 text-[var(--color-warning-600)]" />;
alertTitle = alert.title || t('dashboard:new_dashboard.production_status.alerts.batch_delayed');
alertDescription = alert.description || alert.message || '';
} else {
alertTitle = alert.title || t('dashboard:new_dashboard.production_status.alerts.generic');
alertDescription = alert.description || alert.message || '';
}
// Priority badge styling
const priorityStyles = {
critical: 'bg-[var(--color-error-100)] text-[var(--color-error-700)]',
important: 'bg-[var(--color-warning-100)] text-[var(--color-warning-700)]',
standard: 'bg-[var(--color-info-100)] text-[var(--color-info-700)]',
info: 'bg-[var(--bg-tertiary)] text-[var(--text-tertiary)]',
};
// Format time ago
const formatTimeAgo = (isoString: string) => {
const date = new Date(isoString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
if (diffMins < 1) return t('common:time.just_now');
if (diffMins < 60) return t('common:time.minutes_ago', { count: diffMins });
if (diffHours < 24) return t('common:time.hours_ago', { count: diffHours });
return t('common:time.days_ago', { count: Math.floor(diffHours / 24) });
};
return (
<div
key={alertId || index}
className={`p-4 ${index < total - 1 ? 'border-b border-[var(--border-primary)]' : ''}`}
>
<div className="flex items-start gap-4">
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-center gap-2 mb-2 flex-wrap">
{icon}
<span className="font-semibold text-[var(--text-primary)]">
{alertTitle}
</span>
<div className={`px-2 py-0.5 rounded-full text-xs font-medium ${priorityStyles[priorityLevel as keyof typeof priorityStyles] || priorityStyles.standard}`}>
{t(`dashboard:new_dashboard.production_status.priority.${priorityLevel}`)}
</div>
{alert.created_at && (
<span className="text-xs text-[var(--text-tertiary)]">
{formatTimeAgo(alert.created_at)}
</span>
)}
</div>
{/* Description */}
<p className="text-sm text-[var(--text-secondary)] mb-2">
{alertDescription}
</p>
{/* Additional Details */}
<div className="flex flex-wrap gap-3 text-xs">
{/* Business Impact */}
{businessImpact.affected_orders > 0 && (
<div className="flex items-center gap-1 text-[var(--text-tertiary)]">
<span>📦</span>
<span>
{t('dashboard:new_dashboard.production_status.alerts.affected_orders', {
count: businessImpact.affected_orders
})}
</span>
</div>
)}
{businessImpact.production_delay_hours > 0 && (
<div className="flex items-center gap-1 text-[var(--text-tertiary)]">
<Clock className="w-3 h-3" />
<span>
{t('dashboard:new_dashboard.production_status.alerts.delay_hours', {
hours: Math.round(businessImpact.production_delay_hours * 10) / 10
})}
</span>
</div>
)}
{businessImpact.financial_impact_eur > 0 && (
<div className="flex items-center gap-1 text-[var(--text-tertiary)]">
<span>💰</span>
<span>
{t('dashboard:new_dashboard.production_status.alerts.financial_impact', {
amount: Math.round(businessImpact.financial_impact_eur)
})}
</span>
</div>
)}
{/* Urgency Info */}
{urgency.hours_until_consequence !== undefined && urgency.hours_until_consequence < 24 && (
<div className="flex items-center gap-1 text-[var(--color-warning-600)] font-medium">
<Timer className="w-3 h-3" />
<span>
{t('dashboard:new_dashboard.production_status.alerts.urgent_in', {
hours: Math.round(urgency.hours_until_consequence * 10) / 10
})}
</span>
</div>
)}
{/* Product/Batch Info from metadata */}
{metadata.product_name && (
<div className="flex items-center gap-1 text-[var(--text-tertiary)]">
<span>🥖</span>
<span>{metadata.product_name}</span>
</div>
)}
{metadata.batch_number && (
<div className="flex items-center gap-1 text-[var(--text-tertiary)]">
<span>#</span>
<span>{metadata.batch_number}</span>
</div>
)}
</div>
</div>
</div>
</div>
);
};
// Render a batch item
const renderBatchItem = (batch: any, type: 'late' | 'running' | 'pending', index: number, total: number) => {
const batchId = batch.id || batch.batch_id;
@@ -289,11 +541,123 @@ export function ProductionStatusBlock({
<div className="mt-3 p-3 rounded-lg bg-[var(--color-primary-50)] border border-[var(--color-primary-100)]">
<div className="flex items-start gap-2">
<Brain className="w-4 h-4 text-[var(--color-primary-600)] mt-0.5 flex-shrink-0" />
<div>
<div className="w-full">
<p className="text-sm font-medium text-[var(--color-primary-700)] mb-1">
{t('dashboard:new_dashboard.production_status.ai_reasoning')}
</p>
<p className="text-sm text-[var(--color-primary-600)]">{reasoning}</p>
<p className="text-sm text-[var(--color-primary-600)] mb-2">{reasoning}</p>
{/* Weather Data Display */}
{batch.reasoning_data?.parameters?.weather_data && (
<div className="mb-3 p-2 rounded-lg bg-[var(--bg-primary)] border border-[var(--color-primary-200)]">
<div className="flex items-center gap-3">
<span className="text-2xl">
{batch.reasoning_data.parameters.weather_data.condition === 'sunny' && '☀️'}
{batch.reasoning_data.parameters.weather_data.condition === 'rainy' && '🌧️'}
{batch.reasoning_data.parameters.weather_data.condition === 'cold' && '❄️'}
{batch.reasoning_data.parameters.weather_data.condition === 'hot' && '🔥'}
</span>
<div className="flex-1">
<p className="text-xs font-semibold text-[var(--text-primary)] uppercase">
{t('dashboard:new_dashboard.production_status.weather_forecast')}
</p>
<p className="text-sm text-[var(--text-secondary)]">
{t(`dashboard:new_dashboard.production_status.weather_conditions.${batch.reasoning_data.parameters.weather_data.condition}`, {
temp: batch.reasoning_data.parameters.weather_data.temperature,
humidity: batch.reasoning_data.parameters.weather_data.humidity
})}
</p>
</div>
<div className="text-right">
<p className="text-xs text-[var(--text-tertiary)]">
{t('dashboard:new_dashboard.production_status.demand_impact')}
</p>
<p className={`text-sm font-semibold ${
batch.reasoning_data.parameters.weather_data.impact_factor > 1
? 'text-[var(--color-success-600)]'
: 'text-[var(--color-warning-600)]'
}`}>
{batch.reasoning_data.parameters.weather_data.impact_factor > 1 ? '+' : ''}
{Math.round((batch.reasoning_data.parameters.weather_data.impact_factor - 1) * 100)}%
</p>
</div>
</div>
</div>
)}
{/* Enhanced reasoning with factors */}
{batch.reasoning_data?.parameters?.factors && batch.reasoning_data.parameters.factors.length > 0 && (
<div className="mt-2 space-y-2">
<p className="text-xs font-medium text-[var(--color-primary-800)] uppercase tracking-wide">
{t('dashboard:new_dashboard.production_status.factors_title')}
</p>
<div className="space-y-2">
{batch.reasoning_data.parameters.factors.map((factor: any, factorIndex: number) => (
<div key={factorIndex} className="flex items-center gap-2">
<span className="text-lg">{getFactorIcon(factor.factor)}</span>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[var(--text-secondary)]">
{t(getFactorTranslationKey(factor.factor))}
</span>
<span className="text-xs text-[var(--text-tertiary)]">
({Math.round(factor.weight * 100)}%)
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-1.5 mt-1">
<div
className="bg-[var(--color-primary-500)] h-1.5 rounded-full transition-all"
style={{ width: `${Math.round(factor.weight * 100)}%` }}
/>
</div>
</div>
<span className={`text-sm font-semibold ${
factor.contribution >= 0
? 'text-[var(--color-success-600)]'
: 'text-[var(--color-error-600)]'
}`}>
{factor.contribution >= 0 ? '+' : ''}{Math.round(factor.contribution)}
</span>
</div>
))}
</div>
{/* Confidence and variance info */}
<div className="mt-2 pt-2 border-t border-[var(--color-primary-100)] flex flex-wrap items-center gap-4 text-xs">
{batch.reasoning_data.metadata?.confidence_score && (
<div className="flex items-center gap-1 text-[var(--text-secondary)]">
<span className="text-[var(--color-primary-600)]">🎯</span>
<span>
{t('dashboard:new_dashboard.production_status.confidence', {
confidence: Math.round(batch.reasoning_data.metadata.confidence_score * 100)
})}
</span>
</div>
)}
{batch.reasoning_data.parameters?.variance_percent && (
<div className="flex items-center gap-1 text-[var(--text-secondary)]">
<span className="text-[var(--color-primary-600)]">📈</span>
<span>
{t('dashboard:new_dashboard.production_status.variance', {
variance: batch.reasoning_data.parameters.variance_percent
})}
</span>
</div>
)}
{batch.reasoning_data.parameters?.historical_average && (
<div className="flex items-center gap-1 text-[var(--text-secondary)]">
<span className="text-[var(--color-primary-600)]">📊</span>
<span>
{t('dashboard:new_dashboard.production_status.historical_avg', {
avg: Math.round(batch.reasoning_data.parameters.historical_average)
})}
</span>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
@@ -354,6 +718,21 @@ export function ProductionStatusBlock({
{/* Content */}
{hasAnyProduction ? (
<div className="border-t border-[var(--border-primary)]">
{/* Production Alerts Section */}
{hasAlerts && (
<div className="bg-[var(--color-error-50)]">
<div className="px-6 py-3 border-b border-[var(--color-error-100)]">
<h3 className="text-sm font-semibold text-[var(--color-error-700)] flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
{t('dashboard:new_dashboard.production_status.alerts_section')}
</h3>
</div>
{productionAlerts.map((alert, index) =>
renderAlertItem(alert, index, productionAlerts.length)
)}
</div>
)}
{/* Late to Start Section */}
{hasLate && (
<div className="bg-[var(--color-error-50)]">

View File

@@ -66,8 +66,8 @@ export function SystemStatusBlock({ data, loading }: SystemStatusBlockProps) {
const diffMinutes = Math.floor(diffMs / (1000 * 60));
if (diffMinutes < 1) return t('common:time.just_now', 'Just now');
if (diffMinutes < 60) return t('common:time.minutes_ago', '{{count}} min ago', { count: diffMinutes });
if (diffHours < 24) return t('common:time.hours_ago', '{{count}}h ago', { count: diffHours });
if (diffMinutes < 60) return t('common:time.minutes_ago', '{count} min ago', { count: diffMinutes });
if (diffHours < 24) return t('common:time.hours_ago', '{count}h ago', { count: diffHours });
return date.toLocaleDateString();
};

View File

@@ -8,3 +8,4 @@ export { SystemStatusBlock } from './SystemStatusBlock';
export { PendingPurchasesBlock } from './PendingPurchasesBlock';
export { PendingDeliveriesBlock } from './PendingDeliveriesBlock';
export { ProductionStatusBlock } from './ProductionStatusBlock';
export { AIInsightsBlock } from './AIInsightsBlock';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Clock, Timer, CheckCircle, AlertCircle, Package, Play, Pause, X, Eye } from 'lucide-react';
import { Clock, Timer, CheckCircle, AlertCircle, Package, Play, Pause, X, Eye, Info } from 'lucide-react';
import { StatusCard, StatusIndicatorConfig } from '../../ui/StatusCard/StatusCard';
import { statusColors } from '../../../styles/colors';
import { ProductionBatchResponse, ProductionStatus, ProductionPriority } from '../../../api/types/production';
@@ -258,6 +258,39 @@ export const ProductionStatusCard: React.FC<ProductionStatusCardProps> = ({
metadata.push(safeText(qualityInfo, qualityInfo, 50));
}
// Add reasoning information if available
if (batch.reasoning_data) {
const { trigger_type, trigger_description, factors, consequence, confidence_score, variance, prediction_details } = batch.reasoning_data;
// Add trigger information
if (trigger_type) {
let triggerLabel = t(`reasoning:triggers.${trigger_type.toLowerCase()}`);
if (triggerLabel === `reasoning:triggers.${trigger_type.toLowerCase()}`) {
triggerLabel = trigger_type;
}
metadata.push(`Causa: ${triggerLabel}`);
}
// Add factors
if (factors && Array.isArray(factors) && factors.length > 0) {
const factorLabels = factors.map(factor => {
const factorLabel = t(`reasoning:factors.${factor.toLowerCase()}`);
return factorLabel === `reasoning:factors.${factor.toLowerCase()}` ? factor : factorLabel;
}).join(', ');
metadata.push(`Factores: ${factorLabels}`);
}
// Add confidence score
if (confidence_score) {
metadata.push(`Confianza: ${confidence_score}%`);
}
// Add variance information
if (variance) {
metadata.push(`Varianza: ${variance}%`);
}
}
if (batch.priority === ProductionPriority.URGENT) {
metadata.push('⚡ Orden urgente');
}