882 lines
34 KiB
TypeScript
882 lines
34 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package, PlusCircle, Play, Info } 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<ProductionBatchResponse | null>(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 (
|
|
<div className="flex items-center justify-center min-h-64">
|
|
<LoadingSpinner text="Cargando producción..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Error state
|
|
if (dashboardError || batchesError) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<AlertCircle className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
|
Error al cargar la producción
|
|
</h3>
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
|
{(dashboardError || batchesError)?.message || 'Ha ocurrido un error inesperado'}
|
|
</p>
|
|
<Button onClick={() => window.location.reload()}>
|
|
Reintentar
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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: 'create-batch',
|
|
label: 'Nueva Orden de Producción',
|
|
icon: PlusCircle,
|
|
onClick: () => setIsWizardOpen(true),
|
|
variant: 'primary',
|
|
size: 'md'
|
|
}
|
|
]}
|
|
/>
|
|
|
|
{/* Production Stats */}
|
|
<StatsGrid
|
|
stats={[
|
|
{
|
|
title: 'Lotes Activos',
|
|
value: productionStats.activeBatches,
|
|
variant: 'default' as const,
|
|
icon: Package,
|
|
},
|
|
{
|
|
title: 'Utilización Capacidad',
|
|
value: `${productionStats.capacityUtilization}%`,
|
|
variant: productionStats.capacityUtilization >= 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 */}
|
|
<SearchAndFilter
|
|
searchValue={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
searchPlaceholder="Buscar lotes por producto, número de lote o personal..."
|
|
filters={[
|
|
{
|
|
key: 'status',
|
|
label: 'Estado',
|
|
type: 'dropdown',
|
|
value: statusFilter,
|
|
onChange: (value) => 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 */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{filteredBatches.map((batch) => (
|
|
<ProductionStatusCard
|
|
key={batch.id}
|
|
batch={batch}
|
|
onView={(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}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Empty State */}
|
|
{filteredBatches.length === 0 && (
|
|
<EmptyState
|
|
icon={ChefHat}
|
|
title="No se encontraron lotes de producción"
|
|
description={
|
|
batches.length === 0
|
|
? 'No hay lotes de producción activos. Crea el primer lote para comenzar.'
|
|
: 'Intenta ajustar la búsqueda o crear un nuevo lote de producción'
|
|
}
|
|
actionLabel="Nueva Orden de Producción"
|
|
actionIcon={Plus}
|
|
onAction={() => setIsWizardOpen(true)}
|
|
/>
|
|
)}
|
|
</>
|
|
|
|
|
|
|
|
{/* Production Batch Detail Modal */}
|
|
{showBatchModal && selectedBatch && (
|
|
<EditViewModal
|
|
isOpen={showBatchModal}
|
|
onClose={() => {
|
|
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: (
|
|
<ProcessStageTracker
|
|
processStage={{
|
|
current: selectedBatch.current_process_stage as QualityProcessStage || 'mixing',
|
|
history: selectedBatch.process_stage_history ? selectedBatch.process_stage_history.map((item: any) => ({
|
|
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: 'Detalles del Razonamiento',
|
|
icon: Info,
|
|
fields: [
|
|
{
|
|
label: 'Causa Principal',
|
|
value: selectedBatch.reasoning_data?.trigger_type
|
|
? t(`reasoning:triggers.${selectedBatch.reasoning_data.trigger_type.toLowerCase()}`)
|
|
: 'No especificado',
|
|
span: 2
|
|
},
|
|
{
|
|
label: 'Descripción del Razonamiento',
|
|
value: selectedBatch.reasoning_data?.trigger_description || 'No especificado',
|
|
type: 'textarea',
|
|
span: 2
|
|
},
|
|
{
|
|
label: 'Factores Clave',
|
|
value: selectedBatch.reasoning_data?.factors && Array.isArray(selectedBatch.reasoning_data.factors)
|
|
? selectedBatch.reasoning_data.factors.map(factor =>
|
|
t(`reasoning:factors.${factor.toLowerCase()}`) || factor
|
|
).join(', ')
|
|
: 'No especificados',
|
|
span: 2
|
|
},
|
|
{
|
|
label: 'Consecuencias Potenciales',
|
|
value: selectedBatch.reasoning_data?.consequence || 'No especificado',
|
|
type: 'textarea',
|
|
span: 2
|
|
},
|
|
{
|
|
label: 'Nivel de Confianza',
|
|
value: selectedBatch.reasoning_data?.confidence_score
|
|
? `${selectedBatch.reasoning_data.confidence_score}%`
|
|
: 'No especificado'
|
|
},
|
|
{
|
|
label: 'Variación Histórica',
|
|
value: selectedBatch.reasoning_data?.variance
|
|
? `${selectedBatch.reasoning_data.variance}%`
|
|
: 'No especificado'
|
|
},
|
|
{
|
|
label: 'Detalles de la Predicción',
|
|
value: selectedBatch.reasoning_data?.prediction_details || 'No especificado',
|
|
type: 'textarea',
|
|
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<string, string> = {
|
|
// General Information
|
|
'Cantidad Producida': 'actual_quantity',
|
|
'Estado': 'status',
|
|
'Prioridad': 'priority',
|
|
'Personal Asignado': 'staff_assigned',
|
|
// Reasoning section editable fields
|
|
'Descripción del Razonamiento': 'reasoning_data.trigger_description',
|
|
'Consecuencias Potenciales': 'reasoning_data.consequence',
|
|
'Detalles de la Predicción': 'reasoning_data.prediction_details',
|
|
// 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
|
|
['Causa Principal', 'Descripción del Razonamiento', 'Factores Clave', 'Consecuencias Potenciales', 'Nivel de Confianza', 'Variación Histórica', 'Detalles de la Predicción'], // Reasoning section
|
|
['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;
|
|
}
|
|
|
|
// Handle nested reasoning_data fields
|
|
if (propertyName.startsWith('reasoning_data.')) {
|
|
const nestedProperty = propertyName.split('.')[1];
|
|
setSelectedBatch({
|
|
...selectedBatch,
|
|
reasoning_data: {
|
|
...(selectedBatch.reasoning_data || {}),
|
|
[nestedProperty]: processedValue
|
|
}
|
|
});
|
|
} else {
|
|
setSelectedBatch({
|
|
...selectedBatch,
|
|
[propertyName]: processedValue
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Unified Add Wizard for Production Batches */}
|
|
<UnifiedAddWizard
|
|
isOpen={isWizardOpen}
|
|
onClose={() => setIsWizardOpen(false)}
|
|
onComplete={(itemType: ItemType, data?: any) => {
|
|
console.log('Production batch created:', data);
|
|
refetchBatches();
|
|
}}
|
|
initialItemType="production-batch"
|
|
/>
|
|
|
|
{/* Quality Check Modal */}
|
|
{showQualityModal && selectedBatch && (
|
|
<QualityCheckModal
|
|
isOpen={showQualityModal}
|
|
onClose={() => {
|
|
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);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProductionPage;
|