import React, { useState, useMemo } from 'react'; import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package, PlusCircle, Play } from 'lucide-react'; import { Button, StatsGrid, EditViewModal, Toggle, SearchAndFilter, type FilterConfig, EmptyState } from '../../../../components/ui'; import { statusColors } from '../../../../styles/colors'; import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { LoadingSpinner } from '../../../../components/ui'; import { PageHeader } from '../../../../components/layout'; import { ProductionSchedule, ProductionStatusCard, QualityCheckModal, ProcessStageTracker } from '../../../../components/domain/production'; import { UnifiedAddWizard } from '../../../../components/domain/unified-wizard'; import type { ItemType } from '../../../../components/domain/unified-wizard'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useProductionDashboard, useActiveBatches, useCreateProductionBatch, useUpdateBatchStatus, useTriggerProductionScheduler, productionService } from '../../../../api'; import type { ProductionBatchResponse, ProductionBatchCreate, ProductionBatchStatusUpdate } from '../../../../api'; import { ProductionStatusEnum, ProductionPriorityEnum } from '../../../../api'; import { useTranslation } from 'react-i18next'; import { ProcessStage as QualityProcessStage } from '../../../../api/types/qualityTemplates'; import { showToast } from '../../../../utils/toast'; const ProductionPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [priorityFilter, setPriorityFilter] = useState(''); const [selectedBatch, setSelectedBatch] = useState(null); const [showBatchModal, setShowBatchModal] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false); const [showQualityModal, setShowQualityModal] = useState(false); const [modalMode, setModalMode] = useState<'view' | 'edit'>('view'); const currentTenant = useCurrentTenant(); const tenantId = currentTenant?.id || ''; const { t } = useTranslation(['production', 'common']); // API Data const { data: dashboardData, isLoading: dashboardLoading, error: dashboardError } = useProductionDashboard(tenantId); const { data: activeBatchesData, isLoading: batchesLoading, error: batchesError } = useActiveBatches(tenantId); // Mutations const createBatchMutation = useCreateProductionBatch(); const updateBatchStatusMutation = useUpdateBatchStatus(); // Handlers const handleCreateBatch = async (batchData: ProductionBatchCreate) => { try { await createBatchMutation.mutateAsync({ tenantId, batchData }); } catch (error) { console.error('Error creating production batch:', error); throw error; } }; const handleTriggerScheduler = async () => { try { await triggerSchedulerMutation.mutateAsync(tenantId); showToast.success('Scheduler ejecutado exitosamente'); } catch (error) { console.error('Error triggering scheduler:', error); showToast.error('Error al ejecutar scheduler'); } }; // Stage management handlers const handleStageAdvance = async (batchId: string, currentStage: QualityProcessStage) => { const stages = Object.values(QualityProcessStage); 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: QualityProcessStage) => { 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: QualityProcessStage) => { setSelectedBatch(batch); setShowQualityModal(true); // The QualityCheckModal should be enhanced to handle stage-specific checks }; // Helper function to get process stage data from the batch (now from real backend data) const getProcessStageData = (batch: ProductionBatchResponse) => { // Backend now provides these fields in the API response: // - current_process_stage // - process_stage_history // - pending_quality_checks // - completed_quality_checks return { current: batch.current_process_stage as QualityProcessStage || 'mixing', history: batch.process_stage_history ? batch.process_stage_history.map(item => ({ stage: item.stage as QualityProcessStage, start_time: item.start_time || item.timestamp || '', end_time: item.end_time, duration: item.duration, notes: item.notes, personnel: item.personnel })) : [], pendingQualityChecks: batch.pending_quality_checks ? batch.pending_quality_checks.map(item => ({ id: item.id || '', name: item.name || '', stage: item.stage as QualityProcessStage, isRequired: item.is_required || item.isRequired || false, isCritical: item.is_critical || item.isCritical || false, status: item.status || 'pending', checkType: item.check_type || item.checkType || 'visual' })) : [], completedQualityChecks: batch.completed_quality_checks ? batch.completed_quality_checks.map(item => ({ id: item.id || '', name: item.name || '', stage: item.stage as QualityProcessStage, isRequired: item.is_required || item.isRequired || false, isCritical: item.is_critical || item.isCritical || false, status: item.status || 'completed', checkType: item.check_type || item.checkType || 'visual' })) : [] }; }; // Helper function to calculate total progress percentage const calculateTotalProgressPercentage = (batch: ProductionBatchResponse): number => { const allStages: QualityProcessStage[] = ['mixing', 'proofing', 'shaping', 'baking', 'cooling', 'packaging', 'finishing']; const currentStageIndex = allStages.indexOf(batch.current_process_stage || 'mixing'); // Base percentage based on completed stages const completedStages = batch.process_stage_history?.length || 0; const totalStages = allStages.length; const basePercentage = (completedStages / totalStages) * 100; // If in the last stage, it should be 100% only if completed if (currentStageIndex === totalStages - 1) { return batch.status === 'COMPLETED' ? 100 : Math.min(95, basePercentage + 15); // Almost complete but not quite until marked as completed } // Add partial progress for current stage (estimated as 15% of the remaining percentage) const remainingPercentage = 100 - basePercentage; const currentStageProgress = remainingPercentage * 0.15; // Current stage is 15% of remaining return Math.min(100, Math.round(basePercentage + currentStageProgress)); }; // Helper function to calculate estimated time remaining const calculateEstimatedTimeRemaining = (batch: ProductionBatchResponse): number | undefined => { // This would typically come from backend or be calculated based on historical data // For now, returning a mock value or undefined if (batch.status === 'COMPLETED') return 0; // Mock calculation based on typical stage times const allStages: QualityProcessStage[] = ['mixing', 'proofing', 'shaping', 'baking', 'cooling', 'packaging', 'finishing']; const currentStageIndex = allStages.indexOf(batch.current_process_stage || 'mixing'); if (currentStageIndex === -1) return undefined; // Return a mock value in minutes const stagesRemaining = allStages.length - currentStageIndex - 1; return stagesRemaining * 15; // Assuming ~15 mins per stage as an estimate }; // Helper function to calculate current stage duration const calculateCurrentStageDuration = (batch: ProductionBatchResponse): number | undefined => { const currentStage = batch.current_process_stage; if (!currentStage || !batch.process_stage_history) return undefined; const currentStageHistory = batch.process_stage_history.find(h => h.stage === currentStage); if (!currentStageHistory || !currentStageHistory.start_time) return undefined; const startTime = new Date(currentStageHistory.start_time); const now = new Date(); const diffInMinutes = Math.ceil((now.getTime() - startTime.getTime()) / (1000 * 60)); return diffInMinutes; }; const batches = activeBatchesData?.batches || []; const filteredBatches = useMemo(() => { let filtered = batches; // Apply search filter if (searchQuery) { const searchLower = searchQuery.toLowerCase(); filtered = filtered.filter(batch => batch.product_name.toLowerCase().includes(searchLower) || batch.batch_number.toLowerCase().includes(searchLower) || (batch.staff_assigned && batch.staff_assigned.some(staff => staff.toLowerCase().includes(searchLower) )) ); } // Apply status filter if (statusFilter) { filtered = filtered.filter(batch => batch.status === statusFilter); } // Apply priority filter if (priorityFilter) { filtered = filtered.filter(batch => batch.priority === priorityFilter); } return filtered; }, [batches, searchQuery, statusFilter, priorityFilter]); // Calculate production stats from real data const productionStats = useMemo(() => { if (!dashboardData) { return { activeBatches: 0, todaysTarget: 0, capacityUtilization: 0, onTimeCompletion: 0, qualityScore: 0, totalOutput: 0, efficiency: 0 }; } return { activeBatches: dashboardData.active_batches || 0, todaysTarget: dashboardData.todays_production_plan?.length || 0, capacityUtilization: Math.round(dashboardData.capacity_utilization || 0), onTimeCompletion: Math.round(dashboardData.on_time_completion_rate || 0), qualityScore: Math.round(dashboardData.average_quality_score || 0), totalOutput: dashboardData.total_output_today || 0, efficiency: Math.round(dashboardData.efficiency_percentage || 0) }; }, [dashboardData]); // Loading state if (!tenantId || dashboardLoading || batchesLoading) { return (
); } // Error state if (dashboardError || batchesError) { return (

