Refactor components and modals

This commit is contained in:
Urtzi Alfaro
2025-09-26 07:46:25 +02:00
parent cf4405b771
commit d573c38621
80 changed files with 3421 additions and 4617 deletions

View File

@@ -1,362 +0,0 @@
import React, { useRef, useEffect } from 'react';
export type ChartType = 'line' | 'bar' | 'area' | 'pie' | 'doughnut';
export interface ChartDataPoint {
x: string | number;
y: number;
label?: string;
color?: string;
}
export interface ChartSeries {
id: string;
name: string;
type: ChartType;
data: ChartDataPoint[];
color: string;
visible?: boolean;
}
export interface AnalyticsChartProps {
series: ChartSeries[];
height?: number;
className?: string;
showLegend?: boolean;
showGrid?: boolean;
animate?: boolean;
}
export const AnalyticsChart: React.FC<AnalyticsChartProps> = ({
series,
height = 300,
className = '',
showLegend = true,
showGrid = true,
animate = true
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!canvasRef.current || series.length === 0) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
// Clear canvas
ctx.clearRect(0, 0, rect.width, rect.height);
// Chart settings
const padding = 40;
const chartWidth = rect.width - 2 * padding;
const chartHeight = rect.height - 2 * padding - (showLegend ? 40 : 0);
// Get visible series
const visibleSeries = series.filter(s => s.visible !== false);
if (visibleSeries.length === 0) return;
// Draw based on chart type
const primarySeries = visibleSeries[0];
if (primarySeries.type === 'pie' || primarySeries.type === 'doughnut') {
drawPieChart(ctx, primarySeries, rect.width, rect.height - (showLegend ? 40 : 0));
} else {
drawCartesianChart(ctx, visibleSeries, padding, chartWidth, chartHeight, showGrid);
}
// Draw legend
if (showLegend) {
drawLegend(ctx, visibleSeries, rect.width, rect.height);
}
}, [series, height, showLegend, showGrid]);
const drawPieChart = (ctx: CanvasRenderingContext2D, series: ChartSeries, width: number, height: number) => {
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 3;
const innerRadius = series.type === 'doughnut' ? radius * 0.6 : 0;
const total = series.data.reduce((sum, point) => sum + point.y, 0);
let startAngle = -Math.PI / 2;
series.data.forEach((point, index) => {
const sliceAngle = (point.y / total) * 2 * Math.PI;
const color = point.color || series.color || getDefaultColor(index);
// Draw slice
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
if (innerRadius > 0) {
ctx.arc(centerX, centerY, innerRadius, startAngle + sliceAngle, startAngle, true);
}
ctx.closePath();
ctx.fill();
// Draw slice border
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
// Draw labels
const labelAngle = startAngle + sliceAngle / 2;
const labelRadius = radius + 20;
const labelX = centerX + Math.cos(labelAngle) * labelRadius;
const labelY = centerY + Math.sin(labelAngle) * labelRadius;
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label || point.x.toString(), labelX, labelY);
const percentage = ((point.y / total) * 100).toFixed(1);
ctx.fillText(`${percentage}%`, labelX, labelY + 15);
startAngle += sliceAngle;
});
// Draw center label for doughnut
if (series.type === 'doughnut') {
ctx.fillStyle = '#374151';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(total.toString(), centerX, centerY - 5);
ctx.font = '12px sans-serif';
ctx.fillText('Total', centerX, centerY + 10);
}
};
const drawCartesianChart = (
ctx: CanvasRenderingContext2D,
seriesList: ChartSeries[],
padding: number,
chartWidth: number,
chartHeight: number,
showGrid: boolean
) => {
// Get all data points to determine scales
const allData = seriesList.flatMap(s => s.data);
const maxY = Math.max(...allData.map(d => d.y));
const minY = Math.min(0, Math.min(...allData.map(d => d.y)));
// Draw grid
if (showGrid) {
ctx.strokeStyle = '#e2e8f0';
ctx.lineWidth = 1;
// Horizontal grid lines
for (let i = 0; i <= 5; i++) {
const y = padding + (i * chartHeight) / 5;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(padding + chartWidth, y);
ctx.stroke();
// Y-axis labels
const value = maxY - (i * (maxY - minY)) / 5;
ctx.fillStyle = '#64748b';
ctx.font = '12px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(Math.round(value).toString(), padding - 10, y + 4);
}
}
// Draw axes
ctx.strokeStyle = '#374151';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, padding + chartHeight);
ctx.lineTo(padding + chartWidth, padding + chartHeight);
ctx.stroke();
// Draw each series
seriesList.forEach((series, seriesIndex) => {
ctx.strokeStyle = series.color || getDefaultColor(seriesIndex);
ctx.fillStyle = series.color || getDefaultColor(seriesIndex);
if (series.type === 'line') {
drawLineChart(ctx, series, padding, chartWidth, chartHeight, maxY, minY);
} else if (series.type === 'bar') {
drawBarChart(ctx, series, padding, chartWidth, chartHeight, maxY, minY, seriesIndex, seriesList.length);
} else if (series.type === 'area') {
drawAreaChart(ctx, series, padding, chartWidth, chartHeight, maxY, minY);
}
});
};
const drawLineChart = (
ctx: CanvasRenderingContext2D,
series: ChartSeries,
padding: number,
chartWidth: number,
chartHeight: number,
maxY: number,
minY: number
) => {
if (series.data.length === 0) return;
ctx.lineWidth = 3;
ctx.beginPath();
series.data.forEach((point, index) => {
const x = padding + (index * chartWidth) / (series.data.length - 1 || 1);
const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// Draw points
ctx.fillStyle = ctx.strokeStyle;
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
});
ctx.stroke();
};
const drawBarChart = (
ctx: CanvasRenderingContext2D,
series: ChartSeries,
padding: number,
chartWidth: number,
chartHeight: number,
maxY: number,
minY: number,
seriesIndex: number,
totalSeries: number
) => {
const barGroupWidth = chartWidth / series.data.length;
const barWidth = barGroupWidth / totalSeries - 4;
series.data.forEach((point, index) => {
const x = padding + index * barGroupWidth + seriesIndex * (barWidth + 2) + 2;
const barHeight = ((point.y - minY) / (maxY - minY)) * chartHeight;
const y = padding + chartHeight - barHeight;
ctx.fillRect(x, y, barWidth, barHeight);
// Draw value labels
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.y.toString(), x + barWidth / 2, y - 5);
// Draw x-axis labels
if (seriesIndex === 0) {
ctx.fillText(
point.label || point.x.toString(),
padding + index * barGroupWidth + barGroupWidth / 2,
padding + chartHeight + 20
);
}
});
};
const drawAreaChart = (
ctx: CanvasRenderingContext2D,
series: ChartSeries,
padding: number,
chartWidth: number,
chartHeight: number,
maxY: number,
minY: number
) => {
if (series.data.length === 0) return;
// Create gradient
const gradient = ctx.createLinearGradient(0, padding, 0, padding + chartHeight);
gradient.addColorStop(0, `${series.color}40`);
gradient.addColorStop(1, `${series.color}10`);
ctx.fillStyle = gradient;
ctx.strokeStyle = series.color;
ctx.lineWidth = 2;
ctx.beginPath();
// Start from bottom left
ctx.moveTo(padding, padding + chartHeight);
series.data.forEach((point, index) => {
const x = padding + (index * chartWidth) / (series.data.length - 1 || 1);
const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight;
ctx.lineTo(x, y);
});
// Close the path at bottom right
ctx.lineTo(padding + chartWidth, padding + chartHeight);
ctx.closePath();
ctx.fill();
// Draw the line on top
ctx.beginPath();
series.data.forEach((point, index) => {
const x = padding + (index * chartWidth) / (series.data.length - 1 || 1);
const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
};
const drawLegend = (ctx: CanvasRenderingContext2D, seriesList: ChartSeries[], width: number, height: number) => {
const legendY = height - 30;
let legendX = width / 2 - (seriesList.length * 80) / 2;
seriesList.forEach((series, index) => {
// Draw legend color box
ctx.fillStyle = series.color || getDefaultColor(index);
ctx.fillRect(legendX, legendY, 12, 12);
// Draw legend text
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(series.name, legendX + 18, legendY + 9);
legendX += Math.max(80, ctx.measureText(series.name).width + 30);
});
};
const getDefaultColor = (index: number): string => {
const colors = [
'#d97706', // orange
'#0284c7', // blue
'#16a34a', // green
'#dc2626', // red
'#7c3aed', // purple
'#0891b2', // cyan
'#ea580c', // orange variant
'#059669' // emerald
];
return colors[index % colors.length];
};
return (
<div className={`relative ${className}`} style={{ height: `${height}px` }}>
<canvas
ref={canvasRef}
className="w-full h-full"
style={{ height: `${height}px` }}
/>
</div>
);
};

View File

@@ -1,103 +0,0 @@
import React from 'react';
import { Card } from '../../ui';
import { LucideIcon } from 'lucide-react';
export interface AnalyticsWidgetProps {
title: string;
subtitle?: string;
icon?: LucideIcon;
loading?: boolean;
error?: string;
className?: string;
children: React.ReactNode;
actions?: React.ReactNode;
}
export const AnalyticsWidget: React.FC<AnalyticsWidgetProps> = ({
title,
subtitle,
icon: Icon,
loading = false,
error,
className = '',
children,
actions
}) => {
if (error) {
return (
<Card className={`p-6 ${className}`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
{Icon && (
<div className="w-10 h-10 bg-red-100 dark:bg-red-900/20 rounded-lg flex items-center justify-center">
<Icon className="w-5 h-5 text-red-600 dark:text-red-400" />
</div>
)}
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{title}</h3>
{subtitle && <p className="text-sm text-[var(--text-secondary)]">{subtitle}</p>}
</div>
</div>
{actions}
</div>
<div className="text-center py-8">
<div className="w-12 h-12 mx-auto bg-red-100 dark:bg-red-900/20 rounded-lg flex items-center justify-center mb-3">
{Icon && <Icon className="w-6 h-6 text-red-600 dark:text-red-400" />}
</div>
<p className="text-sm text-red-600 dark:text-red-400">Error: {error}</p>
</div>
</Card>
);
}
if (loading) {
return (
<Card className={`p-6 ${className}`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
{Icon && (
<div className="w-10 h-10 bg-[var(--bg-tertiary)] rounded-lg flex items-center justify-center">
<Icon className="w-5 h-5 text-[var(--text-tertiary)]" />
</div>
)}
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{title}</h3>
{subtitle && <p className="text-sm text-[var(--text-secondary)]">{subtitle}</p>}
</div>
</div>
{actions}
</div>
<div className="animate-pulse">
<div className="h-4 bg-[var(--bg-tertiary)] rounded w-3/4 mb-3"></div>
<div className="h-4 bg-[var(--bg-tertiary)] rounded w-1/2 mb-3"></div>
<div className="h-8 bg-[var(--bg-tertiary)] rounded w-full mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-[var(--bg-tertiary)] rounded"></div>
<div className="h-4 bg-[var(--bg-tertiary)] rounded w-5/6"></div>
<div className="h-4 bg-[var(--bg-tertiary)] rounded w-4/6"></div>
</div>
</div>
</Card>
);
}
return (
<Card className={`p-6 ${className}`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
{Icon && (
<div className="w-10 h-10 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center">
<Icon className="w-5 h-5 text-[var(--color-primary)]" />
</div>
)}
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{title}</h3>
{subtitle && <p className="text-sm text-[var(--text-secondary)]">{subtitle}</p>}
</div>
</div>
{actions}
</div>
{children}
</Card>
);
};

View File

@@ -1,382 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Brain, TrendingUp, AlertTriangle, Target, Zap, DollarSign, Clock } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { Badge, Button } from '../../../ui';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface AIInsight {
id: string;
type: 'optimization' | 'prediction' | 'anomaly' | 'recommendation';
priority: 'low' | 'medium' | 'high' | 'critical';
confidence: number; // percentage
title: string;
description: string;
impact: {
type: 'cost_savings' | 'efficiency_gain' | 'quality_improvement' | 'risk_mitigation';
value: number;
unit: 'euros' | 'percentage' | 'hours' | 'units';
};
actionable: boolean;
category: 'production' | 'quality' | 'maintenance' | 'energy' | 'scheduling';
equipment?: string;
timeline: string;
status: 'new' | 'acknowledged' | 'in_progress' | 'implemented' | 'dismissed';
}
export const AIInsightsWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
// Mock AI insights data - replace with real AI API call
const aiInsights: AIInsight[] = [
{
id: '1',
type: 'optimization',
priority: 'high',
confidence: 92,
title: 'Optimización de Horarios de Horneado',
description: 'Ajustar los horarios de horneado para aprovechar las tarifas eléctricas más bajas puede reducir los costos de energía en un 15%.',
impact: {
type: 'cost_savings',
value: 45,
unit: 'euros'
},
actionable: true,
category: 'energy',
equipment: 'Horno Principal',
timeline: 'Esta semana',
status: 'new'
},
{
id: '2',
type: 'prediction',
priority: 'medium',
confidence: 87,
title: 'Aumento de Demanda de Croissants Predicho',
description: 'Los datos meteorológicos y de eventos sugieren un aumento del 40% en la demanda de croissants este fin de semana.',
impact: {
type: 'efficiency_gain',
value: 25,
unit: 'percentage'
},
actionable: true,
category: 'production',
timeline: 'Este fin de semana',
status: 'acknowledged'
},
{
id: '3',
type: 'anomaly',
priority: 'critical',
confidence: 96,
title: 'Declive en Puntuación de Calidad Detectado',
description: 'La calidad del pan ha disminuido un 8% en los últimos 3 días. Verificar los niveles de humedad de la harina.',
impact: {
type: 'quality_improvement',
value: 12,
unit: 'percentage'
},
actionable: true,
category: 'quality',
timeline: 'Inmediatamente',
status: 'in_progress'
},
{
id: '4',
type: 'recommendation',
priority: 'medium',
confidence: 89,
title: 'Mantenimiento Preventivo Recomendado',
description: 'La Mezcladora #2 muestra patrones de desgaste temprano. Programar mantenimiento para prevenir averías.',
impact: {
type: 'risk_mitigation',
value: 8,
unit: 'hours'
},
actionable: true,
category: 'maintenance',
equipment: 'Mezcladora A',
timeline: 'Próxima semana',
status: 'new'
},
{
id: '5',
type: 'optimization',
priority: 'low',
confidence: 78,
title: 'Optimización de Secuencia de Productos',
description: 'Reordenar la secuencia de productos puede mejorar la eficiencia general en un 5%.',
impact: {
type: 'efficiency_gain',
value: 5,
unit: 'percentage'
},
actionable: true,
category: 'scheduling',
timeline: 'Próximo mes',
status: 'dismissed'
}
];
const getTypeIcon = (type: AIInsight['type']) => {
switch (type) {
case 'optimization': return Target;
case 'prediction': return TrendingUp;
case 'anomaly': return AlertTriangle;
case 'recommendation': return Brain;
default: return Brain;
}
};
const getTypeColor = (type: AIInsight['type']) => {
switch (type) {
case 'optimization': return 'text-green-600';
case 'prediction': return 'text-blue-600';
case 'anomaly': return 'text-red-600';
case 'recommendation': return 'text-purple-600';
default: return 'text-gray-600';
}
};
const getPriorityBadgeVariant = (priority: AIInsight['priority']) => {
switch (priority) {
case 'critical': return 'error';
case 'high': return 'warning';
case 'medium': return 'info';
case 'low': return 'default';
default: return 'default';
}
};
const getStatusBadgeVariant = (status: AIInsight['status']) => {
switch (status) {
case 'new': return 'warning';
case 'acknowledged': return 'info';
case 'in_progress': return 'info';
case 'implemented': return 'success';
case 'dismissed': return 'default';
default: return 'default';
}
};
const getImpactIcon = (type: AIInsight['impact']['type']) => {
switch (type) {
case 'cost_savings': return DollarSign;
case 'efficiency_gain': return Zap;
case 'quality_improvement': return Target;
case 'risk_mitigation': return Clock;
default: return Target;
}
};
const formatImpactValue = (impact: AIInsight['impact']) => {
switch (impact.unit) {
case 'euros': return `${impact.value}`;
case 'percentage': return `${impact.value}%`;
case 'hours': return `${impact.value}h`;
case 'units': return `${impact.value} unidades`;
default: return impact.value.toString();
}
};
const activeInsights = aiInsights.filter(i => i.status !== 'dismissed');
const highPriorityInsights = activeInsights.filter(i => i.priority === 'high' || i.priority === 'critical');
const implementedInsights = aiInsights.filter(i => i.status === 'implemented');
const avgConfidence = activeInsights.reduce((sum, insight) => sum + insight.confidence, 0) / activeInsights.length;
const totalPotentialSavings = aiInsights
.filter(i => i.impact.type === 'cost_savings' && i.status !== 'dismissed')
.reduce((sum, insight) => sum + insight.impact.value, 0);
return (
<AnalyticsWidget
title={t('ai.title')}
subtitle={t('ai.subtitle')}
icon={Brain}
actions={
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Brain className="w-4 h-4 mr-1" />
{t('ai.actions.train_model')}
</Button>
<Button variant="primary" size="sm">
<Target className="w-4 h-4 mr-1" />
{t('ai.actions.implement_all')}
</Button>
</div>
}
>
<div className="space-y-6">
{/* AI Insights Overview Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<Brain className="w-8 h-8 mx-auto text-purple-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{activeInsights.length}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.stats.active_insights')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<AlertTriangle className="w-8 h-8 mx-auto text-red-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{highPriorityInsights.length}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.stats.high_priority')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-green-600 font-bold text-sm"></span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalPotentialSavings}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.stats.potential_savings')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-blue-600 font-bold text-sm">%</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{avgConfidence.toFixed(0)}%</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.stats.avg_confidence')}</p>
</div>
</div>
{/* AI Status */}
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-sm font-medium text-green-600">
{t('ai.status.active')}
</span>
<span className="text-xs text-[var(--text-secondary)] ml-2">
{t('ai.last_updated')}
</span>
</div>
</div>
{/* High Priority Insights */}
{highPriorityInsights.length > 0 && (
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<AlertTriangle className="w-4 h-4 mr-2 text-red-600" />
{t('ai.high_priority_insights')} ({highPriorityInsights.length})
</h4>
<div className="space-y-3">
{highPriorityInsights.slice(0, 3).map((insight) => {
const TypeIcon = getTypeIcon(insight.type);
const ImpactIcon = getImpactIcon(insight.impact.type);
return (
<div key={insight.id} className="p-4 bg-[var(--bg-secondary)] rounded-lg border border-red-200 dark:border-red-800">
<div className="flex items-start justify-between mb-3">
<div className="flex items-start space-x-3">
<TypeIcon className={`w-5 h-5 mt-1 ${getTypeColor(insight.type)}`} />
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<p className="font-medium text-[var(--text-primary)]">{insight.title}</p>
<Badge variant={getPriorityBadgeVariant(insight.priority)}>
{t(`ai.priority.${insight.priority}`)}
</Badge>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-2">{insight.description}</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
<span>{t('ai.confidence')}: {insight.confidence}%</span>
<span>{t('ai.timeline')}: {insight.timeline}</span>
{insight.equipment && <span>{t('ai.equipment')}: {insight.equipment}</span>}
</div>
</div>
</div>
<Badge variant={getStatusBadgeVariant(insight.status)}>
{t(`ai.status.${insight.status}`)}
</Badge>
</div>
{/* Impact */}
<div className="flex items-center justify-between p-3 bg-[var(--bg-tertiary)] rounded-lg">
<div className="flex items-center space-x-2">
<ImpactIcon className="w-4 h-4 text-[var(--color-primary)]" />
<span className="text-sm font-medium text-[var(--text-primary)]">
{t(`ai.impact.${insight.impact.type}`)}
</span>
</div>
<span className="text-lg font-bold text-green-600">
{formatImpactValue(insight.impact)}
</span>
</div>
{insight.actionable && insight.status === 'new' && (
<div className="mt-3 flex space-x-2">
<Button variant="outline" size="sm">
{t('ai.actions.acknowledge')}
</Button>
<Button variant="primary" size="sm">
{t('ai.actions.implement')}
</Button>
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* All Insights */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Brain className="w-4 h-4 mr-2" />
{t('ai.all_insights')} ({activeInsights.length})
</h4>
<div className="space-y-3 max-h-96 overflow-y-auto">
{activeInsights.map((insight) => {
const TypeIcon = getTypeIcon(insight.type);
const ImpactIcon = getImpactIcon(insight.impact.type);
return (
<div key={insight.id} className="p-3 bg-[var(--bg-secondary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<TypeIcon className={`w-4 h-4 ${getTypeColor(insight.type)}`} />
<div className="flex-1">
<div className="flex items-center space-x-2">
<p className="font-medium text-[var(--text-primary)] text-sm">{insight.title}</p>
<Badge variant={getPriorityBadgeVariant(insight.priority)} className="text-xs">
{t(`ai.priority.${insight.priority}`)}
</Badge>
</div>
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)] mt-1">
<span>{insight.confidence}% {t('ai.confidence')}</span>
<span></span>
<span>{insight.timeline}</span>
<span></span>
<div className="flex items-center space-x-1">
<ImpactIcon className="w-3 h-3" />
<span>{formatImpactValue(insight.impact)}</span>
</div>
</div>
</div>
</div>
<Badge variant={getStatusBadgeVariant(insight.status)} className="text-xs">
{t(`ai.status.${insight.status}`)}
</Badge>
</div>
</div>
);
})}
</div>
</div>
{/* AI Performance Summary */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<Brain className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('ai.performance.summary')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{implementedInsights.length} {t('ai.performance.insights_implemented')},
{totalPotentialSavings > 0 && `${totalPotentialSavings} ${t('ai.performance.in_savings_identified')}`}
</p>
</div>
</div>
</div>
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,253 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { BarChart3, TrendingUp, AlertTriangle, Zap } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button, ProgressBar } from '../../../ui';
import { useProductionDashboard, useCapacityStatus } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface CapacityData {
resource: string;
utilization: number;
capacity: number;
allocated: number;
available: number;
type: 'oven' | 'mixer' | 'staff' | 'station';
}
export const CapacityUtilizationWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const today = new Date().toISOString().split('T')[0];
const { data: dashboard, isLoading: dashboardLoading, error: dashboardError } = useProductionDashboard(tenantId);
const { data: capacity, isLoading: capacityLoading, error: capacityError } = useCapacityStatus(tenantId, today);
const isLoading = dashboardLoading || capacityLoading;
const error = dashboardError?.message || capacityError?.message;
const overallUtilization = dashboard?.capacity_utilization || 0;
// Mock capacity data by resource type (in real implementation, this would be processed from capacity API)
const getCapacityByResource = (): CapacityData[] => {
return [
{
resource: t('equipment.oven_capacity'),
utilization: 89,
capacity: 24,
allocated: 21,
available: 3,
type: 'oven'
},
{
resource: t('equipment.mixer_capacity'),
utilization: 67,
capacity: 12,
allocated: 8,
available: 4,
type: 'mixer'
},
{
resource: t('schedule.staff_capacity'),
utilization: 85,
capacity: 8,
allocated: 7,
available: 1,
type: 'staff'
},
{
resource: t('schedule.work_stations'),
utilization: 75,
capacity: 6,
allocated: 4,
available: 2,
type: 'station'
}
];
};
const capacityData = getCapacityByResource();
// Heatmap data for hourly utilization
const getHourlyUtilizationData = (): ChartSeries[] => {
const hours = Array.from({ length: 12 }, (_, i) => i + 6); // 6 AM to 6 PM
const mockData = hours.map(hour => ({
x: `${hour}:00`,
y: Math.max(20, Math.min(100, 50 + Math.sin(hour / 2) * 30 + Math.random() * 20)),
label: `${hour}:00`
}));
return [
{
id: 'hourly-utilization',
name: t('insights.hourly_utilization'),
type: 'area',
color: '#d97706',
data: mockData
}
];
};
const getUtilizationStatus = (utilization: number) => {
if (utilization >= 95) return { color: 'text-red-600', status: 'critical', bgColor: 'bg-red-100 dark:bg-red-900/20' };
if (utilization >= 85) return { color: 'text-orange-600', status: 'high', bgColor: 'bg-orange-100 dark:bg-orange-900/20' };
if (utilization >= 70) return { color: 'text-green-600', status: 'optimal', bgColor: 'bg-green-100 dark:bg-green-900/20' };
return { color: 'text-blue-600', status: 'low', bgColor: 'bg-blue-100 dark:bg-blue-900/20' };
};
const getResourceIcon = (type: CapacityData['type']) => {
switch (type) {
case 'oven': return '🔥';
case 'mixer': return '🥄';
case 'staff': return '👥';
case 'station': return '🏭';
default: return '📊';
}
};
const getProgressBarVariant = (utilization: number) => {
if (utilization >= 95) return 'error';
if (utilization >= 85) return 'warning';
if (utilization >= 70) return 'success';
return 'info';
};
const overallStatus = getUtilizationStatus(overallUtilization);
return (
<AnalyticsWidget
title={t('stats.capacity_utilization')}
subtitle={t('insights.resource_allocation_efficiency')}
icon={BarChart3}
loading={isLoading}
error={error}
actions={
<Button variant="outline" size="sm">
<Zap className="w-4 h-4 mr-1" />
{t('actions.optimize')}
</Button>
}
>
<div className="space-y-4">
{/* Overall Utilization */}
<div className="text-center">
<div className="flex items-center justify-center space-x-2 mb-2">
<BarChart3 className={`w-6 h-6 ${overallStatus.color}`} />
<span className="text-3xl font-bold text-[var(--text-primary)]">
{overallUtilization.toFixed(1)}%
</span>
</div>
<div className={`inline-flex items-center space-x-2 px-3 py-1 rounded-full text-sm ${overallStatus.bgColor}`}>
<span className={overallStatus.color}>
{t(`status.${overallStatus.status}`)}
</span>
</div>
</div>
{/* Resource Breakdown */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('insights.resource_breakdown')}
</h4>
<div className="space-y-3">
{capacityData.map((resource, index) => {
const status = getUtilizationStatus(resource.utilization);
return (
<div key={index} className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<span className="text-lg">{getResourceIcon(resource.type)}</span>
<span className="text-sm font-medium text-[var(--text-primary)]">
{resource.resource}
</span>
</div>
<span className={`text-sm font-semibold ${status.color}`}>
{resource.utilization}%
</span>
</div>
<ProgressBar
value={resource.utilization}
variant={getProgressBarVariant(resource.utilization)}
size="sm"
className="mb-2"
/>
<div className="flex items-center justify-between text-xs text-[var(--text-secondary)]">
<span>
{resource.allocated}/{resource.capacity} {t('common.allocated')}
</span>
<span>
{resource.available} {t('common.available')}
</span>
</div>
</div>
);
})}
</div>
</div>
{/* Hourly Heatmap */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('insights.hourly_utilization_pattern')}
</h4>
<AnalyticsChart
series={getHourlyUtilizationData()}
height={120}
showLegend={false}
showGrid={true}
/>
</div>
{/* Alerts & Recommendations */}
<div className="space-y-2">
{capacityData.some(r => r.utilization >= 95) && (
<div className="flex items-start space-x-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
<AlertTriangle className="w-4 h-4 mt-0.5 text-red-600" />
<div>
<p className="text-sm font-medium text-red-600">
{t('alerts.capacity_critical')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('alerts.capacity_critical_description')}
</p>
</div>
</div>
)}
{capacityData.some(r => r.utilization >= 85 && r.utilization < 95) && (
<div className="flex items-start space-x-2 p-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<TrendingUp className="w-4 h-4 mt-0.5 text-orange-600" />
<div>
<p className="text-sm font-medium text-orange-600">
{t('alerts.capacity_high')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('alerts.capacity_high_description')}
</p>
</div>
</div>
)}
</div>
{/* Peak Hours Summary */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<div className="grid grid-cols-2 gap-3 text-center text-sm">
<div>
<p className="font-semibold text-[var(--text-primary)]">10:00 - 12:00</p>
<p className="text-xs text-[var(--text-secondary)]">{t('insights.peak_hours')}</p>
</div>
<div>
<p className="font-semibold text-[var(--text-primary)]">14:00 - 16:00</p>
<p className="text-xs text-[var(--text-secondary)]">{t('insights.off_peak_optimal')}</p>
</div>
</div>
</div>
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,296 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { DollarSign, TrendingUp, TrendingDown, PieChart } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button } from '../../../ui';
import { useActiveBatches } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface ProductCostData {
product: string;
estimatedCost: number;
actualCost: number;
variance: number;
variancePercent: number;
units: number;
costPerUnit: number;
}
export const CostPerUnitWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
// Process batches to calculate cost per unit data
const getProductCostData = (): ProductCostData[] => {
const productMap = new Map<string, any>();
batches.forEach(batch => {
const key = batch.product_name;
if (!productMap.has(key)) {
productMap.set(key, {
product: batch.product_name,
estimatedCosts: [],
actualCosts: [],
quantities: []
});
}
const data = productMap.get(key);
if (batch.estimated_cost) data.estimatedCosts.push(batch.estimated_cost);
if (batch.actual_cost) data.actualCosts.push(batch.actual_cost);
if (batch.actual_quantity || batch.planned_quantity) {
data.quantities.push(batch.actual_quantity || batch.planned_quantity);
}
});
return Array.from(productMap.values()).map(data => {
const avgEstimated = data.estimatedCosts.reduce((a: number, b: number) => a + b, 0) / (data.estimatedCosts.length || 1);
const avgActual = data.actualCosts.reduce((a: number, b: number) => a + b, 0) / (data.actualCosts.length || 1);
const totalUnits = data.quantities.reduce((a: number, b: number) => a + b, 0);
const variance = avgActual - avgEstimated;
const variancePercent = avgEstimated > 0 ? (variance / avgEstimated) * 100 : 0;
const costPerUnit = totalUnits > 0 ? avgActual / totalUnits : avgActual;
return {
product: data.product,
estimatedCost: avgEstimated,
actualCost: avgActual,
variance,
variancePercent,
units: totalUnits,
costPerUnit
};
}).filter(item => item.actualCost > 0);
};
const productCostData = getProductCostData();
const totalCosts = productCostData.reduce((sum, item) => sum + item.actualCost, 0);
const averageCostPerUnit = productCostData.length > 0
? productCostData.reduce((sum, item) => sum + item.costPerUnit, 0) / productCostData.length
: 0;
// Create chart data for cost comparison
const getCostComparisonChartData = (): ChartSeries[] => {
if (productCostData.length === 0) return [];
const estimatedData = productCostData.map(item => ({
x: item.product,
y: item.estimatedCost,
label: item.product
}));
const actualData = productCostData.map(item => ({
x: item.product,
y: item.actualCost,
label: item.product
}));
return [
{
id: 'estimated-cost',
name: t('cost.estimated_cost'),
type: 'bar',
color: '#94a3b8',
data: estimatedData
},
{
id: 'actual-cost',
name: t('cost.actual_cost'),
type: 'bar',
color: '#d97706',
data: actualData
}
];
};
// Create pie chart for cost distribution
const getCostDistributionChartData = (): ChartSeries[] => {
if (productCostData.length === 0) return [];
const pieData = productCostData.map((item, index) => ({
x: item.product,
y: item.actualCost,
label: item.product,
color: getProductColor(index)
}));
return [
{
id: 'cost-distribution',
name: t('cost.cost_distribution'),
type: 'pie',
color: '#d97706',
data: pieData
}
];
};
const getProductColor = (index: number): string => {
const colors = ['#d97706', '#0284c7', '#16a34a', '#dc2626', '#7c3aed', '#0891b2'];
return colors[index % colors.length];
};
const getVarianceStatus = (variancePercent: number) => {
if (Math.abs(variancePercent) <= 5) return { color: 'text-green-600', status: 'on_target' };
if (variancePercent > 5) return { color: 'text-red-600', status: 'over_budget' };
return { color: 'text-blue-600', status: 'under_budget' };
};
return (
<AnalyticsWidget
title={t('cost.cost_per_unit_analysis')}
subtitle={t('cost.estimated_vs_actual_costs')}
icon={DollarSign}
loading={isLoading}
error={error?.message}
actions={
<Button variant="outline" size="sm">
<PieChart className="w-4 h-4 mr-1" />
{t('cost.view_breakdown')}
</Button>
}
>
<div className="space-y-4">
{/* Overall Metrics */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-2">
<DollarSign className="w-5 h-5 text-[var(--color-primary)]" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{averageCostPerUnit.toFixed(2)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('cost.average_cost_per_unit')}</p>
</div>
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-2">
<TrendingUp className="w-5 h-5 text-[var(--color-primary)]" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{totalCosts.toFixed(0)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('cost.total_production_cost')}</p>
</div>
</div>
{productCostData.length === 0 ? (
<div className="text-center py-8 text-[var(--text-secondary)]">
<DollarSign className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">{t('cost.no_cost_data_available')}</p>
</div>
) : (
<>
{/* Cost Comparison Chart */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('cost.estimated_vs_actual')}
</h4>
<AnalyticsChart
series={getCostComparisonChartData()}
height={200}
showLegend={true}
showGrid={true}
/>
</div>
{/* Product Cost Details */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('cost.product_cost_breakdown')}
</h4>
<div className="space-y-3 max-h-64 overflow-y-auto">
{productCostData.map((item, index) => {
const varianceStatus = getVarianceStatus(item.variancePercent);
const VarianceIcon = item.variance >= 0 ? TrendingUp : TrendingDown;
return (
<div key={index} className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: getProductColor(index) }}
/>
<span className="font-medium text-[var(--text-primary)]">
{item.product}
</span>
</div>
<span className="text-lg font-bold text-[var(--text-primary)]">
{item.costPerUnit.toFixed(2)}
</span>
</div>
<div className="grid grid-cols-3 gap-3 text-sm">
<div>
<p className="text-[var(--text-secondary)]">{t('cost.estimated')}</p>
<p className="font-semibold text-[var(--text-primary)]">
{item.estimatedCost.toFixed(2)}
</p>
</div>
<div>
<p className="text-[var(--text-secondary)]">{t('cost.actual')}</p>
<p className="font-semibold text-[var(--text-primary)]">
{item.actualCost.toFixed(2)}
</p>
</div>
<div>
<p className="text-[var(--text-secondary)]">{t('cost.variance')}</p>
<div className={`flex items-center space-x-1 ${varianceStatus.color}`}>
<VarianceIcon className="w-3 h-3" />
<span className="font-semibold">
{item.variancePercent > 0 ? '+' : ''}{item.variancePercent.toFixed(1)}%
</span>
</div>
</div>
</div>
<div className="mt-2 pt-2 border-t border-[var(--border-primary)] text-xs text-[var(--text-tertiary)]">
{t('cost.units_produced')}: {item.units}
</div>
</div>
);
})}
</div>
</div>
{/* Cost Distribution Pie Chart */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('cost.cost_distribution')}
</h4>
<AnalyticsChart
series={getCostDistributionChartData()}
height={200}
showLegend={true}
showGrid={false}
/>
</div>
{/* Cost Optimization Insights */}
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<DollarSign className="w-4 h-4 mt-0.5 text-blue-600" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('cost.optimization_opportunity')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{productCostData.some(item => item.variancePercent > 10)
? t('cost.high_variance_detected')
: t('cost.costs_within_expected_range')
}
</p>
</div>
</div>
</div>
</>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,389 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Zap, TrendingUp, TrendingDown, BarChart3, Target } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button } from '../../../ui';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface EquipmentEfficiencyData {
equipmentId: string;
equipmentName: string;
type: 'oven' | 'mixer' | 'proofer' | 'packaging';
currentEfficiency: number;
targetEfficiency: number;
trend: 'up' | 'down' | 'stable';
energyConsumption: number; // kWh
productionOutput: number; // units per hour
oee: number; // Overall Equipment Effectiveness
availability: number;
performance: number;
quality: number;
downtimeMinutes: number;
weeklyData: Array<{
day: string;
efficiency: number;
energyConsumption: number;
output: number;
}>;
}
export const EquipmentEfficiencyWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
// Mock efficiency data - replace with real API call
const efficiencyData: EquipmentEfficiencyData[] = [
{
equipmentId: '1',
equipmentName: 'Horno Principal',
type: 'oven',
currentEfficiency: 94,
targetEfficiency: 95,
trend: 'up',
energyConsumption: 45.2,
productionOutput: 120,
oee: 89,
availability: 95,
performance: 96,
quality: 98,
downtimeMinutes: 15,
weeklyData: [
{ day: 'Lun', efficiency: 92, energyConsumption: 44, output: 118 },
{ day: 'Mar', efficiency: 93, energyConsumption: 45, output: 119 },
{ day: 'Mié', efficiency: 94, energyConsumption: 45.2, output: 120 },
{ day: 'Jue', efficiency: 94, energyConsumption: 45.1, output: 121 },
{ day: 'Vie', efficiency: 95, energyConsumption: 45.3, output: 122 },
{ day: 'Sáb', efficiency: 94, energyConsumption: 44.8, output: 119 },
{ day: 'Dom', efficiency: 93, energyConsumption: 44.5, output: 117 }
]
},
{
equipmentId: '2',
equipmentName: 'Mezcladora A',
type: 'mixer',
currentEfficiency: 87,
targetEfficiency: 90,
trend: 'down',
energyConsumption: 12.5,
productionOutput: 85,
oee: 82,
availability: 92,
performance: 89,
quality: 95,
downtimeMinutes: 45,
weeklyData: [
{ day: 'Lun', efficiency: 89, energyConsumption: 12, output: 88 },
{ day: 'Mar', efficiency: 88, energyConsumption: 12.2, output: 87 },
{ day: 'Mié', efficiency: 87, energyConsumption: 12.5, output: 85 },
{ day: 'Jue', efficiency: 86, energyConsumption: 12.8, output: 84 },
{ day: 'Vie', efficiency: 87, energyConsumption: 12.6, output: 86 },
{ day: 'Sáb', efficiency: 88, energyConsumption: 12.3, output: 87 },
{ day: 'Dom', efficiency: 87, energyConsumption: 12.4, output: 85 }
]
},
{
equipmentId: '3',
equipmentName: 'Cámara de Fermentación 1',
type: 'proofer',
currentEfficiency: 96,
targetEfficiency: 95,
trend: 'stable',
energyConsumption: 8.3,
productionOutput: 95,
oee: 94,
availability: 98,
performance: 97,
quality: 99,
downtimeMinutes: 5,
weeklyData: [
{ day: 'Lun', efficiency: 96, energyConsumption: 8.2, output: 94 },
{ day: 'Mar', efficiency: 96, energyConsumption: 8.3, output: 95 },
{ day: 'Mié', efficiency: 96, energyConsumption: 8.3, output: 95 },
{ day: 'Jue', efficiency: 97, energyConsumption: 8.4, output: 96 },
{ day: 'Vie', efficiency: 96, energyConsumption: 8.3, output: 95 },
{ day: 'Sáb', efficiency: 95, energyConsumption: 8.2, output: 94 },
{ day: 'Dom', efficiency: 96, energyConsumption: 8.3, output: 95 }
]
}
];
const getTrendIcon = (trend: EquipmentEfficiencyData['trend']) => {
switch (trend) {
case 'up': return TrendingUp;
case 'down': return TrendingDown;
case 'stable': return BarChart3;
default: return BarChart3;
}
};
const getTrendColor = (trend: EquipmentEfficiencyData['trend']) => {
switch (trend) {
case 'up': return 'text-green-600';
case 'down': return 'text-red-600';
case 'stable': return 'text-blue-600';
default: return 'text-gray-600';
}
};
const getEfficiencyStatus = (current: number, target: number) => {
const percentage = (current / target) * 100;
if (percentage >= 100) return { status: 'excellent', color: 'text-green-600', bgColor: 'bg-green-100 dark:bg-green-900/20' };
if (percentage >= 90) return { status: 'good', color: 'text-blue-600', bgColor: 'bg-blue-100 dark:bg-blue-900/20' };
if (percentage >= 80) return { status: 'warning', color: 'text-orange-600', bgColor: 'bg-orange-100 dark:bg-orange-900/20' };
return { status: 'critical', color: 'text-red-600', bgColor: 'bg-red-100 dark:bg-red-900/20' };
};
// Calculate overall metrics
const avgEfficiency = efficiencyData.reduce((sum, item) => sum + item.currentEfficiency, 0) / efficiencyData.length;
const avgOEE = efficiencyData.reduce((sum, item) => sum + item.oee, 0) / efficiencyData.length;
const totalEnergyConsumption = efficiencyData.reduce((sum, item) => sum + item.energyConsumption, 0);
const totalDowntime = efficiencyData.reduce((sum, item) => sum + item.downtimeMinutes, 0);
// Create efficiency comparison chart
const getEfficiencyComparisonChartData = (): ChartSeries[] => {
const currentData = efficiencyData.map(item => ({
x: item.equipmentName,
y: item.currentEfficiency,
label: `${item.equipmentName}: ${item.currentEfficiency}%`
}));
const targetData = efficiencyData.map(item => ({
x: item.equipmentName,
y: item.targetEfficiency,
label: `Target: ${item.targetEfficiency}%`
}));
return [
{
id: 'current-efficiency',
name: t('equipment.efficiency.current'),
type: 'bar',
color: '#16a34a',
data: currentData
},
{
id: 'target-efficiency',
name: t('equipment.efficiency.target'),
type: 'bar',
color: '#d97706',
data: targetData
}
];
};
// Create weekly efficiency trend chart
const getWeeklyTrendChartData = (): ChartSeries[] => {
return efficiencyData.map((equipment, index) => ({
id: `weekly-${equipment.equipmentId}`,
name: equipment.equipmentName,
type: 'line',
color: ['#16a34a', '#3b82f6', '#f59e0b'][index % 3],
data: equipment.weeklyData.map(day => ({
x: day.day,
y: day.efficiency,
label: `${day.day}: ${day.efficiency}%`
}))
}));
};
// Create OEE breakdown chart
const getOEEBreakdownChartData = (): ChartSeries[] => {
const avgAvailability = efficiencyData.reduce((sum, item) => sum + item.availability, 0) / efficiencyData.length;
const avgPerformance = efficiencyData.reduce((sum, item) => sum + item.performance, 0) / efficiencyData.length;
const avgQuality = efficiencyData.reduce((sum, item) => sum + item.quality, 0) / efficiencyData.length;
return [
{
id: 'oee-breakdown',
name: 'OEE Components',
type: 'doughnut',
color: '#3b82f6',
data: [
{ x: t('equipment.oee.availability'), y: avgAvailability, label: `${t('equipment.oee.availability')}: ${avgAvailability.toFixed(1)}%` },
{ x: t('equipment.oee.performance'), y: avgPerformance, label: `${t('equipment.oee.performance')}: ${avgPerformance.toFixed(1)}%` },
{ x: t('equipment.oee.quality'), y: avgQuality, label: `${t('equipment.oee.quality')}: ${avgQuality.toFixed(1)}%` }
]
}
];
};
return (
<AnalyticsWidget
title={t('equipment.efficiency.title')}
subtitle={t('equipment.efficiency.subtitle')}
icon={Zap}
actions={
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<BarChart3 className="w-4 h-4 mr-1" />
{t('equipment.efficiency.analyze')}
</Button>
<Button variant="primary" size="sm">
<Target className="w-4 h-4 mr-1" />
{t('equipment.efficiency.optimize')}
</Button>
</div>
}
>
<div className="space-y-6">
{/* Efficiency Overview Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<Zap className="w-8 h-8 mx-auto text-green-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{avgEfficiency.toFixed(1)}%</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.efficiency.average')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<Target className="w-8 h-8 mx-auto text-blue-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{avgOEE.toFixed(1)}%</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.oee.overall')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-orange-100 dark:bg-orange-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-orange-600 font-bold text-sm">kWh</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalEnergyConsumption.toFixed(1)}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.energy_consumption')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-red-600 font-bold text-xs">min</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalDowntime}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.downtime.total')}</p>
</div>
</div>
{/* Equipment Efficiency List */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Zap className="w-4 h-4 mr-2" />
{t('equipment.efficiency.by_equipment')}
</h4>
<div className="space-y-3">
{efficiencyData.map((item) => {
const TrendIcon = getTrendIcon(item.trend);
const trendColor = getTrendColor(item.trend);
const status = getEfficiencyStatus(item.currentEfficiency, item.targetEfficiency);
return (
<div key={item.equipmentId} className="p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<Zap className="w-5 h-5 text-[var(--color-primary)]" />
<div>
<p className="font-medium text-[var(--text-primary)]">{item.equipmentName}</p>
<div className="flex items-center space-x-2 text-xs text-[var(--text-secondary)]">
<span>OEE: {item.oee}%</span>
<span></span>
<span>{item.energyConsumption} kWh</span>
<span></span>
<span>{item.productionOutput} {t('equipment.units_per_hour')}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<TrendIcon className={`w-4 h-4 ${trendColor}`} />
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{item.currentEfficiency}%
</div>
<div className="text-xs text-[var(--text-secondary)]">
{t('equipment.efficiency.target')}: {item.targetEfficiency}%
</div>
</div>
</div>
</div>
{/* Progress bar */}
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2 mb-3">
<div
className="bg-[var(--color-primary)] h-2 rounded-full transition-all duration-300"
style={{ width: `${(item.currentEfficiency / 100) * 100}%` }}
/>
</div>
{/* OEE Components */}
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-lg font-semibold text-green-600">{item.availability}%</p>
<p className="text-xs text-[var(--text-secondary)]">{t('equipment.oee.availability')}</p>
</div>
<div>
<p className="text-lg font-semibold text-blue-600">{item.performance}%</p>
<p className="text-xs text-[var(--text-secondary)]">{t('equipment.oee.performance')}</p>
</div>
<div>
<p className="text-lg font-semibold text-orange-600">{item.quality}%</p>
<p className="text-xs text-[var(--text-secondary)]">{t('equipment.oee.quality')}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Efficiency Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Current vs Target Efficiency */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('equipment.efficiency.current_vs_target')}
</h4>
<AnalyticsChart
series={getEfficiencyComparisonChartData()}
height={200}
showLegend={true}
showGrid={true}
/>
</div>
{/* OEE Breakdown */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('equipment.oee.breakdown')}
</h4>
<AnalyticsChart
series={getOEEBreakdownChartData()}
height={200}
showLegend={true}
showGrid={false}
/>
</div>
</div>
{/* Weekly Efficiency Trends */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('equipment.efficiency.weekly_trends')}
</h4>
<AnalyticsChart
series={getWeeklyTrendChartData()}
height={250}
showLegend={true}
showGrid={true}
/>
</div>
{/* Efficiency Recommendations */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<Target className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('equipment.efficiency.recommendations')}
</p>
<ul className="text-xs text-[var(--text-secondary)] mt-1 space-y-1">
<li> {t('equipment.efficiency.recommendation_1')}: Mezcladora A {t('equipment.efficiency.needs_maintenance')}</li>
<li> {t('equipment.efficiency.recommendation_2')}: {t('equipment.efficiency.optimize_energy_consumption')}</li>
<li> {t('equipment.efficiency.recommendation_3')}: {t('equipment.efficiency.schedule_preventive_maintenance')}</li>
</ul>
</div>
</div>
</div>
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,285 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Settings, AlertTriangle, CheckCircle, Clock, Zap } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Badge, Button } from '../../../ui';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface EquipmentStatus {
id: string;
name: string;
type: 'oven' | 'mixer' | 'proofer' | 'packaging';
status: 'operational' | 'warning' | 'maintenance' | 'down';
efficiency: number;
temperature?: number;
uptime: number;
lastMaintenance: string;
nextMaintenance: string;
alertCount: number;
}
export const EquipmentStatusWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
// Mock equipment data - replace with real API call
const equipment: EquipmentStatus[] = [
{
id: '1',
name: 'Horno Principal',
type: 'oven',
status: 'operational',
efficiency: 94,
temperature: 180,
uptime: 98.5,
lastMaintenance: '2024-01-15',
nextMaintenance: '2024-02-15',
alertCount: 0
},
{
id: '2',
name: 'Mezcladora A',
type: 'mixer',
status: 'warning',
efficiency: 87,
uptime: 85.2,
lastMaintenance: '2024-01-10',
nextMaintenance: '2024-02-10',
alertCount: 2
},
{
id: '3',
name: 'Cámara de Fermentación 1',
type: 'proofer',
status: 'operational',
efficiency: 96,
temperature: 28,
uptime: 99.1,
lastMaintenance: '2024-01-20',
nextMaintenance: '2024-02-20',
alertCount: 0
},
{
id: '4',
name: 'Empaquetadora',
type: 'packaging',
status: 'maintenance',
efficiency: 0,
uptime: 0,
lastMaintenance: '2024-01-25',
nextMaintenance: '2024-01-26',
alertCount: 1
}
];
const getStatusColor = (status: EquipmentStatus['status']) => {
switch (status) {
case 'operational': return 'text-green-600';
case 'warning': return 'text-orange-600';
case 'maintenance': return 'text-blue-600';
case 'down': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getStatusBadgeVariant = (status: EquipmentStatus['status']) => {
switch (status) {
case 'operational': return 'success';
case 'warning': return 'warning';
case 'maintenance': return 'info';
case 'down': return 'error';
default: return 'default';
}
};
const getStatusIcon = (status: EquipmentStatus['status']) => {
switch (status) {
case 'operational': return CheckCircle;
case 'warning': return AlertTriangle;
case 'maintenance': return Clock;
case 'down': return AlertTriangle;
default: return Settings;
}
};
const operationalCount = equipment.filter(e => e.status === 'operational').length;
const warningCount = equipment.filter(e => e.status === 'warning').length;
const maintenanceCount = equipment.filter(e => e.status === 'maintenance').length;
const downCount = equipment.filter(e => e.status === 'down').length;
const avgEfficiency = equipment.reduce((sum, e) => sum + e.efficiency, 0) / equipment.length;
// Equipment efficiency chart data
const getEfficiencyChartData = (): ChartSeries[] => {
const data = equipment.map(item => ({
x: item.name,
y: item.efficiency,
label: `${item.name}: ${item.efficiency}%`
}));
return [
{
id: 'equipment-efficiency',
name: t('equipment.efficiency'),
type: 'bar',
color: '#16a34a',
data
}
];
};
// Status distribution chart
const getStatusDistributionChartData = (): ChartSeries[] => {
const statusData = [
{ x: t('equipment.status.operational'), y: operationalCount, label: `${operationalCount} ${t('equipment.status.operational')}` },
{ x: t('equipment.status.warning'), y: warningCount, label: `${warningCount} ${t('equipment.status.warning')}` },
{ x: t('equipment.status.maintenance'), y: maintenanceCount, label: `${maintenanceCount} ${t('equipment.status.maintenance')}` },
{ x: t('equipment.status.down'), y: downCount, label: `${downCount} ${t('equipment.status.down')}` }
].filter(item => item.y > 0);
return [
{
id: 'status-distribution',
name: t('equipment.status.distribution'),
type: 'doughnut',
color: '#3b82f6',
data: statusData
}
];
};
return (
<AnalyticsWidget
title={t('equipment.title')}
subtitle={t('equipment.subtitle')}
icon={Settings}
actions={
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Clock className="w-4 h-4 mr-1" />
{t('equipment.actions.schedule_maintenance')}
</Button>
<Button variant="primary" size="sm">
<Zap className="w-4 h-4 mr-1" />
{t('actions.optimize')}
</Button>
</div>
}
>
<div className="space-y-6">
{/* Equipment Overview Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<CheckCircle className="w-8 h-8 mx-auto text-green-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{operationalCount}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.stats.operational')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<AlertTriangle className="w-8 h-8 mx-auto text-orange-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{warningCount}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.stats.needs_attention')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-blue-600 font-bold text-lg">{avgEfficiency.toFixed(0)}%</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{avgEfficiency.toFixed(1)}%</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.stats.avg_efficiency')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<AlertTriangle className="w-8 h-8 mx-auto text-red-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">
{equipment.reduce((sum, e) => sum + e.alertCount, 0)}
</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.stats.alerts')}</p>
</div>
</div>
{/* Equipment List */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Settings className="w-4 h-4 mr-2" />
{t('equipment.status.equipment_list')} ({equipment.length})
</h4>
<div className="space-y-3">
{equipment.map((item) => {
const StatusIcon = getStatusIcon(item.status);
return (
<div key={item.id} className="p-4 bg-[var(--bg-secondary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<StatusIcon className={`w-5 h-5 ${getStatusColor(item.status)}`} />
<div>
<p className="font-medium text-[var(--text-primary)]">{item.name}</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
<span>{t('equipment.efficiency')}: {item.efficiency}%</span>
<span>{t('equipment.uptime')}: {item.uptime}%</span>
{item.temperature && (
<span>{t('equipment.temperature')}: {item.temperature}°C</span>
)}
{item.alertCount > 0 && (
<span className="text-red-600">{item.alertCount} {t('equipment.unread_alerts')}</span>
)}
</div>
</div>
</div>
<Badge variant={getStatusBadgeVariant(item.status)}>
{t(`equipment.status.${item.status}`)}
</Badge>
</div>
</div>
);
})}
</div>
</div>
{/* Equipment Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Efficiency Chart */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('equipment.efficiency')} {t('stats.by_equipment')}
</h4>
<AnalyticsChart
series={getEfficiencyChartData()}
height={200}
showLegend={false}
showGrid={true}
/>
</div>
{/* Status Distribution */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('equipment.status.distribution')}
</h4>
<AnalyticsChart
series={getStatusDistributionChartData()}
height={200}
showLegend={true}
showGrid={false}
/>
</div>
</div>
{/* Maintenance Alerts */}
{equipment.some(e => e.alertCount > 0 || e.status === 'maintenance') && (
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<AlertTriangle className="w-5 h-5 text-orange-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-orange-600">
{t('equipment.alerts.maintenance_required')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{equipment.filter(e => e.status === 'maintenance').length} {t('equipment.alerts.equipment_in_maintenance')},
{equipment.reduce((sum, e) => sum + e.alertCount, 0)} {t('equipment.alerts.active_alerts')}
</p>
</div>
</div>
</div>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,259 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Activity, Clock, AlertCircle, CheckCircle, Play, Pause } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { Badge, ProgressBar, Button } from '../../../ui';
import { useActiveBatches } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { ProductionStatus } from '../../../../api/types/production';
export const LiveBatchTrackerWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
const getStatusIcon = (status: ProductionStatus) => {
switch (status) {
case ProductionStatus.COMPLETED:
return CheckCircle;
case ProductionStatus.IN_PROGRESS:
return Play;
case ProductionStatus.ON_HOLD:
return Pause;
case ProductionStatus.FAILED:
return AlertCircle;
default:
return Clock;
}
};
const getStatusBadgeVariant = (status: ProductionStatus) => {
switch (status) {
case ProductionStatus.COMPLETED:
return 'success';
case ProductionStatus.IN_PROGRESS:
return 'info';
case ProductionStatus.PENDING:
return 'warning';
case ProductionStatus.ON_HOLD:
return 'warning';
case ProductionStatus.CANCELLED:
case ProductionStatus.FAILED:
return 'error';
default:
return 'default';
}
};
const calculateProgress = (batch: any) => {
if (batch.status === ProductionStatus.COMPLETED) return 100;
if (batch.status === ProductionStatus.PENDING) return 0;
if (batch.actual_start_time && batch.planned_duration_minutes) {
const startTime = new Date(batch.actual_start_time).getTime();
const now = new Date().getTime();
const elapsed = (now - startTime) / (1000 * 60); // minutes
const progress = Math.min((elapsed / batch.planned_duration_minutes) * 100, 100);
return Math.round(progress);
}
return 25; // Default for in-progress without timing info
};
const calculateETA = (batch: any) => {
if (batch.status === ProductionStatus.COMPLETED) return null;
if (!batch.actual_start_time || !batch.planned_duration_minutes) return null;
const startTime = new Date(batch.actual_start_time).getTime();
const eta = new Date(startTime + batch.planned_duration_minutes * 60 * 1000);
const now = new Date();
if (eta < now) {
const delay = Math.round((now.getTime() - eta.getTime()) / (1000 * 60));
return `${delay}m ${t('schedule.delayed')}`;
}
return eta.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
};
const getProgressBarVariant = (status: ProductionStatus, progress: number) => {
if (status === ProductionStatus.COMPLETED) return 'success';
if (status === ProductionStatus.FAILED || status === ProductionStatus.CANCELLED) return 'error';
if (progress > 90) return 'warning';
return 'info';
};
return (
<AnalyticsWidget
title={t('tracker.live_batch_tracker')}
subtitle={t('tracker.current_batches_status_progress')}
icon={Activity}
loading={isLoading}
error={error?.message}
actions={
<Button variant="outline" size="sm">
<Activity className="w-4 h-4 mr-1" />
{t('actions.refresh')}
</Button>
}
>
<div className="space-y-3">
{batches.length === 0 ? (
<div className="text-center py-8 text-[var(--text-secondary)]">
<Activity className="w-12 h-12 mx-auto mb-3 opacity-50" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
{t('messages.no_active_batches')}
</h3>
<p className="text-sm">{t('messages.no_active_batches_description')}</p>
</div>
) : (
<div className="space-y-3 max-h-96 overflow-y-auto">
{batches.map((batch) => {
const StatusIcon = getStatusIcon(batch.status);
const progress = calculateProgress(batch);
const eta = calculateETA(batch);
return (
<div
key={batch.id}
className="p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-primary)] hover:shadow-md transition-all"
>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-start space-x-3 flex-1">
<div className="w-8 h-8 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center mt-0.5">
<StatusIcon className="w-4 h-4 text-[var(--color-primary)]" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
{batch.product_name}
</h4>
<p className="text-sm text-[var(--text-secondary)] mb-1">
{t('batch.batch_number')}: {batch.batch_number}
</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-tertiary)]">
<span>{batch.planned_quantity} {t('common.units')}</span>
{batch.actual_quantity && (
<span>
{t('batch.actual')}: {batch.actual_quantity} {t('common.units')}
</span>
)}
</div>
</div>
</div>
<Badge variant={getStatusBadgeVariant(batch.status)}>
{t(`status.${batch.status.toLowerCase()}`)}
</Badge>
</div>
{/* Progress Bar */}
{batch.status !== ProductionStatus.PENDING && (
<div className="mb-3">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-[var(--text-secondary)]">
{t('tracker.progress')}
</span>
<span className="text-xs font-medium text-[var(--text-primary)]">
{progress}%
</span>
</div>
<ProgressBar
value={progress}
variant={getProgressBarVariant(batch.status, progress)}
size="sm"
/>
</div>
)}
{/* Details */}
<div className="flex items-center justify-between text-xs">
<div className="flex items-center space-x-4">
{batch.priority && (
<span className={`font-medium ${
batch.priority === 'URGENT' ? 'text-red-600' :
batch.priority === 'HIGH' ? 'text-orange-600' :
batch.priority === 'MEDIUM' ? 'text-blue-600' :
'text-gray-600'
}`}>
{t(`priority.${batch.priority.toLowerCase()}`)}
</span>
)}
{batch.is_rush_order && (
<span className="text-red-600 font-medium">
{t('batch.rush_order')}
</span>
)}
</div>
<div className="flex items-center space-x-1 text-[var(--text-secondary)]">
<Clock className="w-3 h-3" />
<span>
{eta ? (
<span className={eta.includes('delayed') ? 'text-red-600' : ''}>
ETA: {eta}
</span>
) : (
t('tracker.pending_start')
)}
</span>
</div>
</div>
{/* Equipment & Staff */}
{(batch.equipment_used?.length > 0 || batch.staff_assigned?.length > 0) && (
<div className="mt-2 pt-2 border-t border-[var(--border-primary)] text-xs text-[var(--text-tertiary)]">
{batch.equipment_used?.length > 0 && (
<div className="mb-1">
{t('batch.equipment')}: {batch.equipment_used.join(', ')}
</div>
)}
{batch.staff_assigned?.length > 0 && (
<div>
{t('batch.staff')}: {batch.staff_assigned.length} {t('common.assigned')}
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
{/* Summary */}
{batches.length > 0 && (
<div className="pt-3 border-t border-[var(--border-primary)]">
<div className="grid grid-cols-4 gap-4 text-center text-xs">
<div>
<p className="font-medium text-green-600">
{batches.filter(b => b.status === ProductionStatus.COMPLETED).length}
</p>
<p className="text-[var(--text-tertiary)]">{t('status.completed')}</p>
</div>
<div>
<p className="font-medium text-blue-600">
{batches.filter(b => b.status === ProductionStatus.IN_PROGRESS).length}
</p>
<p className="text-[var(--text-tertiary)]">{t('status.in_progress')}</p>
</div>
<div>
<p className="font-medium text-orange-600">
{batches.filter(b => b.status === ProductionStatus.PENDING).length}
</p>
<p className="text-[var(--text-tertiary)]">{t('status.pending')}</p>
</div>
<div>
<p className="font-medium text-red-600">
{batches.filter(b => [ProductionStatus.FAILED, ProductionStatus.CANCELLED].includes(b.status)).length}
</p>
<p className="text-[var(--text-tertiary)]">{t('tracker.issues')}</p>
</div>
</div>
</div>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,305 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Calendar, Clock, Wrench, AlertCircle, CheckCircle2 } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { Badge, Button } from '../../../ui';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface MaintenanceTask {
id: string;
equipmentId: string;
equipmentName: string;
type: 'preventive' | 'corrective' | 'inspection';
priority: 'low' | 'medium' | 'high' | 'urgent';
status: 'scheduled' | 'in_progress' | 'completed' | 'overdue';
scheduledDate: string;
completedDate?: string;
estimatedDuration: number; // in hours
actualDuration?: number;
technician?: string;
description: string;
cost?: number;
}
export const MaintenanceScheduleWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
// Mock maintenance data - replace with real API call
const maintenanceTasks: MaintenanceTask[] = [
{
id: '1',
equipmentId: '1',
equipmentName: 'Horno Principal',
type: 'preventive',
priority: 'high',
status: 'scheduled',
scheduledDate: '2024-01-28',
estimatedDuration: 4,
description: 'Limpieza profunda y calibración de temperatura',
cost: 150
},
{
id: '2',
equipmentId: '2',
equipmentName: 'Mezcladora A',
type: 'corrective',
priority: 'urgent',
status: 'overdue',
scheduledDate: '2024-01-25',
estimatedDuration: 2,
description: 'Reparación de motor y reemplazo de correas',
cost: 280
},
{
id: '3',
equipmentId: '3',
equipmentName: 'Cámara de Fermentación 1',
type: 'inspection',
priority: 'medium',
status: 'in_progress',
scheduledDate: '2024-01-26',
estimatedDuration: 1,
technician: 'Carlos López',
description: 'Inspección rutinaria de sistemas de control',
cost: 80
},
{
id: '4',
equipmentId: '4',
equipmentName: 'Empaquetadora',
type: 'preventive',
priority: 'medium',
status: 'completed',
scheduledDate: '2024-01-24',
completedDate: '2024-01-24',
estimatedDuration: 3,
actualDuration: 2.5,
technician: 'Ana García',
description: 'Mantenimiento preventivo mensual',
cost: 120
},
{
id: '5',
equipmentId: '1',
equipmentName: 'Horno Principal',
type: 'inspection',
priority: 'low',
status: 'scheduled',
scheduledDate: '2024-02-05',
estimatedDuration: 0.5,
description: 'Inspección de seguridad semanal'
}
];
const getStatusColor = (status: MaintenanceTask['status']) => {
switch (status) {
case 'completed': return 'text-green-600';
case 'in_progress': return 'text-blue-600';
case 'scheduled': return 'text-orange-600';
case 'overdue': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getStatusBadgeVariant = (status: MaintenanceTask['status']) => {
switch (status) {
case 'completed': return 'success';
case 'in_progress': return 'info';
case 'scheduled': return 'warning';
case 'overdue': return 'error';
default: return 'default';
}
};
const getPriorityColor = (priority: MaintenanceTask['priority']) => {
switch (priority) {
case 'urgent': return 'text-red-600';
case 'high': return 'text-orange-600';
case 'medium': return 'text-blue-600';
case 'low': return 'text-gray-600';
default: return 'text-gray-600';
}
};
const getTypeIcon = (type: MaintenanceTask['type']) => {
switch (type) {
case 'preventive': return Calendar;
case 'corrective': return Wrench;
case 'inspection': return CheckCircle2;
default: return Wrench;
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const scheduledTasks = maintenanceTasks.filter(t => t.status === 'scheduled');
const overdueTasks = maintenanceTasks.filter(t => t.status === 'overdue');
const inProgressTasks = maintenanceTasks.filter(t => t.status === 'in_progress');
const completedTasks = maintenanceTasks.filter(t => t.status === 'completed');
const totalCost = maintenanceTasks.reduce((sum, task) => sum + (task.cost || 0), 0);
const avgDuration = maintenanceTasks.reduce((sum, task) => sum + task.estimatedDuration, 0) / maintenanceTasks.length;
return (
<AnalyticsWidget
title={t('equipment.maintenance.title')}
subtitle={t('equipment.maintenance.subtitle')}
icon={Calendar}
actions={
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Calendar className="w-4 h-4 mr-1" />
{t('equipment.actions.schedule_maintenance')}
</Button>
<Button variant="primary" size="sm">
<Wrench className="w-4 h-4 mr-1" />
{t('equipment.actions.add_task')}
</Button>
</div>
}
>
<div className="space-y-6">
{/* Maintenance Overview Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<Calendar className="w-8 h-8 mx-auto text-orange-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{scheduledTasks.length}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.maintenance.scheduled')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<AlertCircle className="w-8 h-8 mx-auto text-red-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{overdueTasks.length}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.maintenance.overdue')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<Clock className="w-8 h-8 mx-auto text-blue-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{avgDuration.toFixed(1)}h</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.maintenance.avg_duration')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-green-600 font-bold text-sm"></span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalCost}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('equipment.maintenance.total_cost')}</p>
</div>
</div>
{/* Priority Tasks */}
{overdueTasks.length > 0 && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-600">
{t('equipment.maintenance.overdue_tasks')} ({overdueTasks.length})
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('equipment.maintenance.immediate_attention_required')}
</p>
</div>
</div>
</div>
)}
{/* Maintenance Tasks List */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Wrench className="w-4 h-4 mr-2" />
{t('equipment.maintenance.tasks')} ({maintenanceTasks.length})
</h4>
<div className="space-y-3">
{maintenanceTasks.map((task) => {
const TypeIcon = getTypeIcon(task.type);
return (
<div key={task.id} className="p-4 bg-[var(--bg-secondary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
<TypeIcon className={`w-5 h-5 mt-1 ${getStatusColor(task.status)}`} />
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<p className="font-medium text-[var(--text-primary)]">{task.equipmentName}</p>
<span className={`text-xs font-medium ${getPriorityColor(task.priority)}`}>
{t(`priority.${task.priority}`)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-2">{task.description}</p>
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)]">
<span>{t('equipment.maintenance.scheduled')}: {formatDate(task.scheduledDate)}</span>
<span>{t('equipment.maintenance.duration')}: {task.estimatedDuration}h</span>
{task.cost && <span>{t('equipment.maintenance.cost')}: {task.cost}</span>}
{task.technician && <span>{t('equipment.maintenance.technician')}: {task.technician}</span>}
</div>
</div>
</div>
<div className="flex flex-col items-end space-y-2">
<Badge variant={getStatusBadgeVariant(task.status)}>
{t(`equipment.maintenance.status.${task.status}`)}
</Badge>
<Badge variant="outline" className="text-xs">
{t(`equipment.maintenance.type.${task.type}`)}
</Badge>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Weekly Schedule Preview */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Calendar className="w-4 h-4 mr-2" />
{t('equipment.maintenance.this_week')}
</h4>
<div className="grid grid-cols-7 gap-1">
{Array.from({ length: 7 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() + i);
const dateStr = date.toISOString().split('T')[0];
const tasksForDay = maintenanceTasks.filter(task => task.scheduledDate === dateStr);
return (
<div key={i} className="p-2 bg-[var(--bg-secondary)] rounded text-center">
<p className="text-xs text-[var(--text-secondary)] mb-1">
{date.toLocaleDateString('es-ES', { weekday: 'short' })}
</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
{date.getDate()}
</p>
{tasksForDay.length > 0 && (
<div className="w-2 h-2 bg-orange-500 rounded-full mx-auto mt-1"></div>
)}
</div>
);
})}
</div>
</div>
{/* Maintenance Insights */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<CheckCircle2 className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('equipment.maintenance.insights.title')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{completedTasks.length} {t('equipment.maintenance.insights.completed_this_month')},
{scheduledTasks.length} {t('equipment.maintenance.insights.scheduled_next_week')}
</p>
</div>
</div>
</div>
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,195 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Clock, TrendingUp, TrendingDown, Target } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button } from '../../../ui';
import { useProductionDashboard } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
export const OnTimeCompletionWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: dashboard, isLoading, error } = useProductionDashboard(tenantId);
// Mock historical data for the chart (in a real implementation, this would come from an analytics API)
const getHistoricalCompletionData = (): ChartSeries[] => {
const mockData = [
{ x: 'Lun', y: 92, label: 'Lunes' },
{ x: 'Mar', y: 89, label: 'Martes' },
{ x: 'Mié', y: 95, label: 'Miércoles' },
{ x: 'Jue', y: 87, label: 'Jueves' },
{ x: 'Vie', y: 94, label: 'Viernes' },
{ x: 'Sáb', y: 98, label: 'Sábado' },
{ x: 'Dom', y: dashboard?.on_time_completion_rate || 91, label: 'Domingo' },
];
return [
{
id: 'completion-rate',
name: t('stats.on_time_completion'),
type: 'line',
color: '#10b981',
data: mockData
}
];
};
const currentRate = dashboard?.on_time_completion_rate || 0;
const targetRate = 95; // Target completion rate
const weekAverage = 93; // Mock week average
const trend = currentRate - weekAverage;
const getCompletionRateStatus = (rate: number) => {
if (rate >= 95) return { color: 'text-green-600', icon: TrendingUp, status: 'excellent' };
if (rate >= 90) return { color: 'text-blue-600', icon: TrendingUp, status: 'good' };
if (rate >= 85) return { color: 'text-orange-600', icon: TrendingDown, status: 'warning' };
return { color: 'text-red-600', icon: TrendingDown, status: 'critical' };
};
const rateStatus = getCompletionRateStatus(currentRate);
const RateIcon = rateStatus.icon;
const getPerformanceInsight = () => {
if (currentRate >= targetRate) {
return {
message: t('insights.on_time_excellent'),
color: 'text-green-600',
bgColor: 'bg-green-50 dark:bg-green-900/20'
};
} else if (currentRate >= 90) {
return {
message: t('insights.on_time_good_room_improvement'),
color: 'text-blue-600',
bgColor: 'bg-blue-50 dark:bg-blue-900/20'
};
} else if (currentRate >= 85) {
return {
message: t('insights.on_time_needs_attention'),
color: 'text-orange-600',
bgColor: 'bg-orange-50 dark:bg-orange-900/20'
};
} else {
return {
message: t('insights.on_time_critical_delays'),
color: 'text-red-600',
bgColor: 'bg-red-50 dark:bg-red-900/20'
};
}
};
const insight = getPerformanceInsight();
return (
<AnalyticsWidget
title={t('stats.on_time_completion_rate')}
subtitle={t('insights.on_time_vs_planned')}
icon={Clock}
loading={isLoading}
error={error?.message}
actions={
<Button variant="outline" size="sm">
<Target className="w-4 h-4 mr-1" />
{t('actions.analyze_delays')}
</Button>
}
>
<div className="space-y-4">
{/* Current Rate Display */}
<div className="text-center">
<div className="flex items-center justify-center space-x-2 mb-2">
<RateIcon className={`w-6 h-6 ${rateStatus.color}`} />
<span className="text-3xl font-bold text-[var(--text-primary)]">
{currentRate.toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-center space-x-4 text-sm">
<div className={`flex items-center space-x-1 ${trend >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{trend >= 0 ? (
<TrendingUp className="w-4 h-4" />
) : (
<TrendingDown className="w-4 h-4" />
)}
<span>{Math.abs(trend).toFixed(1)}% {t('insights.vs_week_avg')}</span>
</div>
<div className="text-[var(--text-secondary)]">
{t('insights.target')}: {targetRate}%
</div>
</div>
</div>
{/* Progress towards target */}
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-[var(--text-secondary)]">{t('insights.progress_to_target')}</span>
<span className="text-sm font-medium text-[var(--text-primary)]">
{Math.min(currentRate, targetRate).toFixed(1)}% / {targetRate}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
currentRate >= targetRate ? 'bg-green-500' :
currentRate >= 90 ? 'bg-blue-500' :
currentRate >= 85 ? 'bg-orange-500' : 'bg-red-500'
}`}
style={{ width: `${Math.min((currentRate / targetRate) * 100, 100)}%` }}
/>
</div>
</div>
{/* Weekly Trend Chart */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('insights.weekly_trend')}
</h4>
<AnalyticsChart
series={getHistoricalCompletionData()}
height={150}
showLegend={false}
showGrid={true}
/>
</div>
{/* Performance Insight */}
<div className={`p-3 rounded-lg ${insight.bgColor}`}>
<div className="flex items-start space-x-2">
<Clock className={`w-4 h-4 mt-0.5 ${insight.color}`} />
<div>
<p className={`text-sm font-medium ${insight.color}`}>
{t('insights.performance_insight')}
</p>
<p className="text-sm text-[var(--text-secondary)] mt-1">
{insight.message}
</p>
</div>
</div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-3 gap-3 text-center">
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-[var(--text-primary)]">
{Math.round(currentRate * 0.85)}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('insights.batches_on_time')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-[var(--text-primary)]">
{Math.round((100 - currentRate) * 0.15)}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('insights.batches_delayed')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-[var(--text-primary)]">
{Math.round(12 * (100 - currentRate) / 100)}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('insights.avg_delay_minutes')}</p>
</div>
</div>
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,437 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Brain, AlertTriangle, TrendingDown, Calendar, Wrench, Target } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Badge, Button } from '../../../ui';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface PredictiveMaintenanceAlert {
id: string;
equipmentId: string;
equipmentName: string;
equipmentType: 'oven' | 'mixer' | 'proofer' | 'packaging';
alertType: 'wear_prediction' | 'failure_risk' | 'performance_degradation' | 'component_replacement';
severity: 'low' | 'medium' | 'high' | 'critical';
confidence: number; // percentage
predictedFailureDate: string;
currentCondition: number; // percentage (100% = perfect, 0% = failed)
degradationRate: number; // percentage per week
affectedComponents: string[];
recommendedActions: string[];
estimatedCost: number;
potentialDowntime: number; // hours
riskScore: number; // 0-100
dataPoints: Array<{
date: string;
condition: number;
vibration?: number;
temperature?: number;
efficiency?: number;
}>;
}
export const PredictiveMaintenanceWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
// Mock predictive maintenance data - replace with real ML API call
const maintenanceAlerts: PredictiveMaintenanceAlert[] = [
{
id: '1',
equipmentId: '2',
equipmentName: 'Mezcladora A',
equipmentType: 'mixer',
alertType: 'wear_prediction',
severity: 'high',
confidence: 94,
predictedFailureDate: '2024-02-15',
currentCondition: 78,
degradationRate: 3.2,
affectedComponents: ['Motor principal', 'Correas de transmisión', 'Rodamientos'],
recommendedActions: [
'Reemplazar correas de transmisión',
'Lubricar rodamientos',
'Inspeccionar motor'
],
estimatedCost: 280,
potentialDowntime: 4,
riskScore: 85,
dataPoints: [
{ date: '2024-01-01', condition: 95, vibration: 1.2, temperature: 35, efficiency: 94 },
{ date: '2024-01-07', condition: 91, vibration: 1.5, temperature: 37, efficiency: 92 },
{ date: '2024-01-14', condition: 87, vibration: 1.8, temperature: 39, efficiency: 89 },
{ date: '2024-01-21', condition: 83, vibration: 2.1, temperature: 41, efficiency: 87 },
{ date: '2024-01-28', condition: 78, vibration: 2.4, temperature: 43, efficiency: 85 }
]
},
{
id: '2',
equipmentId: '1',
equipmentName: 'Horno Principal',
equipmentType: 'oven',
alertType: 'performance_degradation',
severity: 'medium',
confidence: 87,
predictedFailureDate: '2024-03-10',
currentCondition: 89,
degradationRate: 1.8,
affectedComponents: ['Sistema de calefacción', 'Sensores de temperatura'],
recommendedActions: [
'Calibrar sensores de temperatura',
'Limpiar sistema de calefacción',
'Verificar aislamiento térmico'
],
estimatedCost: 150,
potentialDowntime: 2,
riskScore: 65,
dataPoints: [
{ date: '2024-01-01', condition: 98, temperature: 180, efficiency: 96 },
{ date: '2024-01-07', condition: 95, temperature: 178, efficiency: 95 },
{ date: '2024-01-14', condition: 93, temperature: 176, efficiency: 94 },
{ date: '2024-01-21', condition: 91, temperature: 174, efficiency: 93 },
{ date: '2024-01-28', condition: 89, temperature: 172, efficiency: 92 }
]
},
{
id: '3',
equipmentId: '4',
equipmentName: 'Empaquetadora',
equipmentType: 'packaging',
alertType: 'component_replacement',
severity: 'low',
confidence: 76,
predictedFailureDate: '2024-04-20',
currentCondition: 92,
degradationRate: 1.1,
affectedComponents: ['Cinta transportadora', 'Sistema de sellado'],
recommendedActions: [
'Inspeccionar cinta transportadora',
'Ajustar sistema de sellado',
'Reemplazar filtros'
],
estimatedCost: 120,
potentialDowntime: 1.5,
riskScore: 35,
dataPoints: [
{ date: '2024-01-01', condition: 98, efficiency: 97 },
{ date: '2024-01-07', condition: 96, efficiency: 96 },
{ date: '2024-01-14', condition: 95, efficiency: 95 },
{ date: '2024-01-21', condition: 94, efficiency: 94 },
{ date: '2024-01-28', condition: 92, efficiency: 93 }
]
}
];
const getSeverityColor = (severity: PredictiveMaintenanceAlert['severity']) => {
switch (severity) {
case 'critical': return 'text-red-600';
case 'high': return 'text-orange-600';
case 'medium': return 'text-yellow-600';
case 'low': return 'text-blue-600';
default: return 'text-gray-600';
}
};
const getSeverityBadgeVariant = (severity: PredictiveMaintenanceAlert['severity']) => {
switch (severity) {
case 'critical': return 'error';
case 'high': return 'warning';
case 'medium': return 'warning';
case 'low': return 'info';
default: return 'default';
}
};
const getAlertTypeIcon = (type: PredictiveMaintenanceAlert['alertType']) => {
switch (type) {
case 'wear_prediction': return TrendingDown;
case 'failure_risk': return AlertTriangle;
case 'performance_degradation': return Target;
case 'component_replacement': return Wrench;
default: return AlertTriangle;
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const getDaysUntilFailure = (failureDate: string) => {
const today = new Date();
const failure = new Date(failureDate);
const diffTime = failure.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
const criticalAlerts = maintenanceAlerts.filter(alert => alert.severity === 'critical');
const highRiskAlerts = maintenanceAlerts.filter(alert => alert.riskScore > 70);
const totalEstimatedCost = maintenanceAlerts.reduce((sum, alert) => sum + alert.estimatedCost, 0);
const totalPotentialDowntime = maintenanceAlerts.reduce((sum, alert) => sum + alert.potentialDowntime, 0);
const avgConfidence = maintenanceAlerts.reduce((sum, alert) => sum + alert.confidence, 0) / maintenanceAlerts.length;
// Create equipment condition trend chart
const getConditionTrendChartData = (): ChartSeries[] => {
return maintenanceAlerts.map((alert, index) => ({
id: `condition-${alert.equipmentId}`,
name: alert.equipmentName,
type: 'line',
color: ['#ef4444', '#f97316', '#eab308'][index % 3],
data: alert.dataPoints.map(point => ({
x: new Date(point.date).toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }),
y: point.condition,
label: `${point.condition}%`
}))
}));
};
// Create risk score distribution chart
const getRiskDistributionChartData = (): ChartSeries[] => {
const lowRisk = maintenanceAlerts.filter(a => a.riskScore <= 40).length;
const mediumRisk = maintenanceAlerts.filter(a => a.riskScore > 40 && a.riskScore <= 70).length;
const highRisk = maintenanceAlerts.filter(a => a.riskScore > 70).length;
return [
{
id: 'risk-distribution',
name: 'Risk Distribution',
type: 'doughnut',
color: '#3b82f6',
data: [
{ x: t('ai.risk.low'), y: lowRisk, label: `${t('ai.risk.low')}: ${lowRisk}` },
{ x: t('ai.risk.medium'), y: mediumRisk, label: `${t('ai.risk.medium')}: ${mediumRisk}` },
{ x: t('ai.risk.high'), y: highRisk, label: `${t('ai.risk.high')}: ${highRisk}` }
].filter(item => item.y > 0)
}
];
};
return (
<AnalyticsWidget
title={t('ai.predictive_maintenance.title')}
subtitle={t('ai.predictive_maintenance.subtitle')}
icon={Brain}
actions={
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Calendar className="w-4 h-4 mr-1" />
{t('ai.predictive_maintenance.schedule_all')}
</Button>
<Button variant="primary" size="sm">
<Brain className="w-4 h-4 mr-1" />
{t('ai.predictive_maintenance.retrain_model')}
</Button>
</div>
}
>
<div className="space-y-6">
{/* Predictive Maintenance Overview Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<AlertTriangle className="w-8 h-8 mx-auto text-red-600 mb-2" />
<p className="text-2xl font-bold text-[var(--text-primary)]">{highRiskAlerts.length}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.predictive_maintenance.high_risk')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-orange-100 dark:bg-orange-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-orange-600 font-bold text-sm"></span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalEstimatedCost}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.predictive_maintenance.estimated_cost')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-red-600 font-bold text-xs">h</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{totalPotentialDowntime}</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.predictive_maintenance.potential_downtime')}</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="w-8 h-8 mx-auto bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mb-2">
<span className="text-blue-600 font-bold text-sm">%</span>
</div>
<p className="text-2xl font-bold text-[var(--text-primary)]">{avgConfidence.toFixed(0)}%</p>
<p className="text-sm text-[var(--text-secondary)]">{t('ai.predictive_maintenance.avg_confidence')}</p>
</div>
</div>
{/* Critical Alerts */}
{highRiskAlerts.length > 0 && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<AlertTriangle className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-600">
{t('ai.predictive_maintenance.high_risk_equipment')} ({highRiskAlerts.length})
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('ai.predictive_maintenance.immediate_attention_required')}
</p>
</div>
</div>
</div>
)}
{/* Maintenance Predictions */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Brain className="w-4 h-4 mr-2" />
{t('ai.predictive_maintenance.predictions')} ({maintenanceAlerts.length})
</h4>
<div className="space-y-4">
{maintenanceAlerts.map((alert) => {
const AlertTypeIcon = getAlertTypeIcon(alert.alertType);
const daysUntilFailure = getDaysUntilFailure(alert.predictedFailureDate);
return (
<div key={alert.id} className="p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-start justify-between mb-3">
<div className="flex items-start space-x-3">
<AlertTypeIcon className={`w-5 h-5 mt-1 ${getSeverityColor(alert.severity)}`} />
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<p className="font-medium text-[var(--text-primary)]">{alert.equipmentName}</p>
<Badge variant={getSeverityBadgeVariant(alert.severity)}>
{t(`ai.severity.${alert.severity}`)}
</Badge>
</div>
<div className="flex items-center space-x-4 text-xs text-[var(--text-secondary)] mb-2">
<span>{t('ai.predictive_maintenance.confidence')}: {alert.confidence}%</span>
<span>{t('ai.predictive_maintenance.risk_score')}: {alert.riskScore}/100</span>
<span>{t('ai.predictive_maintenance.days_until_failure')}: {daysUntilFailure}</span>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-2">
{t(`ai.predictive_maintenance.alert_type.${alert.alertType}`)}
</p>
</div>
</div>
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">{alert.currentCondition}%</div>
<div className="text-xs text-[var(--text-secondary)]">{t('ai.predictive_maintenance.condition')}</div>
</div>
</div>
{/* Condition Progress Bar */}
<div className="mb-3">
<div className="flex justify-between text-xs text-[var(--text-secondary)] mb-1">
<span>{t('ai.predictive_maintenance.current_condition')}</span>
<span>{alert.currentCondition}%</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
alert.currentCondition > 80 ? 'bg-green-500' :
alert.currentCondition > 60 ? 'bg-yellow-500' :
alert.currentCondition > 40 ? 'bg-orange-500' : 'bg-red-500'
}`}
style={{ width: `${alert.currentCondition}%` }}
/>
</div>
</div>
{/* Affected Components */}
<div className="mb-3">
<p className="text-xs font-medium text-[var(--text-primary)] mb-1">
{t('ai.predictive_maintenance.affected_components')}:
</p>
<div className="flex flex-wrap gap-1">
{alert.affectedComponents.map((component, index) => (
<Badge key={index} variant="outline" className="text-xs">
{component}
</Badge>
))}
</div>
</div>
{/* Recommended Actions */}
<div className="mb-3">
<p className="text-xs font-medium text-[var(--text-primary)] mb-1">
{t('ai.predictive_maintenance.recommended_actions')}:
</p>
<ul className="text-xs text-[var(--text-secondary)] space-y-1">
{alert.recommendedActions.map((action, index) => (
<li key={index}> {action}</li>
))}
</ul>
</div>
{/* Cost and Downtime */}
<div className="flex items-center justify-between p-3 bg-[var(--bg-tertiary)] rounded-lg">
<div className="flex items-center space-x-4 text-xs">
<span className="flex items-center space-x-1">
<span className="w-2 h-2 bg-orange-500 rounded-full"></span>
<span>{t('ai.predictive_maintenance.estimated_cost')}: {alert.estimatedCost}</span>
</span>
<span className="flex items-center space-x-1">
<span className="w-2 h-2 bg-red-500 rounded-full"></span>
<span>{t('ai.predictive_maintenance.potential_downtime')}: {alert.potentialDowntime}h</span>
</span>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
{t('ai.predictive_maintenance.schedule')}
</Button>
<Button variant="primary" size="sm">
{t('ai.predictive_maintenance.details')}
</Button>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Equipment Condition Trends */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('ai.predictive_maintenance.condition_trends')}
</h4>
<AnalyticsChart
series={getConditionTrendChartData()}
height={200}
showLegend={true}
showGrid={true}
/>
</div>
{/* Risk Distribution */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('ai.predictive_maintenance.risk_distribution')}
</h4>
<AnalyticsChart
series={getRiskDistributionChartData()}
height={200}
showLegend={true}
showGrid={false}
/>
</div>
</div>
{/* ML Model Status */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<Brain className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('ai.predictive_maintenance.model_status')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('ai.predictive_maintenance.last_training')}: {t('ai.predictive_maintenance.yesterday')}.
{t('ai.predictive_maintenance.accuracy')}: 94%. {t('ai.predictive_maintenance.next_training')}: {t('ai.predictive_maintenance.in_7_days')}.
</p>
</div>
</div>
</div>
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,257 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Star, TrendingUp, TrendingDown, Award } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button } from '../../../ui';
import { useActiveBatches } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
export const QualityScoreTrendsWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
// Calculate quality score data
const getQualityData = () => {
const qualityScores = batches
.filter(batch => batch.quality_score)
.map(batch => batch.quality_score!);
if (qualityScores.length === 0) {
return {
averageScore: 0,
totalChecks: 0,
passRate: 0,
trend: 0
};
}
const averageScore = qualityScores.reduce((sum, score) => sum + score, 0) / qualityScores.length;
const totalChecks = qualityScores.length;
const passRate = (qualityScores.filter(score => score >= 7).length / totalChecks) * 100;
// Mock trend calculation (would be compared with previous period in real implementation)
const trend = 2.3; // +2.3 points vs last week
return {
averageScore,
totalChecks,
passRate,
trend
};
};
const qualityData = getQualityData();
// Create daily quality trend chart
const getQualityTrendChartData = (): ChartSeries[] => {
const days = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
const scores = days.map((day, index) => ({
x: day,
y: Math.max(6, 8.5 + Math.sin(index) * 0.8 + (Math.random() - 0.5) * 0.5),
label: day
}));
return [
{
id: 'quality-trend',
name: t('quality.daily_quality_score'),
type: 'line',
color: '#16a34a',
data: scores
}
];
};
// Create quality distribution chart
const getQualityDistributionChartData = (): ChartSeries[] => {
// Mock quality score distribution
const distribution = [
{ range: '9-10', count: Math.round(qualityData.totalChecks * 0.35), label: t('quality.excellent') },
{ range: '8-9', count: Math.round(qualityData.totalChecks * 0.30), label: t('quality.good') },
{ range: '7-8', count: Math.round(qualityData.totalChecks * 0.20), label: t('quality.acceptable') },
{ range: '6-7', count: Math.round(qualityData.totalChecks * 0.10), label: t('quality.poor') },
{ range: '<6', count: Math.round(qualityData.totalChecks * 0.05), label: t('quality.failed') }
].filter(item => item.count > 0);
return [
{
id: 'quality-distribution',
name: t('quality.score_distribution'),
type: 'bar',
color: '#d97706',
data: distribution.map(item => ({
x: item.range,
y: item.count,
label: item.label
}))
}
];
};
const getScoreColor = (score: number) => {
if (score >= 9) return 'text-green-600';
if (score >= 8) return 'text-blue-600';
if (score >= 7) return 'text-orange-600';
return 'text-red-600';
};
const getScoreStatus = (score: number) => {
if (score >= 9) return { status: 'excellent', bgColor: 'bg-green-100 dark:bg-green-900/20' };
if (score >= 8) return { status: 'good', bgColor: 'bg-blue-100 dark:bg-blue-900/20' };
if (score >= 7) return { status: 'acceptable', bgColor: 'bg-orange-100 dark:bg-orange-900/20' };
return { status: 'needs_improvement', bgColor: 'bg-red-100 dark:bg-red-900/20' };
};
const scoreStatus = getScoreStatus(qualityData.averageScore);
const TrendIcon = qualityData.trend >= 0 ? TrendingUp : TrendingDown;
return (
<AnalyticsWidget
title={t('quality.recent_quality_scores')}
subtitle={t('quality.daily_average_quality_trends')}
icon={Star}
loading={isLoading}
error={error?.message}
actions={
<Button variant="outline" size="sm">
<Award className="w-4 h-4 mr-1" />
{t('quality.actions.view_trends')}
</Button>
}
>
<div className="space-y-4">
{/* Quality Score Display */}
<div className="text-center">
<div className="flex items-center justify-center space-x-2 mb-2">
<Star className={`w-6 h-6 ${getScoreColor(qualityData.averageScore)}`} />
<span className="text-3xl font-bold text-[var(--text-primary)]">
{qualityData.averageScore.toFixed(1)}/10
</span>
</div>
<div className={`inline-flex items-center space-x-2 px-3 py-1 rounded-full text-sm ${scoreStatus.bgColor}`}>
<span className={getScoreColor(qualityData.averageScore)}>
{t(`quality.${scoreStatus.status}`)}
</span>
</div>
<div className="flex items-center justify-center space-x-1 mt-2">
<TrendIcon className={`w-4 h-4 ${qualityData.trend >= 0 ? 'text-green-600' : 'text-red-600'}`} />
<span className={`text-sm ${qualityData.trend >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{qualityData.trend > 0 ? '+' : ''}{qualityData.trend.toFixed(1)} {t('quality.vs_last_week')}
</span>
</div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="text-2xl font-bold text-[var(--text-primary)] mb-1">
{qualityData.totalChecks}
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('stats.total_checks')}</p>
</div>
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="text-2xl font-bold text-green-600 mb-1">
{qualityData.passRate.toFixed(1)}%
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('stats.pass_rate')}</p>
</div>
</div>
{qualityData.totalChecks === 0 ? (
<div className="text-center py-8 text-[var(--text-secondary)]">
<Star className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">{t('quality.no_quality_data')}</p>
</div>
) : (
<>
{/* Weekly Quality Trend */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.weekly_quality_trends')}
</h4>
<AnalyticsChart
series={getQualityTrendChartData()}
height={150}
showLegend={false}
showGrid={true}
/>
</div>
{/* Quality Score Distribution */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.score_distribution')}
</h4>
<AnalyticsChart
series={getQualityDistributionChartData()}
height={180}
showLegend={false}
showGrid={true}
/>
</div>
{/* Quality Insights */}
<div className="grid grid-cols-3 gap-3 text-center">
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-green-600">
{Math.round(qualityData.totalChecks * 0.35)}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('quality.excellent_scores')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-blue-600">
{Math.round(qualityData.totalChecks * 0.30)}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('quality.good_scores')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-red-600">
{Math.round(qualityData.totalChecks * 0.15)}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('quality.needs_improvement')}</p>
</div>
</div>
{/* Quality Recommendations */}
{qualityData.averageScore < 8 && (
<div className="p-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<Star className="w-4 h-4 mt-0.5 text-orange-600" />
<div>
<p className="text-sm font-medium text-orange-600">
{t('quality.recommendations.improve_quality')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('quality.recommendations.focus_consistency')}
</p>
</div>
</div>
</div>
)}
{qualityData.averageScore >= 9 && (
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<Award className="w-4 h-4 mt-0.5 text-green-600" />
<div>
<p className="text-sm font-medium text-green-600">
{t('quality.recommendations.excellent_quality')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('quality.recommendations.maintain_standards')}
</p>
</div>
</div>
</div>
)}
</>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,176 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Calendar, Clock, Users, Target, AlertCircle } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { Badge, Button } from '../../../ui';
import { useProductionDashboard, useProductionSchedule } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { ProductionStatus } from '../../../../api/types/production';
export const TodaysScheduleSummaryWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: dashboard, isLoading: dashboardLoading, error: dashboardError } = useProductionDashboard(tenantId);
const today = new Date().toISOString().split('T')[0];
const { data: schedule, isLoading: scheduleLoading, error: scheduleError } = useProductionSchedule(tenantId, today, today);
const isLoading = dashboardLoading || scheduleLoading;
const error = dashboardError?.message || scheduleError?.message;
const todaysSchedule = schedule?.schedules?.[0];
const plannedBatches = dashboard?.todays_production_plan || [];
const getStatusBadgeVariant = (status: ProductionStatus) => {
switch (status) {
case ProductionStatus.COMPLETED:
return 'success';
case ProductionStatus.IN_PROGRESS:
return 'info';
case ProductionStatus.PENDING:
return 'warning';
case ProductionStatus.CANCELLED:
case ProductionStatus.FAILED:
return 'error';
default:
return 'default';
}
};
const getPriorityColor = (priority?: string) => {
if (!priority) return 'text-gray-600';
switch (priority) {
case 'URGENT':
return 'text-red-600';
case 'HIGH':
return 'text-orange-600';
case 'MEDIUM':
return 'text-blue-600';
case 'LOW':
return 'text-gray-600';
default:
return 'text-gray-600';
}
};
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
});
};
return (
<AnalyticsWidget
title={t('schedule.todays_summary')}
subtitle={t('schedule.shift_hours_batches_staff')}
icon={Calendar}
loading={isLoading}
error={error}
actions={
<Button variant="outline" size="sm">
<Target className="w-4 h-4 mr-1" />
{t('actions.optimize')}
</Button>
}
>
<div className="space-y-4">
{/* Schedule Overview */}
{todaysSchedule && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="text-center">
<Clock className="w-5 h-5 text-[var(--color-primary)] mx-auto mb-1" />
<p className="text-sm text-[var(--text-secondary)]">{t('schedule.shift_hours')}</p>
<p className="font-semibold text-[var(--text-primary)]">
{formatTime(todaysSchedule.shift_start)} - {formatTime(todaysSchedule.shift_end)}
</p>
</div>
<div className="text-center">
<Target className="w-5 h-5 text-[var(--color-primary)] mx-auto mb-1" />
<p className="text-sm text-[var(--text-secondary)]">{t('stats.planned_batches')}</p>
<p className="font-semibold text-[var(--text-primary)]">{todaysSchedule.total_batches_planned}</p>
</div>
<div className="text-center">
<Users className="w-5 h-5 text-[var(--color-primary)] mx-auto mb-1" />
<p className="text-sm text-[var(--text-secondary)]">{t('schedule.staff_count')}</p>
<p className="font-semibold text-[var(--text-primary)]">{todaysSchedule.staff_count}</p>
</div>
<div className="text-center">
<AlertCircle className="w-5 h-5 text-[var(--color-primary)] mx-auto mb-1" />
<p className="text-sm text-[var(--text-secondary)]">{t('schedule.capacity_utilization')}</p>
<p className="font-semibold text-[var(--text-primary)]">
{todaysSchedule.utilization_percentage?.toFixed(1) || 0}%
</p>
</div>
</div>
)}
{/* Planned Batches */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Target className="w-4 h-4 mr-2" />
{t('schedule.planned_batches')} ({plannedBatches.length})
</h4>
{plannedBatches.length === 0 ? (
<div className="text-center py-6 text-[var(--text-secondary)]">
<Calendar className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">{t('messages.no_batches_planned')}</p>
</div>
) : (
<div className="space-y-2 max-h-64 overflow-y-auto">
{plannedBatches.map((batch, index) => (
<div
key={batch.batch_id || index}
className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<p className="font-medium text-[var(--text-primary)] truncate">
{batch.product_name}
</p>
{batch.priority && (
<span className={`text-xs font-medium ${getPriorityColor(batch.priority)}`}>
{t(`priority.${batch.priority.toLowerCase()}`)}
</span>
)}
</div>
<p className="text-sm text-[var(--text-secondary)]">
{t('batch.planned_quantity')}: {batch.planned_quantity} {t('common.units')}
</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant={getStatusBadgeVariant(batch.status)}>
{batch.status ? t(`status.${batch.status.toLowerCase()}`) : t('status.unknown')}
</Badge>
</div>
</div>
))}
</div>
)}
</div>
{/* Schedule Status */}
{todaysSchedule && (
<div className="flex items-center justify-between pt-3 border-t border-[var(--border-primary)]">
<div className="flex items-center space-x-2 text-sm">
<div className={`w-2 h-2 rounded-full ${todaysSchedule.is_active ? 'bg-green-500' : 'bg-gray-400'}`} />
<span className="text-[var(--text-secondary)]">
{todaysSchedule.is_active ? t('schedule.active') : t('schedule.inactive')}
</span>
</div>
<div className="text-sm text-[var(--text-secondary)]">
{todaysSchedule.is_finalized ? (
<span className="text-green-600">{t('schedule.finalized')}</span>
) : (
<span className="text-orange-600">{t('schedule.draft')}</span>
)}
</div>
</div>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,329 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, Eye, Camera, CheckSquare } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button, Badge } from '../../../ui';
import { useActiveBatches } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface DefectType {
type: string;
count: number;
percentage: number;
severity: 'low' | 'medium' | 'high';
trend: 'up' | 'down' | 'stable';
estimatedCost: number;
}
export const TopDefectTypesWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
// Calculate defect data from batches
const getDefectData = (): DefectType[] => {
const totalDefects = batches.reduce((sum, batch) => sum + (batch.defect_quantity || 0), 0);
if (totalDefects === 0) return [];
// Mock defect type distribution (in real implementation, this would come from quality check defect_types)
const defectTypes = [
{
type: 'burnt',
count: Math.round(totalDefects * 0.32),
percentage: 32,
severity: 'high' as const,
trend: 'down' as const,
estimatedCost: 45.60
},
{
type: 'underproofed',
count: Math.round(totalDefects * 0.24),
percentage: 24,
severity: 'medium' as const,
trend: 'stable' as const,
estimatedCost: 28.90
},
{
type: 'misshapen',
count: Math.round(totalDefects * 0.19),
percentage: 19,
severity: 'medium' as const,
trend: 'up' as const,
estimatedCost: 22.40
},
{
type: 'color_issues',
count: Math.round(totalDefects * 0.15),
percentage: 15,
severity: 'low' as const,
trend: 'stable' as const,
estimatedCost: 15.20
},
{
type: 'texture_problems',
count: Math.round(totalDefects * 0.10),
percentage: 10,
severity: 'medium' as const,
trend: 'down' as const,
estimatedCost: 12.80
}
].filter(defect => defect.count > 0);
return defectTypes;
};
const defectData = getDefectData();
const totalDefects = defectData.reduce((sum, defect) => sum + defect.count, 0);
const totalDefectCost = defectData.reduce((sum, defect) => sum + defect.estimatedCost, 0);
// Create defect distribution pie chart
const getDefectDistributionChartData = (): ChartSeries[] => {
if (defectData.length === 0) return [];
const colors = ['#dc2626', '#ea580c', '#d97706', '#ca8a04', '#65a30d'];
const pieData = defectData.map((defect, index) => ({
x: t(`quality.defects.${defect.type}`),
y: defect.count,
label: t(`quality.defects.${defect.type}`),
color: colors[index % colors.length]
}));
return [
{
id: 'defect-distribution',
name: t('quality.defect_distribution'),
type: 'pie',
color: '#dc2626',
data: pieData
}
];
};
// Create defect trend over time
const getDefectTrendChartData = (): ChartSeries[] => {
if (defectData.length === 0) return [];
const days = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
return defectData.slice(0, 3).map((defect, seriesIndex) => {
const colors = ['#dc2626', '#ea580c', '#d97706'];
const baseValue = defect.count / 7; // Average per day
const trendData = days.map((day, index) => {
let value = baseValue;
// Apply trend
if (defect.trend === 'up') value *= (1 + index * 0.05);
else if (defect.trend === 'down') value *= (1 - index * 0.05);
return {
x: day,
y: Math.max(0, value + (Math.random() - 0.5) * baseValue * 0.3),
label: day
};
});
return {
id: `defect-trend-${defect.type}`,
name: t(`quality.defects.${defect.type}`),
type: 'line' as const,
color: colors[seriesIndex],
data: trendData
};
});
};
const getSeverityBadgeVariant = (severity: DefectType['severity']) => {
switch (severity) {
case 'high': return 'error';
case 'medium': return 'warning';
case 'low': return 'info';
default: return 'default';
}
};
const getTrendIcon = (trend: DefectType['trend']) => {
switch (trend) {
case 'up': return '↗️';
case 'down': return '↘️';
case 'stable': return '➡️';
default: return '➡️';
}
};
const getTrendColor = (trend: DefectType['trend']) => {
switch (trend) {
case 'up': return 'text-red-600';
case 'down': return 'text-green-600';
case 'stable': return 'text-blue-600';
default: return 'text-gray-600';
}
};
return (
<AnalyticsWidget
title={t('quality.top_defect_types_24h')}
subtitle={t('quality.defect_analysis_cost_impact')}
icon={AlertCircle}
loading={isLoading}
error={error?.message}
actions={
<Button variant="outline" size="sm">
<Eye className="w-4 h-4 mr-1" />
{t('quality.actions.view_details')}
</Button>
}
>
<div className="space-y-4">
{/* Overall Defect Metrics */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<AlertCircle className="w-5 h-5 text-red-600" />
<span className="text-2xl font-bold text-[var(--text-primary)]">
{totalDefects}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('quality.total_defects')}</p>
</div>
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<span className="text-2xl font-bold text-red-600">
{totalDefectCost.toFixed(0)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)]">{t('quality.estimated_cost')}</p>
</div>
</div>
{defectData.length === 0 ? (
<div className="text-center py-8 text-[var(--text-secondary)]">
<CheckSquare className="w-12 h-12 mx-auto mb-3 opacity-50" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
{t('quality.no_defects_detected')}
</h3>
<p className="text-sm">{t('quality.excellent_quality_standards')}</p>
</div>
) : (
<>
{/* Top Defect Types List */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.defect_breakdown')}
</h4>
<div className="space-y-3 max-h-64 overflow-y-auto">
{defectData.map((defect, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-red-100 dark:bg-red-900/20 rounded-lg flex items-center justify-center">
<span className="text-sm font-bold text-red-600">#{index + 1}</span>
</div>
<div>
<p className="font-medium text-[var(--text-primary)]">
{t(`quality.defects.${defect.type}`)}
</p>
<div className="flex items-center space-x-3 text-xs text-[var(--text-secondary)]">
<span>{defect.count} {t('quality.incidents')}</span>
<span></span>
<span>{defect.estimatedCost.toFixed(2)} {t('quality.cost')}</span>
<span className={getTrendColor(defect.trend)}>
{getTrendIcon(defect.trend)} {t(`quality.trend.${defect.trend}`)}
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<span className="text-lg font-bold text-[var(--text-primary)]">
{defect.percentage}%
</span>
<Badge variant={getSeverityBadgeVariant(defect.severity)}>
{t(`quality.severity.${defect.severity}`)}
</Badge>
</div>
</div>
))}
</div>
</div>
{/* Defect Distribution Chart */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.defect_distribution')}
</h4>
<AnalyticsChart
series={getDefectDistributionChartData()}
height={200}
showLegend={true}
showGrid={false}
/>
</div>
{/* Defect Trends Over Time */}
{defectData.length > 0 && (
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.top_defects_weekly_trend')}
</h4>
<AnalyticsChart
series={getDefectTrendChartData()}
height={150}
showLegend={true}
showGrid={true}
/>
</div>
)}
{/* Defect Prevention Insights */}
<div className="space-y-2">
{defectData.some(d => d.severity === 'high') && (
<div className="flex items-start space-x-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
<AlertCircle className="w-4 h-4 mt-0.5 text-red-600" />
<div>
<p className="text-sm font-medium text-red-600">
{t('quality.recommendations.critical_defects')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('quality.recommendations.immediate_action_required')}
</p>
</div>
</div>
)}
<div className="flex items-start space-x-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<Camera className="w-4 h-4 mt-0.5 text-blue-600" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('quality.recommendations.documentation')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('quality.recommendations.photo_documentation_helps')}
</p>
</div>
</div>
</div>
{/* Action Items */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h5 className="text-sm font-medium text-[var(--text-primary)] mb-2">
{t('quality.recommended_actions')}
</h5>
<div className="space-y-1 text-xs text-[var(--text-secondary)]">
{defectData[0] && (
<p> {t('quality.actions.focus_on')} {t(`quality.defects.${defectData[0].type}`).toLowerCase()}</p>
)}
{defectData.some(d => d.trend === 'up') && (
<p> {t('quality.actions.investigate_increasing_defects')}</p>
)}
<p> {t('quality.actions.review_process_controls')}</p>
</div>
</div>
</>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,323 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTriangle, Trash2, Target, TrendingDown } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button, Badge } from '../../../ui';
import { useActiveBatches } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface WasteSource {
source: string;
count: number;
percentage: number;
cost: number;
severity: 'low' | 'medium' | 'high';
}
export const WasteDefectTrackerWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
// Calculate waste and defect data from batches
const getWasteData = () => {
const totalUnits = batches.reduce((sum, batch) => sum + (batch.actual_quantity || batch.planned_quantity || 0), 0);
const totalWaste = batches.reduce((sum, batch) => sum + (batch.waste_quantity || 0), 0);
const totalDefects = batches.reduce((sum, batch) => sum + (batch.defect_quantity || 0), 0);
const wastePercentage = totalUnits > 0 ? (totalWaste / totalUnits) * 100 : 0;
const defectPercentage = totalUnits > 0 ? (totalDefects / totalUnits) * 100 : 0;
return {
totalUnits,
totalWaste,
totalDefects,
wastePercentage,
defectPercentage
};
};
const wasteData = getWasteData();
// Mock waste sources data (in real implementation, this would come from quality check defect types)
const getWasteSources = (): WasteSource[] => {
return [
{
source: t('quality.defects.burnt'),
count: Math.round(wasteData.totalWaste * 0.35),
percentage: 35,
cost: 45.20,
severity: 'high'
},
{
source: t('quality.defects.misshapen'),
count: Math.round(wasteData.totalWaste * 0.28),
percentage: 28,
cost: 32.15,
severity: 'medium'
},
{
source: t('quality.defects.underproofed'),
count: Math.round(wasteData.totalWaste * 0.20),
percentage: 20,
cost: 28.90,
severity: 'medium'
},
{
source: t('quality.defects.temperature_issues'),
count: Math.round(wasteData.totalWaste * 0.12),
percentage: 12,
cost: 18.70,
severity: 'low'
},
{
source: t('quality.defects.expired'),
count: Math.round(wasteData.totalWaste * 0.05),
percentage: 5,
cost: 8.40,
severity: 'low'
}
].filter(source => source.count > 0);
};
const wasteSources = getWasteSources();
const totalWasteCost = wasteSources.reduce((sum, source) => sum + source.cost, 0);
// Create pie chart for waste sources
const getWasteSourcesChartData = (): ChartSeries[] => {
if (wasteSources.length === 0) return [];
const colors = ['#dc2626', '#ea580c', '#d97706', '#ca8a04', '#65a30d'];
const pieData = wasteSources.map((source, index) => ({
x: source.source,
y: source.count,
label: source.source,
color: colors[index % colors.length]
}));
return [
{
id: 'waste-sources',
name: t('quality.waste_sources'),
type: 'pie',
color: '#dc2626',
data: pieData
}
];
};
// Create trend chart for waste over time
const getWasteTrendChartData = (): ChartSeries[] => {
const days = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
const wasteData = days.map((day, index) => ({
x: day,
y: Math.max(1, 8 - index + Math.random() * 3), // Decreasing trend with some variation
label: day
}));
const defectData = days.map((day, index) => ({
x: day,
y: Math.max(0.5, 5 - index * 0.5 + Math.random() * 2),
label: day
}));
return [
{
id: 'waste-trend',
name: t('quality.waste_percentage'),
type: 'line',
color: '#dc2626',
data: wasteData
},
{
id: 'defect-trend',
name: t('quality.defect_percentage'),
type: 'line',
color: '#ea580c',
data: defectData
}
];
};
const getSeverityBadgeVariant = (severity: WasteSource['severity']) => {
switch (severity) {
case 'high': return 'error';
case 'medium': return 'warning';
case 'low': return 'info';
default: return 'default';
}
};
const getWasteStatus = () => {
const totalWastePercentage = wasteData.wastePercentage + wasteData.defectPercentage;
if (totalWastePercentage <= 3) return { status: 'excellent', color: 'text-green-600', bgColor: 'bg-green-100 dark:bg-green-900/20' };
if (totalWastePercentage <= 5) return { status: 'good', color: 'text-blue-600', bgColor: 'bg-blue-100 dark:bg-blue-900/20' };
if (totalWastePercentage <= 8) return { status: 'warning', color: 'text-orange-600', bgColor: 'bg-orange-100 dark:bg-orange-900/20' };
return { status: 'critical', color: 'text-red-600', bgColor: 'bg-red-100 dark:bg-red-900/20' };
};
const wasteStatus = getWasteStatus();
const totalWastePercentage = wasteData.wastePercentage + wasteData.defectPercentage;
return (
<AnalyticsWidget
title={t('quality.waste_defect_tracker')}
subtitle={t('quality.waste_sources_trends_costs')}
icon={AlertTriangle}
loading={isLoading}
error={error?.message}
actions={
<Button variant="outline" size="sm">
<Target className="w-4 h-4 mr-1" />
{t('quality.actions.reduce_waste')}
</Button>
}
>
<div className="space-y-4">
{/* Overall Waste Metrics */}
<div className="grid grid-cols-3 gap-4">
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<Trash2 className="w-4 h-4 text-red-600" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{totalWastePercentage.toFixed(1)}%
</span>
</div>
<p className="text-xs text-[var(--text-secondary)]">{t('quality.total_waste')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<AlertTriangle className="w-4 h-4 text-orange-600" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{wasteData.totalDefects}
</span>
</div>
<p className="text-xs text-[var(--text-secondary)]">{t('quality.total_defects')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg text-center">
<div className="flex items-center justify-center space-x-2 mb-1">
<TrendingDown className="w-4 h-4 text-green-600" />
<span className="text-lg font-bold text-[var(--text-primary)]">
{totalWasteCost.toFixed(0)}
</span>
</div>
<p className="text-xs text-[var(--text-secondary)]">{t('cost.waste_cost')}</p>
</div>
</div>
{/* Waste Status */}
<div className={`p-3 rounded-lg ${wasteStatus.bgColor}`}>
<div className="flex items-center space-x-2">
<AlertTriangle className={`w-4 h-4 ${wasteStatus.color}`} />
<span className={`text-sm font-medium ${wasteStatus.color}`}>
{t(`quality.status.${wasteStatus.status}`)}
</span>
</div>
</div>
{wasteSources.length === 0 ? (
<div className="text-center py-8 text-[var(--text-secondary)]">
<Target className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">{t('quality.no_waste_data')}</p>
</div>
) : (
<>
{/* Waste Sources Breakdown */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.top_waste_sources')}
</h4>
<div className="space-y-2">
{wasteSources.slice(0, 5).map((source, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-red-500 rounded-full" />
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">
{source.source}
</p>
<p className="text-xs text-[var(--text-secondary)]">
{source.count} {t('common.units')} {source.cost.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-semibold text-[var(--text-primary)]">
{source.percentage}%
</span>
<Badge variant={getSeverityBadgeVariant(source.severity)}>
{t(`quality.severity.${source.severity}`)}
</Badge>
</div>
</div>
))}
</div>
</div>
{/* Waste Sources Pie Chart */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.waste_distribution')}
</h4>
<AnalyticsChart
series={getWasteSourcesChartData()}
height={200}
showLegend={true}
showGrid={false}
/>
</div>
{/* Weekly Waste Trend */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('quality.weekly_waste_trend')}
</h4>
<AnalyticsChart
series={getWasteTrendChartData()}
height={150}
showLegend={true}
showGrid={true}
/>
</div>
{/* Reduction Recommendations */}
<div className="space-y-2">
{wasteSources.filter(s => s.severity === 'high').length > 0 && (
<div className="flex items-start space-x-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
<AlertTriangle className="w-4 h-4 mt-0.5 text-red-600" />
<div>
<p className="text-sm font-medium text-red-600">
{t('quality.recommendations.high_waste_detected')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('quality.recommendations.check_temperature_timing')}
</p>
</div>
</div>
)}
<div className="flex items-start space-x-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<Target className="w-4 h-4 mt-0.5 text-blue-600" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('quality.recommendations.improvement_opportunity')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{totalWastePercentage > 5
? t('quality.recommendations.reduce_waste_target', { target: '3%' })
: t('quality.recommendations.maintain_quality_standards')
}
</p>
</div>
</div>
</div>
</>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,321 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Target, TrendingUp, Award, BarChart3 } from 'lucide-react';
import { AnalyticsWidget } from '../AnalyticsWidget';
import { AnalyticsChart, ChartSeries } from '../AnalyticsChart';
import { Button, Badge } from '../../../ui';
import { useActiveBatches } from '../../../../api/hooks/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
interface ProductYieldData {
product: string;
averageYield: number;
bestYield: number;
worstYield: number;
batchCount: number;
trend: 'up' | 'down' | 'stable';
performance: 'excellent' | 'good' | 'average' | 'poor';
}
export const YieldPerformanceWidget: React.FC = () => {
const { t } = useTranslation('production');
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: batchesData, isLoading, error } = useActiveBatches(tenantId);
const batches = batchesData?.batches || [];
// Calculate yield data from batches
const getProductYieldData = (): ProductYieldData[] => {
const productMap = new Map<string, number[]>();
batches.forEach(batch => {
if (batch.yield_percentage) {
const product = batch.product_name;
if (!productMap.has(product)) {
productMap.set(product, []);
}
productMap.get(product)!.push(batch.yield_percentage);
}
});
return Array.from(productMap.entries()).map(([product, yields]) => {
const averageYield = yields.reduce((sum, yieldValue) => sum + yieldValue, 0) / yields.length;
const bestYield = Math.max(...yields);
const worstYield = Math.min(...yields);
// Simulate trend calculation (in real implementation, this would compare with historical data)
const trend: 'up' | 'down' | 'stable' =
averageYield > 92 ? 'up' :
averageYield < 88 ? 'down' : 'stable';
const performance: 'excellent' | 'good' | 'average' | 'poor' =
averageYield >= 95 ? 'excellent' :
averageYield >= 90 ? 'good' :
averageYield >= 85 ? 'average' : 'poor';
return {
product,
averageYield,
bestYield,
worstYield,
batchCount: yields.length,
trend,
performance
};
}).sort((a, b) => b.averageYield - a.averageYield);
};
const productYieldData = getProductYieldData();
const overallYield = productYieldData.length > 0
? productYieldData.reduce((sum, item) => sum + item.averageYield, 0) / productYieldData.length
: 0;
// Create yield comparison chart
const getYieldComparisonChartData = (): ChartSeries[] => {
if (productYieldData.length === 0) return [];
const averageData = productYieldData.map(item => ({
x: item.product,
y: item.averageYield,
label: item.product
}));
const bestData = productYieldData.map(item => ({
x: item.product,
y: item.bestYield,
label: item.product
}));
return [
{
id: 'average-yield',
name: t('stats.average_yield'),
type: 'bar',
color: '#d97706',
data: averageData
},
{
id: 'best-yield',
name: t('stats.best_yield'),
type: 'bar',
color: '#16a34a',
data: bestData
}
];
};
// Create yield trend over time
const getYieldTrendChartData = (): ChartSeries[] => {
const days = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
const trendData = days.map((day, index) => ({
x: day,
y: Math.max(85, 90 + index * 0.5 + Math.random() * 3), // Slightly improving trend
label: day
}));
return [
{
id: 'yield-trend',
name: t('stats.yield_trend'),
type: 'line',
color: '#16a34a',
data: trendData
}
];
};
const getPerformanceBadgeVariant = (performance: ProductYieldData['performance']) => {
switch (performance) {
case 'excellent': return 'success';
case 'good': return 'info';
case 'average': return 'warning';
case 'poor': return 'error';
default: return 'default';
}
};
const getTrendIcon = (trend: ProductYieldData['trend']) => {
switch (trend) {
case 'up': return TrendingUp;
case 'down': return TrendingUp; // We'll rotate it in CSS for down
case 'stable': return BarChart3;
default: return BarChart3;
}
};
const getTrendColor = (trend: ProductYieldData['trend']) => {
switch (trend) {
case 'up': return 'text-green-600';
case 'down': return 'text-red-600';
case 'stable': return 'text-blue-600';
default: return 'text-gray-600';
}
};
const getYieldStatus = () => {
if (overallYield >= 95) return { status: 'excellent', color: 'text-green-600', bgColor: 'bg-green-100 dark:bg-green-900/20' };
if (overallYield >= 90) return { status: 'good', color: 'text-blue-600', bgColor: 'bg-blue-100 dark:bg-blue-900/20' };
if (overallYield >= 85) return { status: 'average', color: 'text-orange-600', bgColor: 'bg-orange-100 dark:bg-orange-900/20' };
return { status: 'poor', color: 'text-red-600', bgColor: 'bg-red-100 dark:bg-red-900/20' };
};
const yieldStatus = getYieldStatus();
return (
<AnalyticsWidget
title={t('stats.yield_performance_leaderboard')}
subtitle={t('stats.product_yield_rankings_trends')}
icon={Award}
loading={isLoading}
error={error?.message}
actions={
<Button variant="outline" size="sm">
<Target className="w-4 h-4 mr-1" />
{t('actions.optimize_yields')}
</Button>
}
>
<div className="space-y-4">
{/* Overall Yield Status */}
<div className="text-center">
<div className="flex items-center justify-center space-x-2 mb-2">
<Award className={`w-6 h-6 ${yieldStatus.color}`} />
<span className="text-3xl font-bold text-[var(--text-primary)]">
{overallYield.toFixed(1)}%
</span>
</div>
<div className={`inline-flex items-center space-x-2 px-3 py-1 rounded-full text-sm ${yieldStatus.bgColor}`}>
<span className={yieldStatus.color}>
{t(`performance.${yieldStatus.status}`)}
</span>
</div>
</div>
{productYieldData.length === 0 ? (
<div className="text-center py-8 text-[var(--text-secondary)]">
<Award className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">{t('stats.no_yield_data')}</p>
</div>
) : (
<>
{/* Yield Leaderboard */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 flex items-center">
<Award className="w-4 h-4 mr-2" />
{t('stats.product_leaderboard')}
</h4>
<div className="space-y-2 max-h-64 overflow-y-auto">
{productYieldData.map((item, index) => {
const TrendIcon = getTrendIcon(item.trend);
const trendColor = getTrendColor(item.trend);
return (
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center space-x-3">
<div className="flex items-center justify-center w-6 h-6 bg-[var(--color-primary)]/10 rounded-full text-xs font-bold text-[var(--color-primary)]">
{index + 1}
</div>
<div>
<p className="font-medium text-[var(--text-primary)]">
{item.product}
</p>
<div className="flex items-center space-x-2 text-xs text-[var(--text-secondary)]">
<span>{item.batchCount} {t('stats.batches')}</span>
<span></span>
<span>{item.bestYield.toFixed(1)}% {t('stats.best')}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<div className={`flex items-center space-x-1 ${trendColor}`}>
<TrendIcon
className={`w-3 h-3 ${item.trend === 'down' ? 'transform rotate-180' : ''}`}
/>
</div>
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{item.averageYield.toFixed(1)}%
</div>
<Badge variant={getPerformanceBadgeVariant(item.performance)} className="text-xs">
{t(`performance.${item.performance}`)}
</Badge>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Yield Comparison Chart */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('stats.yield_comparison')}
</h4>
<AnalyticsChart
series={getYieldComparisonChartData()}
height={200}
showLegend={true}
showGrid={true}
/>
</div>
{/* Weekly Yield Trend */}
<div>
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">
{t('stats.weekly_yield_trend')}
</h4>
<AnalyticsChart
series={getYieldTrendChartData()}
height={150}
showLegend={false}
showGrid={true}
/>
</div>
{/* Yield Insights */}
<div className="grid grid-cols-3 gap-3 text-center">
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-green-600">
{productYieldData.filter(p => p.performance === 'excellent').length}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('performance.excellent')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-blue-600">
{productYieldData.filter(p => p.performance === 'good').length}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('performance.good')}</p>
</div>
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-orange-600">
{productYieldData.filter(p => p.performance === 'average' || p.performance === 'poor').length}
</p>
<p className="text-xs text-[var(--text-secondary)]">{t('performance.needs_improvement')}</p>
</div>
</div>
{/* Improvement Recommendations */}
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-2">
<Target className="w-4 h-4 mt-0.5 text-blue-600" />
<div>
<p className="text-sm font-medium text-blue-600">
{t('recommendations.yield_improvement')}
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{overallYield < 90
? t('recommendations.focus_on_low_performers')
: t('recommendations.maintain_high_standards')
}
</p>
</div>
</div>
</div>
</>
)}
</div>
</AnalyticsWidget>
);
};

View File

@@ -1,38 +0,0 @@
// Bakery Operations Overview Widgets
export { TodaysScheduleSummaryWidget } from './TodaysScheduleSummaryWidget';
export { LiveBatchTrackerWidget } from './LiveBatchTrackerWidget';
export { OnTimeCompletionWidget } from './OnTimeCompletionWidget';
export { CapacityUtilizationWidget } from './CapacityUtilizationWidget';
// Cost & Efficiency Monitoring Widgets
export { CostPerUnitWidget } from './CostPerUnitWidget';
export { WasteDefectTrackerWidget } from './WasteDefectTrackerWidget';
export { YieldPerformanceWidget } from './YieldPerformanceWidget';
// Quality Assurance Panel Widgets
export { QualityScoreTrendsWidget } from './QualityScoreTrendsWidget';
export { TopDefectTypesWidget } from './TopDefectTypesWidget';
// Equipment & Maintenance Widgets
export { EquipmentStatusWidget } from './EquipmentStatusWidget';
export { MaintenanceScheduleWidget } from './MaintenanceScheduleWidget';
export { EquipmentEfficiencyWidget } from './EquipmentEfficiencyWidget';
// AI Insights & Predictive Analytics Widgets
export { AIInsightsWidget } from './AIInsightsWidget';
export { PredictiveMaintenanceWidget } from './PredictiveMaintenanceWidget';
import React from 'react';
// Widget Types
export interface WidgetConfig {
id: string;
title: string;
component: React.ComponentType;
category: 'operations' | 'cost' | 'quality' | 'equipment' | 'ai';
size: 'sm' | 'md' | 'lg' | 'xl';
minSubscription?: 'basic' | 'professional' | 'enterprise';
}
// Export widget configurations (temporarily commented out due to import issues)
// export const WIDGET_CONFIGS: WidgetConfig[] = [];