import React, { useState, useCallback, useEffect } from 'react'; import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui'; import { productionService, type ProductionBatchResponse, ProductionBatchStatus, ProductionPriority } from '../../../services/api/production.service'; import type { ProductionBatch, QualityCheck } from '../../../types/production.types'; interface BatchTrackerProps { className?: string; batchId?: string; onStageUpdate?: (batch: ProductionBatch, newStage: ProductionStage) => void; onQualityCheckRequired?: (batch: ProductionBatch, stage: ProductionStage) => void; } interface ProductionStage { id: string; name: string; spanishName: string; icon: string; estimatedMinutes: number; requiresQualityCheck: boolean; criticalControlPoint: boolean; completedAt?: string; notes?: string; nextStages: string[]; temperature?: { min: number; max: number; unit: string; }; } const PRODUCTION_STAGES: Record = { mixing: { id: 'mixing', name: 'Mixing', spanishName: 'Amasado', icon: '🥄', estimatedMinutes: 15, requiresQualityCheck: true, criticalControlPoint: true, nextStages: ['resting'], temperature: { min: 22, max: 26, unit: '°C' }, }, resting: { id: 'resting', name: 'Resting', spanishName: 'Reposo', icon: '⏰', estimatedMinutes: 60, requiresQualityCheck: false, criticalControlPoint: false, nextStages: ['shaping', 'fermentation'], }, shaping: { id: 'shaping', name: 'Shaping', spanishName: 'Formado', icon: '✋', estimatedMinutes: 20, requiresQualityCheck: true, criticalControlPoint: false, nextStages: ['fermentation'], }, fermentation: { id: 'fermentation', name: 'Fermentation', spanishName: 'Fermentado', icon: '🫧', estimatedMinutes: 120, requiresQualityCheck: true, criticalControlPoint: true, nextStages: ['baking'], temperature: { min: 28, max: 32, unit: '°C' }, }, baking: { id: 'baking', name: 'Baking', spanishName: 'Horneado', icon: '🔥', estimatedMinutes: 45, requiresQualityCheck: true, criticalControlPoint: true, nextStages: ['cooling'], temperature: { min: 180, max: 220, unit: '°C' }, }, cooling: { id: 'cooling', name: 'Cooling', spanishName: 'Enfriado', icon: '❄️', estimatedMinutes: 90, requiresQualityCheck: false, criticalControlPoint: false, nextStages: ['packaging'], temperature: { min: 18, max: 25, unit: '°C' }, }, packaging: { id: 'packaging', name: 'Packaging', spanishName: 'Empaquetado', icon: '📦', estimatedMinutes: 15, requiresQualityCheck: true, criticalControlPoint: false, nextStages: ['completed'], }, completed: { id: 'completed', name: 'Completed', spanishName: 'Completado', icon: '✅', estimatedMinutes: 0, requiresQualityCheck: false, criticalControlPoint: false, nextStages: [], }, }; const STATUS_COLORS = { [ProductionBatchStatus.PLANNED]: 'bg-blue-100 text-blue-800 border-blue-200', [ProductionBatchStatus.IN_PROGRESS]: 'bg-yellow-100 text-yellow-800 border-yellow-200', [ProductionBatchStatus.COMPLETED]: 'bg-green-100 text-green-800 border-green-200', [ProductionBatchStatus.CANCELLED]: 'bg-red-100 text-red-800 border-red-200', [ProductionBatchStatus.ON_HOLD]: 'bg-gray-100 text-gray-800 border-gray-200', }; const PRIORITY_COLORS = { [ProductionPriority.LOW]: 'bg-gray-100 text-gray-800', [ProductionPriority.NORMAL]: 'bg-blue-100 text-blue-800', [ProductionPriority.HIGH]: 'bg-orange-100 text-orange-800', [ProductionPriority.URGENT]: 'bg-red-100 text-red-800', }; export const BatchTracker: React.FC = ({ className = '', batchId, onStageUpdate, onQualityCheckRequired, }) => { const [batches, setBatches] = useState([]); const [selectedBatch, setSelectedBatch] = useState(null); const [loading, setLoading] = useState(false); const [currentStage, setCurrentStage] = useState('mixing'); const [stageNotes, setStageNotes] = useState>({}); const [isStageModalOpen, setIsStageModalOpen] = useState(false); const [selectedStageForUpdate, setSelectedStageForUpdate] = useState(null); const [alerts, setAlerts] = useState>([]); const loadBatches = useCallback(async () => { setLoading(true); try { const response = await productionService.getProductionBatches({ status: ProductionBatchStatus.IN_PROGRESS, }); if (response.success && response.data) { setBatches(response.data.items || []); if (batchId) { const specificBatch = response.data.items.find(b => b.id === batchId); if (specificBatch) { setSelectedBatch(specificBatch); } } else if (response.data.items.length > 0) { setSelectedBatch(response.data.items[0]); } } } catch (error) { console.error('Error loading batches:', error); } finally { setLoading(false); } }, [batchId]); useEffect(() => { loadBatches(); // Mock alerts for demonstration setAlerts([ { id: '1', batchId: 'batch-1', stage: 'fermentation', type: 'overdue', message: 'El fermentado ha superado el tiempo estimado en 30 minutos', severity: 'medium', timestamp: new Date().toISOString(), }, { id: '2', batchId: 'batch-2', stage: 'baking', type: 'temperature', message: 'Temperatura del horno fuera del rango óptimo (185°C)', severity: 'high', timestamp: new Date().toISOString(), }, ]); }, [loadBatches]); const getCurrentStageInfo = (batch: ProductionBatchResponse): { stage: ProductionStage; progress: number } => { // This would typically come from the batch data // For demo purposes, we'll simulate based on batch status let stageId = 'mixing'; let progress = 0; if (batch.status === ProductionBatchStatus.IN_PROGRESS) { // Simulate current stage based on time elapsed const startTime = new Date(batch.actual_start_date || batch.planned_start_date); const now = new Date(); const elapsedMinutes = (now.getTime() - startTime.getTime()) / (1000 * 60); let cumulativeTime = 0; const stageKeys = Object.keys(PRODUCTION_STAGES).slice(0, -1); // Exclude completed for (const key of stageKeys) { const stage = PRODUCTION_STAGES[key]; cumulativeTime += stage.estimatedMinutes; if (elapsedMinutes <= cumulativeTime) { stageId = key; const stageStartTime = cumulativeTime - stage.estimatedMinutes; progress = Math.min(100, ((elapsedMinutes - stageStartTime) / stage.estimatedMinutes) * 100); break; } } } return { stage: PRODUCTION_STAGES[stageId], progress: Math.max(0, progress), }; }; const getTimeRemaining = (batch: ProductionBatchResponse, stage: ProductionStage): string => { const startTime = new Date(batch.actual_start_date || batch.planned_start_date); const now = new Date(); const elapsedMinutes = (now.getTime() - startTime.getTime()) / (1000 * 60); // Calculate when this stage should complete based on cumulative time let cumulativeTime = 0; const stageKeys = Object.keys(PRODUCTION_STAGES); const currentStageIndex = stageKeys.indexOf(stage.id); for (let i = 0; i <= currentStageIndex; i++) { cumulativeTime += PRODUCTION_STAGES[stageKeys[i]].estimatedMinutes; } const remainingMinutes = Math.max(0, cumulativeTime - elapsedMinutes); const hours = Math.floor(remainingMinutes / 60); const minutes = Math.floor(remainingMinutes % 60); if (hours > 0) { return `${hours}h ${minutes}m restantes`; } else { return `${minutes}m restantes`; } }; const updateStage = async (batchId: string, newStage: string, notes?: string) => { try { // This would update the batch stage in the backend const updatedBatch = batches.find(b => b.id === batchId); if (updatedBatch && onStageUpdate) { onStageUpdate(updatedBatch as unknown as ProductionBatch, PRODUCTION_STAGES[newStage]); } // Update local state if (notes) { setStageNotes(prev => ({ ...prev, [`${batchId}-${newStage}`]: notes, })); } await loadBatches(); } catch (error) { console.error('Error updating stage:', error); } }; const handleQualityCheck = (batch: ProductionBatchResponse, stage: ProductionStage) => { if (onQualityCheckRequired) { onQualityCheckRequired(batch as unknown as ProductionBatch, stage); } }; const renderStageProgress = (batch: ProductionBatchResponse) => { const { stage: currentStage, progress } = getCurrentStageInfo(batch); const stages = Object.values(PRODUCTION_STAGES).slice(0, -1); // Exclude completed return (

Progreso del lote

{batch.status === ProductionBatchStatus.IN_PROGRESS && 'En progreso'} {batch.status === ProductionBatchStatus.PLANNED && 'Planificado'} {batch.status === ProductionBatchStatus.COMPLETED && 'Completado'}
{stages.map((stage, index) => { const isActive = stage.id === currentStage.id; const isCompleted = stages.findIndex(s => s.id === currentStage.id) > index; const isOverdue = false; // Would be calculated based on actual timing return ( { setSelectedStageForUpdate(stage); setIsStageModalOpen(true); }} >
{stage.icon} {stage.spanishName}
{stage.criticalControlPoint && ( PCC )}
{isActive && (
Progreso {Math.round(progress)}%

{getTimeRemaining(batch, stage)}

)} {isCompleted && (
Completado
)} {!isActive && !isCompleted && (

~{stage.estimatedMinutes}min

)} {stage.temperature && isActive && (

🌡️ {stage.temperature.min}-{stage.temperature.max}{stage.temperature.unit}

)} ); })}
); }; const renderBatchDetails = (batch: ProductionBatchResponse) => (

{batch.recipe?.name || 'Producto'}

Lote #{batch.batch_number}

{batch.priority === ProductionPriority.LOW && 'Baja'} {batch.priority === ProductionPriority.NORMAL && 'Normal'} {batch.priority === ProductionPriority.HIGH && 'Alta'} {batch.priority === ProductionPriority.URGENT && 'Urgente'}

Cantidad planificada

{batch.planned_quantity} unidades

{batch.actual_quantity && ( <>

Cantidad real

{batch.actual_quantity} unidades

)}

Inicio planificado

{new Date(batch.planned_start_date).toLocaleString('es-ES')}

{batch.actual_start_date && ( <>

Inicio real

{new Date(batch.actual_start_date).toLocaleString('es-ES')}

)}
{batch.notes && (

{batch.notes}

)}
); const renderAlerts = () => (

🚨 Alertas activas

{alerts.length === 0 ? (

No hay alertas activas

) : (
{alerts.map((alert) => (

Lote #{batches.find(b => b.id === alert.batchId)?.batch_number} - {PRODUCTION_STAGES[alert.stage]?.spanishName}

{alert.message}

{alert.severity === 'high' && 'Alta'} {alert.severity === 'medium' && 'Media'} {alert.severity === 'low' && 'Baja'}

{new Date(alert.timestamp).toLocaleTimeString('es-ES')}

))}
)}
); return (

Seguimiento de Lotes

Rastrea el progreso de los lotes a través de las etapas de producción

{selectedBatch && (
)}
{loading ? (
) : ( <> {selectedBatch ? (
{renderBatchDetails(selectedBatch)} {renderStageProgress(selectedBatch)} {renderAlerts()}
) : (

No hay lotes en producción actualmente

)} )} {/* Stage Update Modal */} { setIsStageModalOpen(false); setSelectedStageForUpdate(null); }} title={`${selectedStageForUpdate?.spanishName} - Lote #${selectedBatch?.batch_number}`} > {selectedStageForUpdate && selectedBatch && (

{selectedStageForUpdate.icon} {selectedStageForUpdate.spanishName}

Duración estimada: {selectedStageForUpdate.estimatedMinutes} minutos

{selectedStageForUpdate.temperature && (

🌡️ Temperatura: {selectedStageForUpdate.temperature.min}-{selectedStageForUpdate.temperature.max}{selectedStageForUpdate.temperature.unit}

)} {selectedStageForUpdate.criticalControlPoint && ( Punto Crítico de Control (PCC) )}
{selectedStageForUpdate.requiresQualityCheck && (

⚠️ Esta etapa requiere control de calidad antes de continuar

)}
setStageNotes(prev => ({ ...prev, [`${selectedBatch.id}-${selectedStageForUpdate.id}`]: e.target.value, }))} />
)}
{/* Quick Stats */}

Lotes activos

{batches.length}

📊

Alertas activas

{alerts.length}

🚨

En horneado

3

🔥

Completados hoy

12

); }; export default BatchTracker;