Error al cargar la producción

{(dashboardError || batchesError)?.message || 'Ha ocurrido un error inesperado'}

); } return (
setIsWizardOpen(true), variant: 'primary', size: 'md' } ]} /> {/* Production Stats */} = 80 ? 'success' as const : 'warning' as const, icon: Timer, }, { title: 'Completado a Tiempo', value: `${productionStats.onTimeCompletion}%`, variant: productionStats.onTimeCompletion >= 90 ? 'success' as const : 'error' as const, icon: CheckCircle, }, { title: 'Puntuación Calidad', value: `${productionStats.qualityScore}%`, variant: productionStats.qualityScore >= 85 ? 'success' as const : 'warning' as const, icon: Package, }, { title: 'Producción Hoy', value: formatters.number(productionStats.totalOutput), variant: 'info' as const, icon: ChefHat, }, { title: 'Eficiencia', value: `${productionStats.efficiency}%`, variant: productionStats.efficiency >= 75 ? 'success' as const : 'warning' as const, icon: Timer, }, ]} columns={3} /> {/* Production Batches Section - No tabs needed */} <> {/* Search and Filter Controls */} setStatusFilter(value as string), placeholder: 'Todos los estados', options: Object.values(ProductionStatusEnum).map(status => ({ value: status, label: status.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) })) }, { key: 'priority', label: 'Prioridad', type: 'dropdown', value: priorityFilter, onChange: (value) => setPriorityFilter(value as string), placeholder: 'Todas las prioridades', options: Object.values(ProductionPriorityEnum).map(priority => ({ value: priority, label: priority.charAt(0).toUpperCase() + priority.slice(1).toLowerCase() })) } ] as FilterConfig[]} /> {/* Production Batches Grid */}
{filteredBatches.map((batch) => ( { setSelectedBatch(batch); setModalMode('view'); setShowBatchModal(true); }} onStart={async (batch) => { try { await updateBatchStatusMutation.mutateAsync({ batchId: batch.id, updates: { status: ProductionStatusEnum.IN_PROGRESS } }); } catch (error) { console.error('Error starting batch:', error); } }} onPause={async (batch) => { try { await updateBatchStatusMutation.mutateAsync({ batchId: batch.id, updates: { status: ProductionStatusEnum.ON_HOLD } }); } catch (error) { console.error('Error pausing batch:', error); } }} onComplete={async (batch) => { try { await updateBatchStatusMutation.mutateAsync({ batchId: batch.id, updates: { status: ProductionStatusEnum.QUALITY_CHECK } }); } catch (error) { console.error('Error completing batch:', error); } }} onCancel={async (batch) => { try { await updateBatchStatusMutation.mutateAsync({ batchId: batch.id, updates: { status: ProductionStatusEnum.CANCELLED } }); } catch (error) { console.error('Error cancelling batch:', error); } }} onQualityCheck={(batch) => { setSelectedBatch(batch); setShowQualityModal(true); }} showDetailedProgress={true} /> ))}
{/* Empty State */} {filteredBatches.length === 0 && ( setIsWizardOpen(true)} /> )} {/* Production Batch Detail Modal */} {showBatchModal && selectedBatch && ( { setShowBatchModal(false); setSelectedBatch(null); setModalMode('view'); }} mode={modalMode} onModeChange={setModalMode} title={selectedBatch.product_name} subtitle={`Lote de Producción #${selectedBatch.batch_number}`} statusIndicator={{ color: statusColors.inProgress.primary, text: t(`production:status.${selectedBatch.status.toLowerCase()}`), icon: Package }} size="xl" sections={[ { title: 'Información General', icon: Package, fields: [ { label: 'Producto', value: selectedBatch.product_name, highlight: true }, { label: 'Número de Lote', value: selectedBatch.batch_number }, { label: 'Cantidad Planificada', value: `${selectedBatch.planned_quantity} unidades`, highlight: true }, { label: 'Cantidad Producida', value: selectedBatch.actual_quantity ? `${selectedBatch.actual_quantity} unidades` : 'Pendiente', editable: modalMode === 'edit', type: 'number' }, { label: 'Estado', value: selectedBatch.status, type: 'select', editable: modalMode === 'edit', options: Object.values(ProductionStatusEnum).map(value => ({ value, label: t(`production:status.${value.toLowerCase()}`) })) }, { label: 'Prioridad', value: selectedBatch.priority, type: 'select', editable: modalMode === 'edit', options: Object.values(ProductionPriorityEnum).map(value => ({ value, label: t(`production:priority.${value.toLowerCase()}`) })) }, { label: 'Personal Asignado', value: selectedBatch.staff_assigned?.join(', ') || 'No asignado', editable: modalMode === 'edit', type: 'text' }, { label: 'Equipos Utilizados', value: selectedBatch.equipment_used?.join(', ') || 'No especificado' } ] }, { title: 'Cronograma', icon: Clock, fields: [ { label: 'Inicio Planificado', value: selectedBatch.planned_start_time, type: 'datetime' }, { label: 'Fin Planificado', value: selectedBatch.planned_end_time, type: 'datetime' }, { label: 'Duración Planificada', value: selectedBatch.planned_duration_minutes ? `${selectedBatch.planned_duration_minutes} minutos` : 'No especificada' }, { label: 'Inicio Real', value: selectedBatch.actual_start_time || 'Pendiente', type: 'datetime' }, { label: 'Fin Real', value: selectedBatch.actual_end_time || 'Pendiente', type: 'datetime' }, { label: 'Duración Real', value: selectedBatch.actual_duration_minutes ? `${selectedBatch.actual_duration_minutes} minutos` : 'Pendiente' } ] }, { title: 'Fases del Proceso de Producción', icon: Timer, fields: [ { label: '', value: ( ({ stage: item.stage as QualityProcessStage, start_time: item.start_time || item.timestamp, end_time: item.end_time, duration: item.duration, notes: item.notes, personnel: item.personnel })) : [], pendingQualityChecks: selectedBatch.pending_quality_checks ? selectedBatch.pending_quality_checks.map((item: any) => ({ id: item.id || '', name: item.name || '', stage: item.stage as QualityProcessStage || 'mixing', isRequired: item.isRequired || item.is_required || false, isCritical: item.isCritical || item.is_critical || false, status: item.status || 'pending', checkType: item.checkType || item.check_type || 'visual' })) : [], completedQualityChecks: selectedBatch.completed_quality_checks ? selectedBatch.completed_quality_checks.map((item: any) => ({ id: item.id || '', name: item.name || '', stage: item.stage as QualityProcessStage || 'mixing', isRequired: item.isRequired || item.is_required || false, isCritical: item.isCritical || item.is_critical || false, status: item.status || 'completed', checkType: item.checkType || item.check_type || 'visual' })) : [], totalProgressPercentage: calculateTotalProgressPercentage(selectedBatch), estimatedTimeRemaining: calculateEstimatedTimeRemaining(selectedBatch), currentStageDuration: calculateCurrentStageDuration(selectedBatch) }} onAdvanceStage={(currentStage) => handleStageAdvance(selectedBatch.id, currentStage)} onQualityCheck={(checkId) => { setShowQualityModal(true); console.log('Opening quality check:', checkId); }} onViewStageDetails={(stage) => { console.log('View stage details:', stage); // This would open a detailed view for the stage }} onStageAction={(stage, action) => { console.log('Stage action:', stage, action); // This would handle stage-specific actions }} className="w-full" /> ), span: 2 } ] }, { title: 'Calidad y Costos', icon: CheckCircle, fields: [ { label: 'Puntuación de Calidad', value: selectedBatch.quality_score ? `${selectedBatch.quality_score}/10` : 'Pendiente', highlight: selectedBatch.quality_score ? selectedBatch.quality_score >= 8 : false }, { label: 'Rendimiento', value: selectedBatch.yield_percentage ? `${selectedBatch.yield_percentage}%` : 'Calculando...' }, { label: 'Costo Estimado', value: selectedBatch.estimated_cost || 0, type: 'currency' }, { label: 'Costo Real', value: selectedBatch.actual_cost || 0, type: 'currency' }, { label: 'Notas de Producción', value: selectedBatch.production_notes || 'Sin notas', type: 'textarea', editable: modalMode === 'edit', span: 2 }, { label: 'Notas de Calidad', value: selectedBatch.quality_notes || 'Sin notas de calidad', type: 'textarea', span: 2 } ] } ]} onSave={async () => { try { // Implementation would depend on specific fields changed console.log('Saving batch changes:', selectedBatch.id); // await updateBatchStatusMutation.mutateAsync({ // batchId: selectedBatch.id, // updates: selectedBatch // }); setShowBatchModal(false); setSelectedBatch(null); setModalMode('view'); } catch (error) { console.error('Error saving batch:', error); } }} onFieldChange={(sectionIndex, fieldIndex, value) => { if (!selectedBatch) return; const fieldMapping: Record = { // General Information 'Cantidad Producida': 'actual_quantity', 'Estado': 'status', 'Prioridad': 'priority', 'Personal Asignado': 'staff_assigned', // Schedule - most fields are read-only datetime // Quality and Costs 'Notas de Producción': 'production_notes', 'Notas de Calidad': 'quality_notes' }; // Get section labels to map back to field names const sectionLabels = [ ['Producto', 'Número de Lote', 'Cantidad Planificada', 'Cantidad Producida', 'Estado', 'Prioridad', 'Personal Asignado', 'Equipos Utilizados'], ['Inicio Planificado', 'Fin Planificado', 'Duración Planificada', 'Inicio Real', 'Fin Real', 'Duración Real'], [], // Process Stage Tracker section - no editable fields ['Puntuación de Calidad', 'Rendimiento', 'Costo Estimado', 'Costo Real', 'Notas de Producción', 'Notas de Calidad'] ]; const fieldLabel = sectionLabels[sectionIndex]?.[fieldIndex]; const propertyName = fieldMapping[fieldLabel]; if (propertyName) { let processedValue: any = value; // Process specific field types if (propertyName === 'staff_assigned' && typeof value === 'string') { processedValue = value.split(',').map(s => s.trim()).filter(s => s.length > 0); } else if (propertyName === 'actual_quantity') { processedValue = parseFloat(value as string) || 0; } setSelectedBatch({ ...selectedBatch, [propertyName]: processedValue }); } }} /> )} {/* Unified Add Wizard for Production Batches */} setIsWizardOpen(false)} onComplete={(itemType: ItemType, data?: any) => { console.log('Production batch created:', data); refetchBatches(); }} initialItemType="production-batch" /> {/* Quality Check Modal */} {showQualityModal && selectedBatch && ( { setShowQualityModal(false); setSelectedBatch(null); }} batch={selectedBatch} onComplete={async (result) => { console.log('Quality check completed:', result); // Optionally update batch status to completed or quality_passed try { await updateBatchStatusMutation.mutateAsync({ batchId: selectedBatch.id, updates: { status: result.overallPass ? ProductionStatusEnum.COMPLETED : ProductionStatusEnum.ON_HOLD } }); } catch (error) { console.error('Error updating batch status after quality check:', error); } }} /> )}
); }; export default ProductionPage;