Files
bakery-ia/frontend/src/components/domain/dashboard/TodayProduction.tsx

414 lines
14 KiB
TypeScript
Raw Normal View History

2025-10-21 19:50:07 +02:00
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardHeader, CardBody } from '../../ui/Card';
import { StatusCard } from '../../ui/StatusCard/StatusCard';
import { Badge } from '../../ui/Badge';
import { Button } from '../../ui/Button';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useActiveBatches } from '../../../api/hooks/production';
import {
Factory,
Clock,
Play,
Pause,
CheckCircle,
AlertTriangle,
ChevronRight,
Timer,
ChefHat,
Flame,
Calendar
} from 'lucide-react';
export interface TodayProductionProps {
className?: string;
maxBatches?: number;
onStartBatch?: (batchId: string) => void;
onPauseBatch?: (batchId: string) => void;
onViewDetails?: (batchId: string) => void;
onViewAllPlans?: () => void;
}
const TodayProduction: React.FC<TodayProductionProps> = ({
className,
maxBatches = 5,
onStartBatch,
onPauseBatch,
onViewDetails,
onViewAllPlans
}) => {
const { t } = useTranslation(['dashboard']);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
// Get today's date
const todayDate = useMemo(() => {
return new Date().toISOString().split('T')[0];
}, []);
// Fetch active production batches
const { data: productionData, isLoading, error } = useActiveBatches(
tenantId,
{
enabled: !!tenantId,
}
);
const getBatchStatusConfig = (batch: any) => {
const baseConfig = {
isCritical: batch.status === 'FAILED' || batch.priority === 'URGENT',
isHighlight: batch.status === 'IN_PROGRESS' || batch.priority === 'HIGH',
};
switch (batch.status) {
case 'PENDING':
return {
...baseConfig,
color: 'var(--color-warning)',
text: 'Pendiente',
icon: Clock
};
case 'IN_PROGRESS':
return {
...baseConfig,
color: 'var(--color-info)',
text: 'En Proceso',
icon: Flame
};
case 'COMPLETED':
return {
...baseConfig,
color: 'var(--color-success)',
text: 'Completado',
icon: CheckCircle
};
case 'ON_HOLD':
return {
...baseConfig,
color: 'var(--color-warning)',
text: 'Pausado',
icon: Pause
};
case 'FAILED':
return {
...baseConfig,
color: 'var(--color-error)',
text: 'Fallido',
icon: AlertTriangle
};
case 'QUALITY_CHECK':
return {
...baseConfig,
color: 'var(--color-info)',
text: 'Control de Calidad',
icon: CheckCircle
};
default:
return {
...baseConfig,
color: 'var(--color-warning)',
text: 'Pendiente',
icon: Clock
};
}
};
const formatDuration = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0) {
return `${hours}h ${mins}m`;
}
return `${mins}m`;
};
// Process batches and sort by priority
const displayBatches = useMemo(() => {
if (!productionData?.batches || !Array.isArray(productionData.batches)) return [];
const batches = [...productionData.batches];
// Filter for today's batches only
const todayBatches = batches.filter(batch => {
const batchDate = new Date(batch.planned_start_time || batch.created_at);
return batchDate.toISOString().split('T')[0] === todayDate;
});
// Sort by priority and start time
const priorityOrder = { URGENT: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
todayBatches.sort((a, b) => {
// First sort by status (pending/in_progress first)
const statusOrder = { PENDING: 0, IN_PROGRESS: 1, QUALITY_CHECK: 2, ON_HOLD: 3, COMPLETED: 4, FAILED: 5, CANCELLED: 6 };
const aStatus = statusOrder[a.status as keyof typeof statusOrder] ?? 7;
const bStatus = statusOrder[b.status as keyof typeof statusOrder] ?? 7;
if (aStatus !== bStatus) return aStatus - bStatus;
// Then by priority
const aPriority = priorityOrder[a.priority as keyof typeof priorityOrder] ?? 4;
const bPriority = priorityOrder[b.priority as keyof typeof priorityOrder] ?? 4;
if (aPriority !== bPriority) return aPriority - bPriority;
// Finally by start time
const aTime = new Date(a.planned_start_time || a.created_at).getTime();
const bTime = new Date(b.planned_start_time || b.created_at).getTime();
return aTime - bTime;
});
return todayBatches.slice(0, maxBatches);
}, [productionData, todayDate, maxBatches]);
const inProgressBatches = productionData?.batches?.filter(
b => b.status === 'IN_PROGRESS'
).length || 0;
const completedBatches = productionData?.batches?.filter(
b => b.status === 'COMPLETED'
).length || 0;
const delayedBatches = productionData?.batches?.filter(
b => b.status === 'FAILED'
).length || 0;
const pendingBatches = productionData?.batches?.filter(
b => b.status === 'PENDING'
).length || 0;
if (isLoading) {
return (
<Card className={className} variant="elevated" padding="none">
<CardHeader padding="lg" divider>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[var(--color-primary)]/20">
<Factory className="w-5 h-5 text-[var(--color-primary)]" />
</div>
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('dashboard:sections.production_today', 'Producción de Hoy')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('dashboard:production.title', '¿Qué necesito producir hoy?')}
</p>
</div>
</div>
</CardHeader>
<CardBody>
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div>
</div>
</CardBody>
</Card>
);
}
if (error) {
return (
<Card className={className} variant="elevated" padding="none">
<CardHeader padding="lg" divider>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[var(--color-primary)]/20">
<Factory className="w-5 h-5 text-[var(--color-primary)]" />
</div>
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('dashboard:sections.production_today', 'Producción de Hoy')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('dashboard:production.title', '¿Qué necesito producir hoy?')}
</p>
</div>
</div>
</CardHeader>
<CardBody>
<div className="p-4 bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg">
<p className="text-[var(--color-error)] text-sm">
{t('dashboard:messages.error_loading', 'Error al cargar los datos')}
</p>
</div>
</CardBody>
</Card>
);
}
return (
<Card className={className} variant="elevated" padding="none">
<CardHeader padding="lg" divider>
<div className="flex items-center justify-between w-full flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[var(--color-primary)]/20">
<Factory className="w-5 h-5 text-[var(--color-primary)]" />
</div>
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{t('dashboard:sections.production_today', 'Producción de Hoy')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('dashboard:production.title', '¿Qué necesito producir hoy?')}
</p>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
{delayedBatches > 0 && (
<Badge variant="error" size="sm">
{delayedBatches} retrasados
</Badge>
)}
{inProgressBatches > 0 && (
<Badge variant="info" size="sm">
{inProgressBatches} activos
</Badge>
)}
{completedBatches > 0 && (
<Badge variant="success" size="sm">
{completedBatches} completados
</Badge>
)}
<div className="flex items-center gap-1 text-sm text-[var(--text-secondary)]">
<Calendar className="w-4 h-4" />
<span>{new Date(todayDate).toLocaleDateString('es-ES')}</span>
</div>
</div>
</div>
</CardHeader>
<CardBody padding="none">
{displayBatches.length === 0 ? (
<div className="p-8 text-center">
<div
className="p-4 rounded-full mx-auto mb-4 w-16 h-16 flex items-center justify-center"
style={{ backgroundColor: 'var(--color-success)/20' }}
>
<CheckCircle className="w-8 h-8" style={{ color: 'var(--color-success)' }} />
</div>
<h4 className="text-lg font-medium mb-2 text-[var(--text-primary)]">
{t('dashboard:production.empty', 'Sin producción programada para hoy')}
</h4>
<p className="text-sm text-[var(--text-secondary)]">
No hay lotes programados para iniciar hoy
</p>
</div>
) : (
<div className="space-y-3 p-4">
{displayBatches.map((batch) => {
const statusConfig = getBatchStatusConfig(batch);
// Calculate progress based on status and time
let progress = 0;
if (batch.status === 'COMPLETED') {
progress = 100;
} else if (batch.status === 'IN_PROGRESS' && batch.actual_start_time && batch.planned_duration_minutes) {
const elapsed = Date.now() - new Date(batch.actual_start_time).getTime();
const elapsedMinutes = elapsed / (1000 * 60);
progress = Math.min(Math.round((elapsedMinutes / batch.planned_duration_minutes) * 100), 99);
} else if (batch.status === 'QUALITY_CHECK') {
progress = 95;
}
const startTime = batch.planned_start_time
? new Date(batch.planned_start_time).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})
: 'No programado';
const assignedStaff = batch.staff_assigned && batch.staff_assigned.length > 0
? batch.staff_assigned[0]
: 'Sin asignar';
return (
<StatusCard
key={batch.id}
id={batch.id}
statusIndicator={statusConfig}
title={batch.product_name}
subtitle={`Lote ${batch.batch_number}${batch.planned_quantity} unidades`}
primaryValue={`${progress}%`}
primaryValueLabel="PROGRESO"
secondaryInfo={{
label: 'Panadero asignado',
value: assignedStaff
}}
progress={batch.status !== 'PENDING' ? {
label: `Progreso de producción`,
percentage: progress,
color: progress === 100 ? 'var(--color-success)' :
progress > 70 ? 'var(--color-info)' :
progress > 30 ? 'var(--color-warning)' : 'var(--color-error)'
} : undefined}
metadata={[
`⏰ Inicio: ${startTime}`,
...(batch.planned_duration_minutes ? [`⏱️ Duración: ${formatDuration(batch.planned_duration_minutes)}`] : []),
...(batch.station_id ? [`🏭 Estación: ${batch.station_id}`] : []),
...(batch.priority === 'URGENT' ? [`⚠️ URGENTE`] : []),
...(batch.production_notes ? [`📋 ${batch.production_notes}`] : [])
]}
actions={[
...(batch.status === 'PENDING' ? [{
label: 'Iniciar',
icon: Play,
variant: 'primary' as const,
onClick: () => onStartBatch?.(batch.id),
priority: 'primary' as const
}] : []),
...(batch.status === 'IN_PROGRESS' ? [{
label: 'Pausar',
icon: Pause,
variant: 'outline' as const,
onClick: () => onPauseBatch?.(batch.id),
priority: 'primary' as const,
destructive: true
}] : []),
{
label: 'Ver Detalles',
icon: ChevronRight,
variant: 'outline' as const,
onClick: () => onViewDetails?.(batch.id),
priority: 'secondary' as const
}
]}
compact={true}
className="border-l-4"
/>
);
})}
</div>
)}
{displayBatches.length > 0 && (
<div
className="p-4 border-t"
style={{
borderColor: 'var(--border-primary)',
backgroundColor: 'var(--bg-secondary)'
}}
>
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="text-sm">
<span className="text-[var(--text-secondary)]">
{pendingBatches} {t('dashboard:production.batches_pending', 'lotes pendientes')} de {productionData?.batches?.length || 0} total
</span>
</div>
{onViewAllPlans && (
<Button
variant="outline"
size="sm"
onClick={onViewAllPlans}
className="flex items-center gap-2"
>
Ver Todos los Planes
<ChevronRight className="w-4 h-4" />
</Button>
)}
</div>
</div>
)}
</CardBody>
</Card>
);
};
export default TodayProduction;