demo seed change
This commit is contained in:
181
frontend/src/components/dashboard/blocks/AIInsightsBlock.tsx
Normal file
181
frontend/src/components/dashboard/blocks/AIInsightsBlock.tsx
Normal 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;
|
||||
@@ -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)]">
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -8,3 +8,4 @@ export { SystemStatusBlock } from './SystemStatusBlock';
|
||||
export { PendingPurchasesBlock } from './PendingPurchasesBlock';
|
||||
export { PendingDeliveriesBlock } from './PendingDeliveriesBlock';
|
||||
export { ProductionStatusBlock } from './ProductionStatusBlock';
|
||||
export { AIInsightsBlock } from './AIInsightsBlock';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user