Add improved production UI 3

This commit is contained in:
Urtzi Alfaro
2025-09-23 19:24:22 +02:00
parent 7f871fc933
commit 7892c5a739
47 changed files with 6211 additions and 267 deletions

View File

@@ -0,0 +1,252 @@
import React, { useState } from 'react';
import { TrendingUp, DollarSign, Activity, AlertTriangle, Settings } from 'lucide-react';
import { Card, StatsGrid, Button } from '../../../components/ui';
import { PageHeader } from '../../../components/layout';
import { QualityDashboard, EquipmentManager } from '../../../components/domain/production';
// Production Cost Monitor Component (placeholder)
const ProductionCostMonitor: React.FC = () => {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Monitor de Costos de Producción</h3>
<DollarSign className="w-5 h-5 text-[var(--color-primary)]" />
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-[var(--surface-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)]">Costo por unidad</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">2.45</p>
<p className="text-xs text-green-600">-8% vs mes anterior</p>
</div>
<div className="p-4 bg-[var(--surface-secondary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)]">Eficiencia de costos</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">92%</p>
<p className="text-xs text-green-600">+3% vs objetivo</p>
</div>
</div>
<div className="h-64 bg-[var(--surface-secondary)] rounded-lg flex items-center justify-center">
<p className="text-[var(--text-secondary)]">Gráfico de tendencias de costos</p>
</div>
</div>
</Card>
);
};
// AI Insights Component (placeholder)
const AIInsights: React.FC = () => {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Insights de IA</h3>
<Activity className="w-5 h-5 text-[var(--color-primary)]" />
</div>
<div className="space-y-4">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start gap-3">
<TrendingUp className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<h4 className="font-medium text-blue-900">Optimización de Producción</h4>
<p className="text-sm text-blue-700">Se detectó que producir croissants los martes reduce costos en 15%</p>
</div>
</div>
</div>
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-amber-600 mt-0.5" />
<div>
<h4 className="font-medium text-amber-900">Predicción de Demanda</h4>
<p className="text-sm text-amber-700">Aumento del 25% en demanda de pan integral previsto para el fin de semana</p>
</div>
</div>
</div>
</div>
</Card>
);
};
// Equipment Status Component (placeholder)
const EquipmentStatus: React.FC = () => {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Estado de Equipos</h3>
<Settings className="w-5 h-5 text-[var(--color-primary)]" />
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg">
<div>
<p className="font-medium text-green-900">Horno Principal</p>
<p className="text-sm text-green-700">Funcionando óptimamente</p>
</div>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
</div>
<div className="flex items-center justify-between p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div>
<p className="font-medium text-yellow-900">Mezcladora #2</p>
<p className="text-sm text-yellow-700">Mantenimiento requerido en 3 días</p>
</div>
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
</div>
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg">
<div>
<p className="font-medium text-green-900">Refrigerador</p>
<p className="text-sm text-green-700">Temperatura estable</p>
</div>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
</div>
</div>
</Card>
);
};
const ProductionAnalyticsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
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
onClick={() => setActiveTab('equipment')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'equipment'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Equipos
</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">
<ProductionCostMonitor />
<AIInsights />
<EquipmentStatus />
<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' && (
<div className="grid gap-6">
<ProductionCostMonitor />
</div>
)}
{activeTab === 'ai-insights' && (
<div className="grid gap-6">
<AIInsights />
</div>
)}
{activeTab === 'equipment' && (
<div className="grid gap-6">
<EquipmentStatus />
<EquipmentManager />
</div>
)}
{activeTab === 'quality' && (
<QualityDashboard />
)}
</div>
);
};
export default ProductionAnalyticsPage;

View File

@@ -4,4 +4,5 @@ export * from './recipes';
export * from './procurement';
export * from './orders';
export * from './suppliers';
export * from './pos';
export * from './pos';
export * from './maquinaria';

View File

