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;
|