680 lines
23 KiB
Plaintext
680 lines
23 KiB
Plaintext
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<string, ProductionStage> = {
|
|
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<BatchTrackerProps> = ({
|
|
className = '',
|
|
batchId,
|
|
onStageUpdate,
|
|
onQualityCheckRequired,
|
|
}) => {
|
|
const [batches, setBatches] = useState<ProductionBatchResponse[]>([]);
|
|
const [selectedBatch, setSelectedBatch] = useState<ProductionBatchResponse | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [currentStage, setCurrentStage] = useState<string>('mixing');
|
|
const [stageNotes, setStageNotes] = useState<Record<string, string>>({});
|
|
const [isStageModalOpen, setIsStageModalOpen] = useState(false);
|
|
const [selectedStageForUpdate, setSelectedStageForUpdate] = useState<ProductionStage | null>(null);
|
|
const [alerts, setAlerts] = useState<Array<{
|
|
id: string;
|
|
batchId: string;
|
|
stage: string;
|
|
type: 'overdue' | 'temperature' | 'quality' | 'equipment';
|
|
message: string;
|
|
severity: 'low' | 'medium' | 'high';
|
|
timestamp: string;
|
|
}>>([]);
|
|
|
|
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 (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-semibold text-lg">Progreso del lote</h3>
|
|
<Badge className={STATUS_COLORS[batch.status]}>
|
|
{batch.status === ProductionBatchStatus.IN_PROGRESS && 'En progreso'}
|
|
{batch.status === ProductionBatchStatus.PLANNED && 'Planificado'}
|
|
{batch.status === ProductionBatchStatus.COMPLETED && 'Completado'}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{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 (
|
|
<Card
|
|
key={stage.id}
|
|
className={`p-4 border-2 transition-all cursor-pointer hover:shadow-md ${
|
|
isActive
|
|
? 'border-blue-500 bg-blue-50'
|
|
: isCompleted
|
|
? 'border-green-500 bg-green-50'
|
|
: 'border-gray-200 bg-white'
|
|
} ${isOverdue ? 'border-red-500 bg-red-50' : ''}`}
|
|
onClick={() => {
|
|
setSelectedStageForUpdate(stage);
|
|
setIsStageModalOpen(true);
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl">{stage.icon}</span>
|
|
<span className="font-medium text-sm">{stage.spanishName}</span>
|
|
</div>
|
|
{stage.criticalControlPoint && (
|
|
<Badge className="bg-orange-100 text-orange-800 text-xs">
|
|
PCC
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{isActive && (
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-xs text-gray-600">
|
|
<span>Progreso</span>
|
|
<span>{Math.round(progress)}%</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-gray-600">
|
|
{getTimeRemaining(batch, stage)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{isCompleted && (
|
|
<div className="flex items-center text-green-600 text-sm">
|
|
<span className="mr-1">✓</span>
|
|
Completado
|
|
</div>
|
|
)}
|
|
|
|
{!isActive && !isCompleted && (
|
|
<p className="text-xs text-gray-500">
|
|
~{stage.estimatedMinutes}min
|
|
</p>
|
|
)}
|
|
|
|
{stage.temperature && isActive && (
|
|
<p className="text-xs text-gray-600 mt-1">
|
|
🌡️ {stage.temperature.min}-{stage.temperature.max}{stage.temperature.unit}
|
|
</p>
|
|
)}
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderBatchDetails = (batch: ProductionBatchResponse) => (
|
|
<Card className="p-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<h4 className="font-semibold text-gray-900">{batch.recipe?.name || 'Producto'}</h4>
|
|
<p className="text-sm text-gray-600">Lote #{batch.batch_number}</p>
|
|
<Badge className={PRIORITY_COLORS[batch.priority]} size="sm">
|
|
{batch.priority === ProductionPriority.LOW && 'Baja'}
|
|
{batch.priority === ProductionPriority.NORMAL && 'Normal'}
|
|
{batch.priority === ProductionPriority.HIGH && 'Alta'}
|
|
{batch.priority === ProductionPriority.URGENT && 'Urgente'}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-sm text-gray-600">Cantidad planificada</p>
|
|
<p className="font-semibold">{batch.planned_quantity} unidades</p>
|
|
{batch.actual_quantity && (
|
|
<>
|
|
<p className="text-sm text-gray-600 mt-1">Cantidad real</p>
|
|
<p className="font-semibold">{batch.actual_quantity} unidades</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-sm text-gray-600">Inicio planificado</p>
|
|
<p className="font-semibold">
|
|
{new Date(batch.planned_start_date).toLocaleString('es-ES')}
|
|
</p>
|
|
{batch.actual_start_date && (
|
|
<>
|
|
<p className="text-sm text-gray-600 mt-1">Inicio real</p>
|
|
<p className="font-semibold">
|
|
{new Date(batch.actual_start_date).toLocaleString('es-ES')}
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{batch.notes && (
|
|
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
|
<p className="text-sm text-gray-700">{batch.notes}</p>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
|
|
const renderAlerts = () => (
|
|
<Card className="p-4">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
🚨 Alertas activas
|
|
</h3>
|
|
|
|
{alerts.length === 0 ? (
|
|
<p className="text-gray-500 text-center py-4">No hay alertas activas</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{alerts.map((alert) => (
|
|
<div
|
|
key={alert.id}
|
|
className={`p-3 rounded-lg border-l-4 ${
|
|
alert.severity === 'high'
|
|
? 'bg-red-50 border-red-500'
|
|
: alert.severity === 'medium'
|
|
? 'bg-yellow-50 border-yellow-500'
|
|
: 'bg-blue-50 border-blue-500'
|
|
}`}
|
|
>
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<p className="font-medium text-sm">
|
|
Lote #{batches.find(b => b.id === alert.batchId)?.batch_number} - {PRODUCTION_STAGES[alert.stage]?.spanishName}
|
|
</p>
|
|
<p className="text-sm text-gray-700 mt-1">{alert.message}</p>
|
|
</div>
|
|
<Badge
|
|
className={
|
|
alert.severity === 'high'
|
|
? 'bg-red-100 text-red-800'
|
|
: alert.severity === 'medium'
|
|
? 'bg-yellow-100 text-yellow-800'
|
|
: 'bg-blue-100 text-blue-800'
|
|
}
|
|
>
|
|
{alert.severity === 'high' && 'Alta'}
|
|
{alert.severity === 'medium' && 'Media'}
|
|
{alert.severity === 'low' && 'Baja'}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
{new Date(alert.timestamp).toLocaleTimeString('es-ES')}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
|
|
return (
|
|
<div className={`space-y-6 ${className}`}>
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900">Seguimiento de Lotes</h2>
|
|
<p className="text-gray-600">Rastrea el progreso de los lotes a través de las etapas de producción</p>
|
|
</div>
|
|
|
|
{selectedBatch && (
|
|
<div className="flex gap-2">
|
|
<Select
|
|
value={selectedBatch.id}
|
|
onChange={(e) => {
|
|
const batch = batches.find(b => b.id === e.target.value);
|
|
if (batch) setSelectedBatch(batch);
|
|
}}
|
|
className="w-64"
|
|
>
|
|
{batches.map((batch) => (
|
|
<option key={batch.id} value={batch.id}>
|
|
Lote #{batch.batch_number} - {batch.recipe?.name}
|
|
</option>
|
|
))}
|
|
</Select>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={loadBatches}
|
|
disabled={loading}
|
|
>
|
|
{loading ? 'Actualizando...' : 'Actualizar'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{selectedBatch ? (
|
|
<div className="space-y-6">
|
|
{renderBatchDetails(selectedBatch)}
|
|
{renderStageProgress(selectedBatch)}
|
|
{renderAlerts()}
|
|
</div>
|
|
) : (
|
|
<Card className="p-8 text-center">
|
|
<p className="text-gray-500">No hay lotes en producción actualmente</p>
|
|
<Button variant="primary" className="mt-4">
|
|
Ver todos los lotes
|
|
</Button>
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Stage Update Modal */}
|
|
<Modal
|
|
isOpen={isStageModalOpen}
|
|
onClose={() => {
|
|
setIsStageModalOpen(false);
|
|
setSelectedStageForUpdate(null);
|
|
}}
|
|
title={`${selectedStageForUpdate?.spanishName} - Lote #${selectedBatch?.batch_number}`}
|
|
>
|
|
{selectedStageForUpdate && selectedBatch && (
|
|
<div className="space-y-4">
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h4 className="font-medium mb-2 flex items-center gap-2">
|
|
<span className="text-xl">{selectedStageForUpdate.icon}</span>
|
|
{selectedStageForUpdate.spanishName}
|
|
</h4>
|
|
<p className="text-sm text-gray-600 mb-2">
|
|
Duración estimada: {selectedStageForUpdate.estimatedMinutes} minutos
|
|
</p>
|
|
|
|
{selectedStageForUpdate.temperature && (
|
|
<p className="text-sm text-gray-600">
|
|
🌡️ Temperatura: {selectedStageForUpdate.temperature.min}-{selectedStageForUpdate.temperature.max}{selectedStageForUpdate.temperature.unit}
|
|
</p>
|
|
)}
|
|
|
|
{selectedStageForUpdate.criticalControlPoint && (
|
|
<Badge className="bg-orange-100 text-orange-800 mt-2">
|
|
Punto Crítico de Control (PCC)
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{selectedStageForUpdate.requiresQualityCheck && (
|
|
<div className="bg-yellow-50 border border-yellow-200 p-3 rounded-lg">
|
|
<p className="text-sm text-yellow-800">
|
|
⚠️ Esta etapa requiere control de calidad antes de continuar
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="mt-2"
|
|
onClick={() => handleQualityCheck(selectedBatch, selectedStageForUpdate)}
|
|
>
|
|
Realizar control de calidad
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Notas de la etapa
|
|
</label>
|
|
<Input
|
|
as="textarea"
|
|
rows={3}
|
|
placeholder="Añadir observaciones o notas sobre esta etapa..."
|
|
value={stageNotes[`${selectedBatch.id}-${selectedStageForUpdate.id}`] || ''}
|
|
onChange={(e) => setStageNotes(prev => ({
|
|
...prev,
|
|
[`${selectedBatch.id}-${selectedStageForUpdate.id}`]: e.target.value,
|
|
}))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsStageModalOpen(false)}
|
|
>
|
|
Cerrar
|
|
</Button>
|
|
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
const notes = stageNotes[`${selectedBatch.id}-${selectedStageForUpdate.id}`];
|
|
updateStage(selectedBatch.id, selectedStageForUpdate.id, notes);
|
|
setIsStageModalOpen(false);
|
|
}}
|
|
>
|
|
Marcar como completado
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<Card className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Lotes activos</p>
|
|
<p className="text-2xl font-bold text-blue-600">{batches.length}</p>
|
|
</div>
|
|
<span className="text-2xl">📊</span>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Alertas activas</p>
|
|
<p className="text-2xl font-bold text-red-600">{alerts.length}</p>
|
|
</div>
|
|
<span className="text-2xl">🚨</span>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">En horneado</p>
|
|
<p className="text-2xl font-bold text-orange-600">3</p>
|
|
</div>
|
|
<span className="text-2xl">🔥</span>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Completados hoy</p>
|
|
<p className="text-2xl font-bold text-green-600">12</p>
|
|
</div>
|
|
<span className="text-2xl">✅</span>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BatchTracker; |