@@ -0,0 +1,604 @@
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, AlertTriangle, Settings, CheckCircle, Eye, Clock, Zap, Wrench, Thermometer, Activity, Search, Filter, Download, TrendingUp, Bell, History, Calendar, Edit, Trash2 } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor } from '../../../../components/ui';
import { LoadingSpinner } from '../../../../components/shared';
import { PageHeader } from '../../../../components/layout';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { Equipment } from '../../../../types/equipment';
const MOCK_EQUIPMENT: 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
}
}
];
const MaquinariaPage: React.FC = () => {
const { t } = useTranslation(['equipment', 'common']);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<Equipment['status'] | 'all'>('all');
const [selectedItem, setSelectedItem] = useState<Equipment | null>(null);
const [showMaintenanceModal, setShowMaintenanceModal] = useState(false);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
// Mock functions for equipment actions - these would be replaced with actual API calls
const handleCreateEquipment = () => {
console.log('Create new equipment');
// Implementation would go here
};
const handleEditEquipment = (equipmentId: string) => {
console.log('Edit equipment:', equipmentId);
// Implementation would go here
};
const handleScheduleMaintenance = (equipmentId: string) => {
console.log('Schedule maintenance for equipment:', equipmentId);
// Implementation would go here
};
const handleAcknowledgeAlert = (equipmentId: string, alertId: string) => {
console.log('Acknowledge alert:', alertId, 'for equipment:', equipmentId);
// Implementation would go here
};
const handleViewMaintenanceHistory = (equipmentId: string) => {
console.log('View maintenance history for equipment:', equipmentId);
// Implementation would go here
};
const filteredEquipment = useMemo(() => {
return 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;
});
}, [MOCK_EQUIPMENT, searchTerm, statusFilter]);
const equipmentStats = useMemo(() => {
const total = MOCK_EQUIPMENT.length;
const operational = MOCK_EQUIPMENT.filter(e => e.status === 'operational').length;
const warning = MOCK_EQUIPMENT.filter(e => e.status === 'warning').length;
const maintenance = MOCK_EQUIPMENT.filter(e => e.status === 'maintenance').length;
const down = MOCK_EQUIPMENT.filter(e => e.status === 'down').length;
const avgEfficiency = MOCK_EQUIPMENT.reduce((sum, e) => sum + e.efficiency, 0) / total;
const avgUptime = MOCK_EQUIPMENT.reduce((sum, e) => sum + e.uptime, 0) / total;
const totalAlerts = MOCK_EQUIPMENT.reduce((sum, e) => sum + e.alerts.filter(a => !a.acknowledged).length, 0);
return {
total,
operational,
warning,
maintenance,
down,
avgEfficiency,
avgUptime,
totalAlerts
};
}, [MOCK_EQUIPMENT]);
const getStatusConfig = (status: Equipment['status']) => {
const configs = {
operational: { color: getStatusColor('completed'), text: t('equipment_status.operational'), icon: CheckCircle },
warning: { color: getStatusColor('warning'), text: t('equipment_status.warning'), icon: AlertTriangle },
maintenance: { color: getStatusColor('info'), text: t('equipment_status.maintenance'), icon: Wrench },
down: { color: getStatusColor('error'), text: t('equipment_status.down'), icon: AlertTriangle }
};
return configs[status];
};
const getTypeIcon = (type: Equipment['type']) => {
const icons = {
oven: Thermometer,
mixer: Activity,
proofer: Settings,
freezer: Zap,
packaging: Settings,
other: Settings
};
return icons[type];
};
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
};
const stats = [
{
title: t('labels.total_equipment'),
value: equipmentStats.total,
icon: Settings,
variant: 'default' as const
},
{
title: t('labels.operational'),
value: equipmentStats.operational,
icon: CheckCircle,
variant: 'success' as const,
subtitle: `${((equipmentStats.operational / equipmentStats.total) * 100).toFixed(1)}%`
},
{
title: t('labels.avg_efficiency'),
value: `${equipmentStats.avgEfficiency.toFixed(1)}%`,
icon: TrendingUp,
variant: equipmentStats.avgEfficiency >= 90 ? 'success' as const : 'warning' as const
},
{
title: t('labels.active_alerts'),
value: equipmentStats.totalAlerts,
icon: Bell,
variant: equipmentStats.totalAlerts === 0 ? 'success' as const : 'error' as const
}
];
const handleShowMaintenanceDetails = (equipment: Equipment) => {
setSelectedItem(equipment);
setShowMaintenanceModal(true);
};
const handleCloseMaintenanceModal = () => {
setShowMaintenanceModal(false);
setSelectedItem(null);
};
// Loading state
if (!tenantId) {
return (
<div className="flex items-center justify-center min-h-64">
<LoadingSpinner text="Cargando datos..." />
</div>
);
}
return (
<div className="space-y-6">
<PageHeader
title={t('title')}
description={t('subtitle')}
actions={[
{
id: "add-new-equipment",
label: t('actions.add_equipment'),
variant: "primary" as const,
icon: Plus,
onClick: handleCreateEquipment,
tooltip: t('actions.add_equipment'),
size: "md"
}
]}
/>
{/* Stats Grid */}
<StatsGrid
stats={stats}
columns={3}
/>
{/* Controls */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
<Input
placeholder={t('common:forms.search_placeholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Filter className="w-4 h-4 text-[var(--text-tertiary)]" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as Equipment['status'] | 'all')}
className="px-3 py-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
<option value="all">{t('common:forms.select_option')}</option>
<option value="operational">{t('equipment_status.operational')}</option>
<option value="warning">{t('equipment_status.warning')}</option>
<option value="maintenance">{t('equipment_status.maintenance')}</option>
<option value="down">{t('equipment_status.down')}</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);
// Calculate maintenance status
const nextMaintenanceDate = new Date(equipment.nextMaintenance);
const daysUntilMaintenance = Math.ceil((nextMaintenanceDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
const isOverdue = daysUntilMaintenance < 0;
return (
<StatusCard
key={equipment.id}
id={equipment.id}
statusIndicator={statusConfig}
title={equipment.name}
subtitle={equipment.location}
primaryValue={`${equipment.efficiency}%`}
primaryValueLabel={t('fields.efficiency')}
secondaryInfo={{
label: t('fields.uptime'),
value: `${equipment.uptime.toFixed(1)}%`
}}
actions={[
{
label: t('actions.view_details'),
icon: Eye,
variant: 'primary',
priority: 'primary',
onClick: () => handleShowMaintenanceDetails(equipment)
},
{
label: t('actions.view_history'),
icon: History,
priority: 'secondary',
onClick: () => handleViewMaintenanceHistory(equipment.id)
},
{
label: t('actions.schedule_maintenance'),
icon: Wrench,
priority: 'secondary',
onClick: () => handleScheduleMaintenance(equipment.id)
}
]}
/>
);
})}
</div>
{/* Empty State */}
{filteredEquipment.length === 0 && (
<div className="text-center py-12">
<Settings 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">
{t('common:forms.no_results')}
</h3>
<p className="text-[var(--text-secondary)] mb-4">
{t('common:forms.empty_state')}
</p>
<Button
onClick={handleCreateEquipment}
variant="primary"
size="md"
className="font-medium px-4 sm:px-6 py-2 sm:py-3 shadow-sm hover:shadow-md transition-all duration-200"
>
<Plus className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2 flex-shrink-0" />
<span className="text-sm sm:text-base">{t('actions.add_equipment')}</span>
</Button>
</div>
)}
{/* Maintenance Details Modal */}
{selectedItem && showMaintenanceModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div className="bg-[var(--bg-primary)] rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto my-8">
<div className="p-4 sm:p-6">
<div className="flex items-center justify-between mb-4 sm:mb-6">
<div>
<h2 className="text-lg sm:text-xl font-semibold text-[var(--text-primary)]">
{selectedItem.name}
</h2>
<p className="text-[var(--text-secondary)] text-sm">
{selectedItem.model} - {selectedItem.serialNumber}
</p>
</div>
<button
onClick={handleCloseMaintenanceModal}
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)] p-1"
>
<svg className="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4 sm:space-y-6">
{/* Equipment Status */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.status')}</h3>
<div className="flex items-center space-x-2">
<div
className="w-2 h-2 sm:w-3 sm:h-3 rounded-full"
style={{ backgroundColor: getStatusConfig(selectedItem.status).color }}
/>
<span className="text-[var(--text-primary)] text-sm sm:text-base">
{t(`equipment_status.${selectedItem.status}`)}
</span>
</div>
</div>
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.efficiency')}</h3>
<div className="text-lg sm:text-2xl font-bold text-[var(--text-primary)]">
{selectedItem.efficiency}%
</div>
</div>
</div>
{/* Maintenance Information */}
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.title')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.last')}</p>
<p className="text-[var(--text-primary)] text-sm sm:text-base">
{new Date(selectedItem.lastMaintenance).toLocaleDateString('es-ES')}
</p>
</div>
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.next')}</p>
<p className={`font-medium ${(new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime()) ? 'text-red-500' : 'text-[var(--text-primary)]'} text-sm sm:text-base`}>
{new Date(selectedItem.nextMaintenance).toLocaleDateString('es-ES')}
</p>
</div>
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.interval')}</p>
<p className="text-[var(--text-primary)] text-sm sm:text-base">
{selectedItem.maintenanceInterval} {t('common:units.days')}
</p>
</div>
</div>
{new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime() && (
<div className="mt-3 p-2 bg-red-50 dark:bg-red-900/20 rounded border-l-2 border-red-500">
<div className="flex items-center space-x-2">
<AlertTriangle className="w-4 h-4 text-red-500" />
<span className="text-xs sm:text-sm font-medium text-red-700 dark:text-red-300">
{t('maintenance.overdue')}
</span>
</div>
</div>
)}
</div>
{/* Active Alerts */}
{selectedItem.alerts.filter(a => !a.acknowledged).length > 0 && (
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('alerts.title')}</h3>
<div className="space-y-2 sm:space-y-3">
{selectedItem.alerts.filter(a => !a.acknowledged).map((alert) => (
<div
key={alert.id}
className={`p-2 sm:p-3 rounded border-l-2 ${
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-3 h-3 sm:w-4 sm: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)] text-xs sm:text-sm">
{alert.message}
</span>
</div>
<span className="text-xs text-[var(--text-secondary)] hidden sm:block">
{new Date(alert.timestamp).toLocaleString('es-ES')}
</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Maintenance History */}
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.history')}</h3>
<div className="space-y-3 sm:space-y-4">
{selectedItem.maintenanceHistory.map((history) => (
<div key={history.id} className="border-b border-[var(--border-primary)] pb-2 sm:pb-3 last:border-0 last:pb-0">
<div className="flex justify-between items-start">
<div>
<h4 className="font-medium text-[var(--text-primary)] text-sm">{history.description}</h4>
<p className="text-xs text-[var(--text-secondary)]">
{new Date(history.date).toLocaleDateString('es-ES')} - {history.technician}
</p>
</div>
<span className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
{t(`maintenance.type.${history.type}`)}
</span>
</div>
<div className="mt-1 sm:mt-2 flex flex-wrap gap-2">
<span className="text-xs">
<span className="text-[var(--text-secondary)]">{t('common:actions.cost')}:</span>
<span className="font-medium text-[var(--text-primary)]"> {history.cost}</span>
</span>
<span className="text-xs">
<span className="text-[var(--text-secondary)]">{t('fields.uptime')}:</span>
<span className="font-medium text-[var(--text-primary)]"> {history.downtime}h</span>
</span>
</div>
{history.partsUsed.length > 0 && (
<div className="mt-1 sm:mt-2">
<span className="text-xs text-[var(--text-secondary)]">{t('fields.parts')}:</span>
<div className="flex flex-wrap gap-1 mt-1">
{history.partsUsed.map((part, index) => (
<span key={index} className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
{part}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
<div className="flex justify-end space-x-2 sm:space-x-3 mt-4 sm:mt-6">
<Button variant="outline" size="sm" onClick={handleCloseMaintenanceModal}>
{t('common:actions.close')}
</Button>
<Button variant="primary" size="sm" onClick={() => selectedItem && handleScheduleMaintenance(selectedItem.id)}>
{t('actions.schedule_maintenance')}
</Button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default MaquinariaPage;

View File

@@ -0,0 +1 @@
export { default as MaquinariaPage } from './MaquinariaPage';

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Plus, ShoppingCart, Truck, Euro, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, X, Save, Building2, Play } from 'lucide-react';
import { Plus, ShoppingCart, Truck, Euro, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, X, Save, Building2, Play, Zap, User } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
@@ -28,6 +28,7 @@ const ProcurementPage: React.FC = () => {
const [selectedRequirement, setSelectedRequirement] = useState<any>(null);
const [showCreatePurchaseOrderModal, setShowCreatePurchaseOrderModal] = useState(false);
const [selectedRequirementsForPO, setSelectedRequirementsForPO] = useState<any[]>([]);
const [isAIMode, setIsAIMode] = useState(true);
const [generatePlanForm, setGeneratePlanForm] = useState({
plan_date: new Date().toISOString().split('T')[0],
planning_horizon_days: 14,
@@ -253,28 +254,63 @@ const ProcurementPage: React.FC = () => {
return (
<div className="space-y-6">
<PageHeader
title="Planificación de Compras"
description="Administra planes de compras, requerimientos y análisis de procurement"
actions={[
{
id: "create-po",
label: "Crear Orden de Compra",
variant: "primary" as const,
icon: Plus,
onClick: () => {
// Open the purchase order modal with empty requirements
// This allows manual creation of purchase orders without procurement plans
setSelectedRequirementsForPO([]);
setShowCreatePurchaseOrderModal(true);
}
},
{
id: "trigger",
label: triggerSchedulerMutation.isPending ? "Ejecutando..." : "Ejecutar Programador",
variant: "outline" as const,
icon: triggerSchedulerMutation.isPending ? Loader : Calendar,
onClick: () => {
<div className="flex items-center justify-between mb-6">
<PageHeader
title="Planificación de Compras"
description="Administra planes de compras, requerimientos y análisis de procurement"
/>
<div className="flex items-center gap-4">
{/* AI/Manual Mode Segmented Control */}
<div className="inline-flex p-1 bg-[var(--surface-secondary)] rounded-xl border border-[var(--border-primary)] shadow-sm">
<button
onClick={() => setIsAIMode(true)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
${isAIMode
? 'bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
}
`}
>
<Zap className="w-4 h-4" />
Automático IA
</button>
<button
onClick={() => setIsAIMode(false)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
${!isAIMode
? 'bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
}
`}
>
<User className="w-4 h-4" />
Manual
</button>
</div>
{/* Action Buttons */}
{!isAIMode && (
<Button
variant="primary"
onClick={() => {
// Open the purchase order modal with empty requirements
// This allows manual creation of purchase orders without procurement plans
setSelectedRequirementsForPO([]);
setShowCreatePurchaseOrderModal(true);
}}
className="flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Crear Orden de Compra
</Button>
)}
{/* Testing button - keep for development */}
<Button
variant="outline"
onClick={() => {
triggerSchedulerMutation.mutate(tenantId, {
onSuccess: (data) => {
// Scheduler executed successfully
@@ -287,11 +323,19 @@ const ProcurementPage: React.FC = () => {
// toast.error('Error ejecutando el programador de compras');
}
})
},
disabled: triggerSchedulerMutation.isPending
}
]}
/>
}}
disabled={triggerSchedulerMutation.isPending}
className="flex items-center gap-2"
>
{triggerSchedulerMutation.isPending ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<Calendar className="w-4 h-4" />
)}
{triggerSchedulerMutation.isPending ? "Ejecutando..." : "Ejecutar Programador"}
</Button>
</div>
</div>
{/* Stats Grid */}
{isDashboardLoading ? (

View File

@@ -1,11 +1,11 @@
import React, { useState, useMemo } from 'react';
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusModal } from '../../../../components/ui';
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package, Zap, User, PlusCircle } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusModal, Toggle } from '../../../../components/ui';
import { statusColors } from '../../../../styles/colors';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { LoadingSpinner } from '../../../../components/shared';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityDashboard, EquipmentManager, CreateProductionBatchModal, ProductionStatusCard, QualityCheckModal } from '../../../../components/domain/production';
import { ProductionSchedule, BatchTracker, QualityDashboard, EquipmentManager, CreateProductionBatchModal, ProductionStatusCard, QualityCheckModal, ProcessStageTracker, CompactProcessStageTracker } from '../../../../components/domain/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import {
useProductionDashboard,
@@ -24,6 +24,7 @@ import {
ProductionPriorityEnum
} from '../../../../api';
import { useProductionEnums } from '../../../../utils/enumHelpers';
import { ProcessStage } from '../../../../api/types/qualityTemplates';
const ProductionPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('schedule');
@@ -33,6 +34,7 @@ const ProductionPage: React.FC = () => {
const [showCreateModal, setShowCreateModal] = useState(false);
const [showQualityModal, setShowQualityModal] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [isAIMode, setIsAIMode] = useState(true);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
@@ -68,6 +70,133 @@ const ProductionPage: React.FC = () => {
}
};
// Stage management handlers
const handleStageAdvance = async (batchId: string, currentStage: ProcessStage) => {
const stages = Object.values(ProcessStage);
const currentIndex = stages.indexOf(currentStage);
const nextStage = stages[currentIndex + 1];
if (nextStage) {
try {
await updateBatchStatusMutation.mutateAsync({
batchId,
updates: {
current_process_stage: nextStage,
process_stage_history: {
[currentStage]: { end_time: new Date().toISOString() },
[nextStage]: { start_time: new Date().toISOString() }
}
}
});
} catch (error) {
console.error('Error advancing stage:', error);
}
} else {
// Final stage - mark as completed
await updateBatchStatusMutation.mutateAsync({
batchId,
updates: { status: ProductionStatusEnum.COMPLETED }
});
}
};
const handleStageStart = async (batchId: string, stage: ProcessStage) => {
try {
await updateBatchStatusMutation.mutateAsync({
batchId,
updates: {
status: ProductionStatusEnum.IN_PROGRESS,
current_process_stage: stage,
process_stage_history: {
[stage]: { start_time: new Date().toISOString() }
}
}
});
} catch (error) {
console.error('Error starting stage:', error);
}
};
const handleQualityCheckForStage = (batch: ProductionBatchResponse, stage: ProcessStage) => {
setSelectedBatch(batch);
setShowQualityModal(true);
// The QualityCheckModal should be enhanced to handle stage-specific checks
};
// Helper function to generate mock process stage data for the selected batch
const generateMockProcessStageData = (batch: ProductionBatchResponse) => {
// Mock data based on batch status - this would come from the API in real implementation
const mockProcessStage = {
current: batch.status === ProductionStatusEnum.PENDING ? 'mixing' as const :
batch.status === ProductionStatusEnum.IN_PROGRESS ? 'baking' as const :
batch.status === ProductionStatusEnum.QUALITY_CHECK ? 'cooling' as const :
'finishing' as const,
history: batch.status !== ProductionStatusEnum.PENDING ? [
{ stage: 'mixing' as const, timestamp: batch.actual_start_time || batch.planned_start_time, duration: 30 },
...(batch.status === ProductionStatusEnum.IN_PROGRESS || batch.status === ProductionStatusEnum.QUALITY_CHECK || batch.status === ProductionStatusEnum.COMPLETED ? [
{ stage: 'proofing' as const, timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), duration: 90 },
{ stage: 'shaping' as const, timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(), duration: 15 }
] : []),
...(batch.status === ProductionStatusEnum.QUALITY_CHECK || batch.status === ProductionStatusEnum.COMPLETED ? [
{ stage: 'baking' as const, timestamp: new Date(Date.now() - 30 * 60 * 1000).toISOString(), duration: 45 }
] : [])
] : [],
pendingQualityChecks: batch.status === ProductionStatusEnum.IN_PROGRESS ? [
{
id: 'qc1',
name: 'Control de temperatura interna',
stage: 'baking' as const,
isRequired: true,
isCritical: true,
status: 'pending' as const,
checkType: 'temperature' as const
}
] : batch.status === ProductionStatusEnum.QUALITY_CHECK ? [
{
id: 'qc2',
name: 'Inspección visual final',
stage: 'cooling' as const,
isRequired: true,
isCritical: false,
status: 'pending' as const,
checkType: 'visual' as const
}
] : [],
completedQualityChecks: batch.status === ProductionStatusEnum.COMPLETED ? [
{
id: 'qc1',
name: 'Control de temperatura interna',
stage: 'baking' as const,
isRequired: true,
isCritical: true,
status: 'completed' as const,
checkType: 'temperature' as const
},
{
id: 'qc2',
name: 'Inspección visual final',
stage: 'cooling' as const,
isRequired: true,
isCritical: false,
status: 'completed' as const,
checkType: 'visual' as const
}
] : batch.status === ProductionStatusEnum.IN_PROGRESS ? [
{
id: 'qc3',
name: 'Verificación de masa',
stage: 'mixing' as const,
isRequired: true,
isCritical: false,
status: 'completed' as const,
checkType: 'visual' as const
}
] : []
};
return mockProcessStage;
};
const batches = activeBatchesData?.batches || [];
@@ -139,19 +268,55 @@ const ProductionPage: React.FC = () => {
return (
<div className="space-y-6">
<PageHeader
title="Gestión de Producción"
description="Planifica y controla la producción diaria de tu panadería"
actions={[
{
id: "new",
label: "Nueva Orden de Producción",
variant: "primary" as const,
icon: Plus,
onClick: () => setShowCreateModal(true)
}
]}
/>
<div className="flex items-center justify-between mb-6">
<PageHeader
title="Gestión de Producción"
description="Planifica y controla la producción diaria de tu panadería"
/>
<div className="flex items-center gap-4">
{/* AI/Manual Mode Segmented Control */}
<div className="inline-flex p-1 bg-[var(--surface-secondary)] rounded-xl border border-[var(--border-primary)] shadow-sm">
<button
onClick={() => setIsAIMode(true)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
${isAIMode
? 'bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
}
`}
>
<Zap className="w-4 h-4" />
Automático IA
</button>
<button
onClick={() => setIsAIMode(false)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
${!isAIMode
? 'bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
}
`}
>
<User className="w-4 h-4" />
Manual
</button>
</div>
{!isAIMode && (
<Button
variant="primary"
size="md"
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2.5 px-6 py-3 text-sm font-semibold tracking-wide shadow-lg hover:shadow-xl transition-all duration-200"
>
<PlusCircle className="w-5 h-5" />
Nueva Orden de Producción
</Button>
)}
</div>
</div>
{/* Production Stats */}
<StatsGrid
@@ -209,26 +374,6 @@ const ProductionPage: React.FC = () => {
>
Programación
</button>
<button
onClick={() => setActiveTab('quality-dashboard')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality-dashboard'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Dashboard Calidad
</button>
<button
onClick={() => setActiveTab('equipment')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'equipment'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Equipos
</button>
</nav>
</div>
@@ -342,13 +487,6 @@ const ProductionPage: React.FC = () => {
)}
{activeTab === 'quality-dashboard' && (
<QualityDashboard />
)}
{activeTab === 'equipment' && (
<EquipmentManager />
)}
{/* Production Batch Modal */}
{showBatchModal && selectedBatch && (
@@ -436,36 +574,23 @@ const ProductionPage: React.FC = () => {
]
},
{
title: 'Etapas de Producción',
title: 'Seguimiento de Proceso',
icon: Timer,
fields: [
{
label: 'Etapa Actual',
value: selectedBatch.status === ProductionStatusEnum.IN_PROGRESS
? 'En progreso'
: selectedBatch.status === ProductionStatusEnum.QUALITY_CHECK
? 'Control de calidad'
: productionEnums.getProductionStatusLabel(selectedBatch.status)
},
{
label: 'Progreso del Lote',
value: selectedBatch.status === ProductionStatusEnum.COMPLETED
? '100%'
: selectedBatch.status === ProductionStatusEnum.PENDING
? '0%'
: '50%'
},
{
label: 'Tiempo Transcurrido',
value: selectedBatch.actual_start_time
? `${Math.round((new Date().getTime() - new Date(selectedBatch.actual_start_time).getTime()) / (1000 * 60))} minutos`
: 'No iniciado'
},
{
label: 'Tiempo Estimado Restante',
value: selectedBatch.planned_end_time && selectedBatch.actual_start_time
? `${Math.max(0, Math.round((new Date(selectedBatch.planned_end_time).getTime() - new Date().getTime()) / (1000 * 60)))} minutos`
: 'Calculando...'
label: '',
value: (
<CompactProcessStageTracker
processStage={generateMockProcessStageData(selectedBatch)}
onAdvanceStage={(currentStage) => handleStageAdvance(selectedBatch.id, currentStage)}
onQualityCheck={(checkId) => {
setShowQualityModal(true);
console.log('Opening quality check:', checkId);
}}
className="w-full"
/>
),
span: 2
}
]
},

View File

@@ -1,5 +1,5 @@
import React, { useState, useMemo } from 'react';
import { Plus, Star, Clock, Euro, Package, Eye, Edit, ChefHat, Timer } from 'lucide-react';
import { Plus, Star, Clock, Euro, Package, Eye, Edit, ChefHat, Timer, CheckCircle } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { LoadingSpinner } from '../../../../components/shared';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
@@ -7,7 +7,10 @@ import { PageHeader } from '../../../../components/layout';
import { useRecipes, useRecipeStatistics, useCreateRecipe, useUpdateRecipe, useDeleteRecipe } from '../../../../api/hooks/recipes';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import type { RecipeResponse, RecipeCreate, MeasurementUnit } from '../../../../api/types/recipes';
import { useQualityTemplatesForRecipe } from '../../../../api/hooks/qualityTemplates';
import { ProcessStage, type RecipeQualityConfiguration } from '../../../../api/types/qualityTemplates';
import { CreateRecipeModal } from '../../../../components/domain/recipes';
import { QualityCheckConfigurationModal } from '../../../../components/domain/recipes/QualityCheckConfigurationModal';
const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
@@ -15,6 +18,7 @@ const RecipesPage: React.FC = () => {
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedRecipe, setSelectedRecipe] = useState<RecipeResponse | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showQualityConfigModal, setShowQualityConfigModal] = useState(false);
const [editedRecipe, setEditedRecipe] = useState<Partial<RecipeResponse>>({});
const currentTenant = useCurrentTenant();
@@ -76,6 +80,29 @@ const RecipesPage: React.FC = () => {
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const getQualityConfigSummary = (config: any) => {
if (!config || !config.stages) return 'No configurado';
const stageLabels = {
[ProcessStage.MIXING]: 'Mezclado',
[ProcessStage.PROOFING]: 'Fermentación',
[ProcessStage.SHAPING]: 'Formado',
[ProcessStage.BAKING]: 'Horneado',
[ProcessStage.COOLING]: 'Enfriado',
[ProcessStage.PACKAGING]: 'Empaquetado',
[ProcessStage.FINISHING]: 'Acabado'
};
const configuredStages = Object.keys(config.stages)
.filter(stage => config.stages[stage]?.template_ids?.length > 0)
.map(stage => stageLabels[stage as ProcessStage] || stage);
if (configuredStages.length === 0) return 'No configurado';
if (configuredStages.length <= 2) return `Configurado para: ${configuredStages.join(', ')}`;
return `Configurado para ${configuredStages.length} etapas`;
};
const filteredRecipes = useMemo(() => {
if (!searchTerm) return recipes;
@@ -222,6 +249,31 @@ const RecipesPage: React.FC = () => {
}
};
// Handle saving quality configuration
const handleSaveQualityConfiguration = async (config: RecipeQualityConfiguration) => {
if (!selectedRecipe) return;
try {
await updateRecipeMutation.mutateAsync({
id: selectedRecipe.id,
data: {
quality_check_configuration: config
}
});
// Update local state
setSelectedRecipe({
...selectedRecipe,
quality_check_configuration: config
});
console.log('Quality configuration updated successfully');
} catch (error) {
console.error('Error updating quality configuration:', error);
throw error;
}
};
// Get current value for field (edited value or original)
const getFieldValue = (originalValue: any, fieldKey: string) => {
return editedRecipe[fieldKey as keyof RecipeResponse] !== undefined
@@ -356,6 +408,26 @@ const RecipesPage: React.FC = () => {
readonly: true // For now, ingredients editing can be complex, so we'll keep it read-only
}
]
},
{
title: 'Control de Calidad',
icon: CheckCircle,
fields: [
{
label: 'Configuración de controles',
value: selectedRecipe.quality_check_configuration
? getQualityConfigSummary(selectedRecipe.quality_check_configuration)
: 'No configurado',
type: modalMode === 'edit' ? 'button' : 'text',
span: 2,
buttonText: modalMode === 'edit' ? 'Configurar Controles de Calidad' : undefined,
onButtonClick: modalMode === 'edit' ? () => {
// Open quality check configuration modal
setShowQualityConfigModal(true);
} : undefined,
readonly: modalMode !== 'edit'
}
]
}
];
};
@@ -509,6 +581,17 @@ const RecipesPage: React.FC = () => {
onClose={() => setShowCreateModal(false)}
onCreateRecipe={handleCreateRecipe}
/>
{/* Quality Check Configuration Modal */}
{selectedRecipe && (
<QualityCheckConfigurationModal
isOpen={showQualityConfigModal}
onClose={() => setShowQualityConfigModal(false)}
recipe={selectedRecipe}
onSaveConfiguration={handleSaveQualityConfiguration}
isLoading={updateRecipeMutation.isPending}
/>
)}
</div>
);
};