Files
bakery-ia/frontend/src/pages/app/analytics/ProductionAnalyticsPage.tsx

1080 lines
45 KiB
TypeScript
Raw Normal View History

2025-09-23 19:24:22 +02:00
import React, { useState } from 'react';
2025-09-23 22:11:34 +02:00
import { TrendingUp, DollarSign, Activity, AlertTriangle, Settings, CheckCircle, Wrench, Zap, Thermometer, Eye, Clock, CalendarClock, WrenchIcon, BarChart3, Bell, History, Lock } from 'lucide-react';
2025-09-23 19:24:22 +02:00
import { Card, StatsGrid, Button } from '../../../components/ui';
import { PageHeader } from '../../../components/layout';
2025-09-23 22:11:34 +02:00
import { QualityDashboard } from '../../../components/domain/production';
import ProductionCostAnalytics from '../../../components/domain/analytics/ProductionCostAnalytics';
import AIInsightsWidget from '../../../components/domain/dashboard/AIInsightsWidget';
import EquipmentStatusWidget from '../../../components/domain/dashboard/EquipmentStatusWidget';
import { Badge } from '../../../components/ui/Badge';
import { useSubscription } from '../../../api/hooks/subscription';
import { useCurrentTenant } from '../../../stores/tenant.store';
2025-09-23 19:24:22 +02:00
2025-09-23 22:11:34 +02:00
// Mock data for equipment (from MaquinariaPage)
const MOCK_EQUIPMENT = [
{
id: '1',
name: 'Horno Principal #1',
type: 'oven',
model: 'Miwe Condo CO 4.1212',
serialNumber: 'MCO-2021-001',
location: 'Área de Horneado - Zona A',
status: 'operational',
installDate: '2021-03-15',
lastMaintenance: '2024-01-15',
nextMaintenance: '2024-04-15',
maintenanceInterval: 90,
temperature: 220,
targetTemperature: 220,
efficiency: 92,
uptime: 98.5,
energyUsage: 45.2,
utilizationToday: 87,
alerts: [],
maintenanceHistory: [
{
id: '1',
date: '2024-01-15',
type: 'preventive',
description: 'Limpieza general y calibración de termostatos',
technician: 'Juan Pérez',
cost: 150,
downtime: 2,
partsUsed: ['Filtros de aire', 'Sellos de puerta']
}
],
specifications: {
power: 45,
capacity: 24,
dimensions: { width: 200, height: 180, depth: 120 },
weight: 850
}
},
{
id: '2',
name: 'Batidora Industrial #2',
type: 'mixer',
model: 'Hobart HL800',
serialNumber: 'HHL-2020-002',
location: 'Área de Preparación - Zona B',
status: 'warning',
installDate: '2020-08-10',
lastMaintenance: '2024-01-20',
nextMaintenance: '2024-02-20',
maintenanceInterval: 30,
efficiency: 88,
uptime: 94.2,
energyUsage: 12.8,
utilizationToday: 76,
alerts: [
{
id: '1',
type: 'warning',
message: 'Vibración inusual detectada en el motor',
timestamp: '2024-01-23T10:30:00Z',
acknowledged: false
},
{
id: '2',
type: 'info',
message: 'Mantenimiento programado en 5 días',
timestamp: '2024-01-23T08:00:00Z',
acknowledged: true
}
],
maintenanceHistory: [
{
id: '1',
date: '2024-01-20',
type: 'corrective',
description: 'Reemplazo de correas de transmisión',
technician: 'María González',
cost: 85,
downtime: 4,
partsUsed: ['Correa tipo V', 'Rodamientos']
}
],
specifications: {
power: 15,
capacity: 80,
dimensions: { width: 120, height: 150, depth: 80 },
weight: 320
}
},
{
id: '3',
name: 'Cámara de Fermentación #1',
type: 'proofer',
model: 'Bongard EUROPA 16.18',
serialNumber: 'BEU-2022-001',
location: 'Área de Fermentación',
status: 'maintenance',
installDate: '2022-06-20',
lastMaintenance: '2024-01-23',
nextMaintenance: '2024-01-24',
maintenanceInterval: 60,
temperature: 32,
targetTemperature: 35,
efficiency: 0,
uptime: 85.1,
energyUsage: 0,
utilizationToday: 0,
alerts: [
{
id: '1',
type: 'info',
message: 'En mantenimiento programado',
timestamp: '2024-01-23T06:00:00Z',
acknowledged: true
}
],
maintenanceHistory: [
{
id: '1',
date: '2024-01-23',
type: 'preventive',
description: 'Mantenimiento programado - sistema de humidificación',
technician: 'Carlos Rodríguez',
cost: 200,
downtime: 8,
partsUsed: ['Sensor de humedad', 'Válvulas']
}
],
specifications: {
power: 8,
capacity: 16,
dimensions: { width: 180, height: 200, depth: 100 },
weight: 450
}
}
];
// Mock chart data for equipment analytics (from MaquinariaPage)
const MOCK_CHART_DATA = [
{
id: 'efficiency',
name: 'Eficiencia',
type: 'bar' as const,
visible: true,
color: '#10B981',
data: [
{ x: 1, y: 92, label: 'Horno Principal #1' },
{ x: 2, y: 88, label: 'Batidora Industrial #2' },
{ x: 3, y: 0, label: 'Cámara de Fermentación #1' }
]
},
{
id: 'uptime',
name: 'Tiempo de Actividad',
type: 'line' as const,
visible: true,
color: '#3B82F6',
data: [
{ x: 1, y: 98.5, label: 'Horno Principal #1' },
{ x: 2, y: 94.2, label: 'Batidora Industrial #2' },
{ x: 3, y: 85.1, label: 'Cámara de Fermentación #1' }
]
},
{
id: 'energy',
name: 'Consumo Energético',
type: 'area' as const,
visible: true,
color: '#F59E0B',
data: [
{ x: 1, y: 45.2, label: 'Horno Principal #1' },
{ x: 2, y: 12.8, label: 'Batidora Industrial #2' },
{ x: 3, y: 0, label: 'Cámara de Fermentación #1' }
]
}
];
const MOCK_MAINTENANCE_CHART_DATA = [
{
id: 'costs',
name: 'Costos de Mantenimiento',
type: 'bar' as const,
visible: true,
color: '#8B5CF6',
data: [
{ x: 1, y: 150, label: 'Horno Principal #1' },
{ x: 2, y: 85, label: 'Batidora Industrial #2' },
{ x: 3, y: 200, label: 'Cámara de Fermentación #1' }
]
},
{
id: 'downtime',
name: 'Tiempo de Inactividad',
type: 'line' as const,
visible: true,
color: '#EF4444',
data: [
{ x: 1, y: 2, label: 'Horno Principal #1' },
{ x: 2, y: 4, label: 'Batidora Industrial #2' },
{ x: 3, y: 8, label: 'Cámara de Fermentación #1' }
]
}
];
const MOCK_STATUS_CHART_DATA = [
{
id: 'status',
name: 'Estado de Equipos',
type: 'pie' as const,
visible: true,
color: '#10B981',
data: [
{ x: 'Operativo', y: 1, label: 'Operativo' },
{ x: 'Advertencia', y: 1, label: 'Advertencia' },
{ x: 'Mantenimiento', y: 1, label: 'Mantenimiento' }
]
}
];
// Import statement is at the top of the file - already included
// The enhanced ProductionCostMonitor is now imported from the components directory
// Helper functions for equipment status
const getStatusConfig = (status: string) => {
const configs = {
operational: { color: '#10B981', text: 'Operativo', icon: CheckCircle },
warning: { color: '#F59E0B', text: 'Advertencia', icon: AlertTriangle },
maintenance: { color: '#3B82F6', text: 'Mantenimiento', icon: Wrench },
down: { color: '#EF4444', text: 'Fuera de Servicio', icon: AlertTriangle }
};
return configs[status as keyof typeof configs] || { color: '#6B7280', text: 'Desconocido', icon: Settings };
2025-09-23 19:24:22 +02:00
};
2025-09-23 22:11:34 +02:00
const getTypeIcon = (type: string) => {
const icons = {
oven: Thermometer,
mixer: Activity,
proofer: Settings,
freezer: Zap,
packaging: Settings,
other: Settings
};
return icons[type as keyof typeof icons] || Settings;
};
const getStatusColor = (status: string): string => {
const statusColors: { [key: string]: string } = {
operational: '#10B981', // green-500
warning: '#F59E0B', // amber-500
maintenance: '#3B82F6', // blue-500
down: '#EF4444' // red-500
};
return statusColors[status] || '#6B7280'; // gray-500 as default
2025-09-23 19:24:22 +02:00
};
2025-09-23 22:11:34 +02:00
// Equipment Analytics Component (from MaquinariaPage)
const EquipmentAnalytics: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
// Filter equipment based on search and status
const filteredEquipment = MOCK_EQUIPMENT.filter(eq => {
const matchesSearch = !searchTerm ||
eq.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
eq.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
eq.type.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || eq.status === statusFilter;
return matchesSearch && matchesStatus;
});
// Calculate equipment stats
const equipmentStats = {
total: MOCK_EQUIPMENT.length,
operational: MOCK_EQUIPMENT.filter(e => e.status === 'operational').length,
warning: MOCK_EQUIPMENT.filter(e => e.status === 'warning').length,
maintenance: MOCK_EQUIPMENT.filter(e => e.status === 'maintenance').length,
down: MOCK_EQUIPMENT.filter(e => e.status === 'down').length,
avgEfficiency: MOCK_EQUIPMENT.reduce((sum, e) => sum + e.efficiency, 0) / MOCK_EQUIPMENT.length,
avgUptime: MOCK_EQUIPMENT.reduce((sum, e) => sum + e.uptime, 0) / MOCK_EQUIPMENT.length,
totalAlerts: MOCK_EQUIPMENT.reduce((sum, e) => sum + e.alerts.filter(a => !a.acknowledged).length, 0)
};
2025-09-23 19:24:22 +02:00
return (
2025-09-23 22:11:34 +02:00
<div className="space-y-6">
{/* Stats Grid */}
<StatsGrid
stats={[
{
title: 'Total Equipos',
value: equipmentStats.total,
icon: Settings,
variant: 'default' as const
},
{
title: 'Operativo',
value: equipmentStats.operational,
icon: CheckCircle,
variant: 'success' as const,
subtitle: `${((equipmentStats.operational / equipmentStats.total) * 100).toFixed(1)}%`
},
{
title: 'Eficiencia Promedio',
value: `${equipmentStats.avgEfficiency.toFixed(1)}%`,
icon: TrendingUp,
variant: equipmentStats.avgEfficiency >= 90 ? 'success' as const : 'warning' as const
},
{
title: 'Alertas Activas',
value: equipmentStats.totalAlerts,
icon: Bell,
variant: equipmentStats.totalAlerts === 0 ? 'success' as const : 'error' as const
}
]}
columns={4}
/>
{/* Tabs for different views */}
<Card className="p-0">
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8 px-6 pt-4">
<button
onClick={() => setActiveTab('overview')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'overview'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
<BarChart3 className="w-4 h-4 inline mr-2" />
Resumen
</button>
<button
onClick={() => setActiveTab('maintenance')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'maintenance'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
<CalendarClock className="w-4 h-4 inline mr-2" />
Mantenimiento
</button>
<button
onClick={() => setActiveTab('status')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'status'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
<WrenchIcon className="w-4 h-4 inline mr-2" />
Estado
</button>
</nav>
2025-09-23 19:24:22 +02:00
</div>
2025-09-23 22:11:34 +02:00
<div className="p-6">
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Charts for Equipment Analytics */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Eficiencia y Tiempo de Actividad</h3>
<div className="h-64">
<canvas ref={(canvas) => {
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
// Set canvas size
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw simple bar chart for efficiency
const padding = 40;
const chartWidth = canvas.width - 2 * padding;
const chartHeight = canvas.height - 2 * padding;
// Get max value for scaling
const maxValue = Math.max(...MOCK_CHART_DATA[0].data.map(d => d.y));
// Draw bars
MOCK_CHART_DATA[0].data.forEach((point, index) => {
const barWidth = chartWidth / MOCK_CHART_DATA[0].data.length - 10;
const x = padding + index * (chartWidth / MOCK_CHART_DATA[0].data.length) + 5;
const barHeight = (point.y / maxValue) * chartHeight;
const y = padding + chartHeight - barHeight;
// Bar
ctx.fillStyle = MOCK_CHART_DATA[0].color;
ctx.fillRect(x, y, barWidth, barHeight);
// Label
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label, x + barWidth / 2, canvas.height - 10);
ctx.fillText(point.y.toString(), x + barWidth / 2, y - 5);
});
// 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();
}
}
}} className="w-full h-full" />
</div>
</Card>
<Card className="p-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Estado de Equipos</h3>
<div className="h-64">
<canvas ref={(canvas) => {
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
// Set canvas size
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw pie chart for status distribution
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(canvas.width, canvas.height) / 3;
const total = MOCK_STATUS_CHART_DATA[0].data.reduce((sum, point) => sum + point.y, 0);
let startAngle = -Math.PI / 2;
const colors = ['#10B981', '#F59E0B', '#3B82F6'];
MOCK_STATUS_CHART_DATA[0].data.forEach((point, index) => {
const sliceAngle = (point.y / total) * 2 * Math.PI;
const color = colors[index];
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fill();
// Draw slice border
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
// Draw labels
const labelAngle = startAngle + sliceAngle / 2;
const labelX = centerX + Math.cos(labelAngle) * (radius + 30);
const labelY = centerY + Math.sin(labelAngle) * (radius + 30);
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.x, labelX, labelY);
const percentage = ((point.y / total) * 100).toFixed(1);
ctx.fillText(`${percentage}%`, labelX, labelY + 15);
startAngle += sliceAngle;
});
}
}
}} className="w-full h-full" />
</div>
</Card>
</div>
{/* Controls */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Settings className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
<input
placeholder="Buscar equipos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Wrench className="w-4 h-4 text-[var(--text-tertiary)]" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
<option value="all">Todos los estados</option>
<option value="operational">Operativo</option>
<option value="warning">Advertencia</option>
<option value="maintenance">Mantenimiento</option>
<option value="down">Fuera de Servicio</option>
</select>
</div>
</div>
</Card>
{/* Equipment Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredEquipment.map((equipment) => {
const statusConfig = getStatusConfig(equipment.status);
const TypeIcon = getTypeIcon(equipment.type);
return (
<Card key={equipment.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: statusConfig.color }}
/>
<div>
<h3 className="font-semibold text-[var(--text-primary)]">{equipment.name}</h3>
<p className="text-sm text-[var(--text-secondary)]">{equipment.location}</p>
</div>
</div>
<TypeIcon className="w-5 h-5 text-[var(--text-tertiary)]" />
</div>
<div className="mt-4 grid grid-cols-2 gap-3">
<div>
<p className="text-xs text-[var(--text-secondary)]">Eficiencia</p>
<p className="font-semibold text-[var(--text-primary)]">{equipment.efficiency}%</p>
</div>
<div>
<p className="text-xs text-[var(--text-secondary)]">Tiempo Activo</p>
<p className="font-semibold text-[var(--text-primary)]">{equipment.uptime.toFixed(1)}%</p>
</div>
<div>
<p className="text-xs text-[var(--text-secondary)]">Consumo</p>
<p className="font-semibold text-[var(--text-primary)]">{equipment.energyUsage} kW</p>
</div>
<div>
<p className="text-xs text-[var(--text-secondary)]">Utilización Hoy</p>
<p className="font-semibold text-[var(--text-primary)]">{equipment.utilizationToday}%</p>
</div>
</div>
<div className="mt-4 flex justify-between">
<Badge
variant={equipment.status === 'operational' ? 'success' :
equipment.status === 'warning' ? 'warning' :
equipment.status === 'maintenance' ? 'info' : 'error'}
>
{statusConfig.text}
</Badge>
<Button variant="outline" size="sm">
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
</div>
</Card>
);
})}
</div>
</div>
)}
{/* Maintenance Tab */}
{activeTab === 'maintenance' && (
<div className="space-y-6">
{/* Maintenance Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Costos de Mantenimiento</h3>
<div className="h-64">
<canvas ref={(canvas) => {
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
// Set canvas size
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw bar chart for maintenance costs
const padding = 40;
const chartWidth = canvas.width - 2 * padding;
const chartHeight = canvas.height - 2 * padding;
// Get max value for scaling
const maxValue = Math.max(...MOCK_MAINTENANCE_CHART_DATA[0].data.map(d => d.y));
// Draw bars
MOCK_MAINTENANCE_CHART_DATA[0].data.forEach((point, index) => {
const barWidth = chartWidth / MOCK_MAINTENANCE_CHART_DATA[0].data.length - 10;
const x = padding + index * (chartWidth / MOCK_MAINTENANCE_CHART_DATA[0].data.length) + 5;
const barHeight = (point.y / maxValue) * chartHeight;
const y = padding + chartHeight - barHeight;
// Bar
ctx.fillStyle = MOCK_MAINTENANCE_CHART_DATA[0].color;
ctx.fillRect(x, y, barWidth, barHeight);
// Label
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label, x + barWidth / 2, canvas.height - 10);
ctx.fillText(`${point.y}`, x + barWidth / 2, y - 5);
});
// 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();
}
}
}} className="w-full h-full" />
</div>
</Card>
<Card className="p-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Tiempo de Inactividad</h3>
<div className="h-64">
<canvas ref={(canvas) => {
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
// Set canvas size
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw line chart for downtime
const padding = 40;
const chartWidth = canvas.width - 2 * padding;
const chartHeight = canvas.height - 2 * padding;
// Get max value for scaling
const maxValue = Math.max(...MOCK_MAINTENANCE_CHART_DATA[1].data.map(d => d.y));
// Draw line
ctx.strokeStyle = MOCK_MAINTENANCE_CHART_DATA[1].color;
ctx.lineWidth = 2;
ctx.beginPath();
MOCK_MAINTENANCE_CHART_DATA[1].data.forEach((point, index) => {
const x = padding + (index * chartWidth) / (MOCK_MAINTENANCE_CHART_DATA[1].data.length - 1);
const y = padding + chartHeight - ((point.y / maxValue) * chartHeight);
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// Draw points
ctx.fillStyle = MOCK_MAINTENANCE_CHART_DATA[1].color;
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
// Labels
ctx.fillStyle = '#374151';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label, x, canvas.height - 10);
ctx.fillText(`${point.y}h`, x, y - 10);
});
ctx.stroke();
// 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();
}
}
}} className="w-full h-full" />
</div>
</Card>
</div>
{/* Maintenance Schedule */}
<Card className="p-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Programación de Mantenimiento</h3>
<div className="space-y-4">
{MOCK_EQUIPMENT.map((equipment) => {
const nextMaintenanceDate = new Date(equipment.nextMaintenance);
const daysUntilMaintenance = Math.ceil((nextMaintenanceDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
const isOverdue = daysUntilMaintenance < 0;
return (
<div key={equipment.id} className="p-4 bg-[var(--bg-secondary)] rounded-lg border-l-4 border-blue-500">
<div className="flex items-center justify-between">
<div>
<h4 className="font-semibold text-[var(--text-primary)]">{equipment.name}</h4>
<p className="text-sm text-[var(--text-secondary)]">{equipment.model}</p>
</div>
<div className="text-right">
<p className={`font-medium ${isOverdue ? 'text-red-500' : 'text-[var(--text-primary)]'}`}>
{isOverdue ? 'Atrasado' : 'Programado'}
</p>
<p className="text-sm text-[var(--text-secondary)]">
{nextMaintenanceDate.toLocaleDateString('es-ES')}
</p>
</div>
</div>
<div className="mt-3 flex items-center justify-between">
<div className="text-sm">
<span className="text-[var(--text-secondary)]">Técnico: </span>
<span className="text-[var(--text-primary)]">Juan Pérez</span>
</div>
<Button
variant="outline"
size="sm"
>
<CalendarClock className="w-4 h-4 mr-2" />
Reagendar
</Button>
</div>
</div>
);
})}
</div>
</Card>
</div>
)}
{/* Status Tab */}
{activeTab === 'status' && (
<div className="space-y-6">
{/* Status Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="p-4 text-center">
<div className="text-3xl font-bold text-green-500">{equipmentStats.operational}</div>
<div className="text-sm text-[var(--text-secondary)]">Operativo</div>
</Card>
<Card className="p-4 text-center">
<div className="text-3xl font-bold text-yellow-500">{equipmentStats.warning}</div>
<div className="text-sm text-[var(--text-secondary)]">Advertencia</div>
</Card>
<Card className="p-4 text-center">
<div className="text-3xl font-bold text-blue-500">{equipmentStats.maintenance}</div>
<div className="text-sm text-[var(--text-secondary)]">Mantenimiento</div>
</Card>
<Card className="p-4 text-center">
<div className="text-3xl font-bold text-red-500">{equipmentStats.down}</div>
<div className="text-sm text-[var(--text-secondary)]">Fuera de Servicio</div>
</Card>
</div>
{/* Active Alerts */}
<Card className="p-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas Activas</h3>
<div className="space-y-3">
{MOCK_EQUIPMENT.flatMap(eq =>
eq.alerts.filter(a => !a.acknowledged).map(alert => (
<div
key={`${eq.id}-${alert.id}`}
className={`p-3 rounded border-l-4 ${
alert.type === 'critical' ? 'bg-red-50 border-red-500 dark:bg-red-900/20' :
alert.type === 'warning' ? 'bg-orange-50 border-orange-500 dark:bg-orange-900/20' :
'bg-blue-50 border-blue-500 dark:bg-blue-900/20'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<AlertTriangle className={`w-4 h-4 ${
alert.type === 'critical' ? 'text-red-500' :
alert.type === 'warning' ? 'text-orange-500' : 'text-blue-500'
}`} />
<span className="font-medium text-[var(--text-primary)]">{eq.name}</span>
</div>
<span className="text-xs text-[var(--text-secondary)]">
{new Date(alert.timestamp).toLocaleString('es-ES')}
</span>
</div>
<p className="text-[var(--text-secondary)] mt-2">{alert.message}</p>
<div className="flex justify-end mt-3">
<Button
variant="outline"
size="sm"
>
Acknowledge
</Button>
</div>
</div>
))
)}
{MOCK_EQUIPMENT.flatMap(eq => eq.alerts.filter(a => !a.acknowledged)).length === 0 && (
<div className="text-center py-8">
<CheckCircle className="mx-auto h-12 w-12 text-green-500" />
<h3 className="mt-2 text-lg font-medium text-[var(--text-primary)]">No hay alertas activas</h3>
<p className="mt-1 text-[var(--text-secondary)]">Todos los equipos están funcionando correctamente</p>
</div>
)}
</div>
</Card>
{/* Equipment Status Details */}
<Card className="p-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Detalles de Estado de Equipos</h3>
<div className="space-y-4">
{MOCK_EQUIPMENT.map((equipment) => {
const statusConfig = getStatusConfig(equipment.status);
return (
<div key={equipment.id} className="p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: statusConfig.color }}
/>
<div>
<h4 className="font-semibold text-[var(--text-primary)]">{equipment.name}</h4>
<p className="text-sm text-[var(--text-secondary)]">{equipment.model}</p>
</div>
</div>
<Badge
variant={equipment.status === 'operational' ? 'success' :
equipment.status === 'warning' ? 'warning' :
equipment.status === 'maintenance' ? 'info' : 'error'}
>
{statusConfig.text}
</Badge>
</div>
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-lg font-bold text-[var(--text-primary)]">{equipment.efficiency}%</div>
<div className="text-xs text-[var(--text-secondary)]">Eficiencia</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-[var(--text-primary)]">{equipment.uptime.toFixed(1)}%</div>
<div className="text-xs text-[var(--text-secondary)]">Tiempo de Actividad</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-[var(--text-primary)]">{equipment.energyUsage} kW</div>
<div className="text-xs text-[var(--text-secondary)]">Consumo Energético</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-[var(--text-primary)]">{equipment.utilizationToday}%</div>
<div className="text-xs text-[var(--text-secondary)]">Utilización Hoy</div>
</div>
</div>
</div>
);
})}
</div>
</Card>
</div>
)}
2025-09-23 19:24:22 +02:00
</div>
2025-09-23 22:11:34 +02:00
</Card>
</div>
2025-09-23 19:24:22 +02:00
);
};
const ProductionAnalyticsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
2025-09-23 22:11:34 +02:00
const { canAccessAnalytics } = useSubscription();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
// Check if user has access to advanced analytics (professional/enterprise)
const hasAdvancedAccess = canAccessAnalytics('advanced');
// If user doesn't have access to advanced analytics, show upgrade message
if (!hasAdvancedAccess) {
return (
<div className="space-y-6">
<PageHeader
title="Análisis de Producción"
description="Análisis avanzado y insights para profesionales y empresas"
/>
<Card className="p-8 text-center">
<Lock className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
Contenido exclusivo para planes Professional y Enterprise
</h3>
<p className="text-[var(--text-secondary)] mb-4">
El análisis avanzado de producción está disponible solo para usuarios con planes Professional o Enterprise.
Actualiza tu plan para acceder a todas las funcionalidades.
</p>
<Button
variant="primary"
size="md"
onClick={() => window.location.hash = '#/app/settings/profile'}
>
Actualizar Plan
</Button>
</Card>
</div>
);
}
2025-09-23 19:24:22 +02:00
return (
<div className="space-y-6">
<PageHeader
title="Análisis de Producción"
description="Análisis avanzado y insights para profesionales y empresas"
/>
{/* Key Metrics */}
<StatsGrid
stats={[
{
title: 'Eficiencia General',
value: '94%',
variant: 'success' as const,
icon: TrendingUp,
},
{
title: 'Costo Promedio',
value: '€2.45',
variant: 'info' as const,
icon: DollarSign,
},
{
title: 'Equipos Activos',
value: '8/9',
variant: 'warning' as const,
icon: Settings,
},
{
title: 'Calidad Promedio',
value: '9.2/10',
variant: 'success' as const,
icon: Activity,
}
]}
columns={4}
/>
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('overview')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'overview'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Resumen
</button>
<button
onClick={() => setActiveTab('costs')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'costs'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Costos
</button>
<button
onClick={() => setActiveTab('ai-insights')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'ai-insights'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
IA Insights
</button>
<button
2025-09-23 22:11:34 +02:00
onClick={() => setActiveTab('maquinaria')}
2025-09-23 19:24:22 +02:00
className={`py-2 px-1 border-b-2 font-medium text-sm ${
2025-09-23 22:11:34 +02:00
activeTab === 'maquinaria'
2025-09-23 19:24:22 +02:00
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
2025-09-23 22:11:34 +02:00
Maquinaria
2025-09-23 19:24:22 +02:00
</button>
<button
onClick={() => setActiveTab('quality')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Calidad
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="grid gap-6 lg:grid-cols-2">
2025-09-23 22:11:34 +02:00
<ProductionCostAnalytics />
<AIInsightsWidget />
<EquipmentStatusWidget />
2025-09-23 19:24:22 +02:00
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Resumen de Calidad</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Productos aprobados</span>
<span className="font-medium">96%</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Rechazos por calidad</span>
<span className="font-medium">2%</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Reprocesos</span>
<span className="font-medium">2%</span>
</div>
</div>
</Card>
</div>
)}
{activeTab === 'costs' && (
2025-09-23 22:11:34 +02:00
<div className="space-y-6">
<ProductionCostAnalytics />
2025-09-23 19:24:22 +02:00
</div>
)}
{activeTab === 'ai-insights' && (
<div className="grid gap-6">
2025-09-23 22:11:34 +02:00
<AIInsightsWidget />
2025-09-23 19:24:22 +02:00
</div>
)}
2025-09-23 22:11:34 +02:00
{activeTab === 'maquinaria' && (
<EquipmentAnalytics />
2025-09-23 19:24:22 +02:00
)}
{activeTab === 'quality' && (
<QualityDashboard />
)}
</div>
);
};
export default ProductionAnalyticsPage;