371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
import React from 'react';
|
|
import { Card, CardHeader, CardBody } from '../../ui/Card';
|
|
import { StatusCard } from '../../ui/StatusCard/StatusCard';
|
|
import { Badge } from '../../ui/Badge';
|
|
import { Button } from '../../ui/Button';
|
|
import {
|
|
Factory,
|
|
Clock,
|
|
Users,
|
|
Thermometer,
|
|
Play,
|
|
Pause,
|
|
CheckCircle,
|
|
AlertTriangle,
|
|
ChevronRight,
|
|
Timer,
|
|
Package,
|
|
Flame
|
|
} from 'lucide-react';
|
|
|
|
export interface ProductionOrder {
|
|
id: string;
|
|
product: string;
|
|
quantity: number;
|
|
unit: string;
|
|
priority: 'urgent' | 'high' | 'medium' | 'low';
|
|
status: 'pending' | 'in_progress' | 'completed' | 'paused' | 'delayed';
|
|
startTime: string;
|
|
estimatedDuration: number; // in minutes
|
|
assignedBaker: string;
|
|
ovenNumber?: number;
|
|
temperature?: number;
|
|
progress: number; // 0-100
|
|
notes?: string;
|
|
recipe: string;
|
|
ingredients: Array<{
|
|
name: string;
|
|
quantity: number;
|
|
unit: string;
|
|
available: boolean;
|
|
}>;
|
|
}
|
|
|
|
export interface ProductionPlansProps {
|
|
className?: string;
|
|
orders?: ProductionOrder[];
|
|
onStartOrder?: (orderId: string) => void;
|
|
onPauseOrder?: (orderId: string) => void;
|
|
onViewDetails?: (orderId: string) => void;
|
|
onViewAllPlans?: () => void;
|
|
}
|
|
|
|
const ProductionPlansToday: React.FC<ProductionPlansProps> = ({
|
|
className,
|
|
orders = [],
|
|
onStartOrder,
|
|
onPauseOrder,
|
|
onViewDetails,
|
|
onViewAllPlans
|
|
}) => {
|
|
const defaultOrders: ProductionOrder[] = [
|
|
{
|
|
id: '1',
|
|
product: 'Pan de Molde Integral',
|
|
quantity: 20,
|
|
unit: 'unidades',
|
|
priority: 'urgent',
|
|
status: 'in_progress',
|
|
startTime: '06:00',
|
|
estimatedDuration: 180,
|
|
assignedBaker: 'María González',
|
|
ovenNumber: 1,
|
|
temperature: 220,
|
|
progress: 65,
|
|
recipe: 'Receta Estándar Integral',
|
|
ingredients: [
|
|
{ name: 'Harina integral', quantity: 5, unit: 'kg', available: true },
|
|
{ name: 'Levadura', quantity: 0.5, unit: 'kg', available: true },
|
|
{ name: 'Sal', quantity: 0.2, unit: 'kg', available: true },
|
|
{ name: 'Agua', quantity: 3, unit: 'L', available: true }
|
|
]
|
|
},
|
|
{
|
|
id: '2',
|
|
product: 'Croissants de Mantequilla',
|
|
quantity: 50,
|
|
unit: 'unidades',
|
|
priority: 'high',
|
|
status: 'pending',
|
|
startTime: '07:30',
|
|
estimatedDuration: 240,
|
|
assignedBaker: 'Carlos Rodríguez',
|
|
ovenNumber: 2,
|
|
temperature: 200,
|
|
progress: 0,
|
|
recipe: 'Croissant Francés',
|
|
notes: 'Masa preparada ayer, lista para horneado',
|
|
ingredients: [
|
|
{ name: 'Masa de croissant', quantity: 3, unit: 'kg', available: true },
|
|
{ name: 'Mantequilla', quantity: 1, unit: 'kg', available: false },
|
|
{ name: 'Huevo', quantity: 6, unit: 'unidades', available: true }
|
|
]
|
|
},
|
|
{
|
|
id: '3',
|
|
product: 'Baguettes Tradicionales',
|
|
quantity: 30,
|
|
unit: 'unidades',
|
|
priority: 'medium',
|
|
status: 'completed',
|
|
startTime: '05:00',
|
|
estimatedDuration: 240,
|
|
assignedBaker: 'Ana Martín',
|
|
ovenNumber: 3,
|
|
temperature: 240,
|
|
progress: 100,
|
|
recipe: 'Baguette Francesa',
|
|
ingredients: [
|
|
{ name: 'Harina blanca', quantity: 4, unit: 'kg', available: true },
|
|
{ name: 'Levadura', quantity: 0.3, unit: 'kg', available: true },
|
|
{ name: 'Sal', quantity: 0.15, unit: 'kg', available: true },
|
|
{ name: 'Agua', quantity: 2.5, unit: 'L', available: true }
|
|
]
|
|
},
|
|
{
|
|
id: '4',
|
|
product: 'Magdalenas de Vainilla',
|
|
quantity: 100,
|
|
unit: 'unidades',
|
|
priority: 'medium',
|
|
status: 'delayed',
|
|
startTime: '09:00',
|
|
estimatedDuration: 90,
|
|
assignedBaker: 'Luis Fernández',
|
|
ovenNumber: 4,
|
|
temperature: 180,
|
|
progress: 0,
|
|
recipe: 'Magdalenas Clásicas',
|
|
notes: 'Retraso por falta de moldes',
|
|
ingredients: [
|
|
{ name: 'Harina', quantity: 2, unit: 'kg', available: true },
|
|
{ name: 'Azúcar', quantity: 1.5, unit: 'kg', available: true },
|
|
{ name: 'Huevos', quantity: 24, unit: 'unidades', available: true },
|
|
{ name: 'Mantequilla', quantity: 1, unit: 'kg', available: false },
|
|
{ name: 'Vainilla', quantity: 50, unit: 'ml', available: true }
|
|
]
|
|
}
|
|
];
|
|
|
|
const displayOrders = orders.length > 0 ? orders : defaultOrders;
|
|
|
|
const getOrderStatusConfig = (order: ProductionOrder) => {
|
|
const baseConfig = {
|
|
isCritical: order.status === 'delayed' || order.priority === 'urgent',
|
|
isHighlight: order.status === 'in_progress' || order.priority === 'high',
|
|
};
|
|
|
|
switch (order.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: Play
|
|
};
|
|
case 'completed':
|
|
return {
|
|
...baseConfig,
|
|
color: 'var(--color-success)',
|
|
text: 'Completado',
|
|
icon: CheckCircle
|
|
};
|
|
case 'paused':
|
|
return {
|
|
...baseConfig,
|
|
color: 'var(--color-warning)',
|
|
text: 'Pausado',
|
|
icon: Pause
|
|
};
|
|
case 'delayed':
|
|
return {
|
|
...baseConfig,
|
|
color: 'var(--color-error)',
|
|
text: 'Retrasado',
|
|
icon: AlertTriangle
|
|
};
|
|
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`;
|
|
};
|
|
|
|
const inProgressOrders = displayOrders.filter(order => order.status === 'in_progress').length;
|
|
const completedOrders = displayOrders.filter(order => order.status === 'completed').length;
|
|
const delayedOrders = displayOrders.filter(order => order.status === 'delayed').length;
|
|
|
|
return (
|
|
<Card className={className} variant="elevated" padding="none">
|
|
<CardHeader padding="lg" divider>
|
|
<div className="flex items-center justify-between w-full">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className="p-2 rounded-lg"
|
|
style={{ backgroundColor: 'var(--color-primary)20' }}
|
|
>
|
|
<Factory className="w-5 h-5" style={{ color: 'var(--color-primary)' }} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>
|
|
Planes de Producción - Hoy
|
|
</h3>
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
Gestiona la producción programada para hoy
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{delayedOrders > 0 && (
|
|
<Badge variant="error" size="sm">
|
|
{delayedOrders} retrasadas
|
|
</Badge>
|
|
)}
|
|
{inProgressOrders > 0 && (
|
|
<Badge variant="info" size="sm">
|
|
{inProgressOrders} activas
|
|
</Badge>
|
|
)}
|
|
<Badge variant="success" size="sm">
|
|
{completedOrders} completadas
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardBody padding="none">
|
|
{displayOrders.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' }}
|
|
>
|
|
<Factory className="w-8 h-8" style={{ color: 'var(--color-success)' }} />
|
|
</div>
|
|
<h4 className="text-lg font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
No hay producción programada
|
|
</h4>
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
Día libre de producción
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3 p-4">
|
|
{displayOrders.map((order) => {
|
|
const statusConfig = getOrderStatusConfig(order);
|
|
const availableIngredients = order.ingredients.filter(ing => ing.available).length;
|
|
const totalIngredients = order.ingredients.length;
|
|
const ingredientsReady = availableIngredients === totalIngredients;
|
|
|
|
return (
|
|
<StatusCard
|
|
key={order.id}
|
|
id={order.id}
|
|
statusIndicator={statusConfig}
|
|
title={order.product}
|
|
subtitle={`${order.recipe} • ${order.quantity} ${order.unit}`}
|
|
primaryValue={`${order.progress}%`}
|
|
primaryValueLabel="PROGRESO"
|
|
secondaryInfo={{
|
|
label: 'Panadero asignado',
|
|
value: order.assignedBaker
|
|
}}
|
|
progress={order.status !== 'pending' ? {
|
|
label: `Progreso de producción`,
|
|
percentage: order.progress,
|
|
color: order.progress === 100 ? 'var(--color-success)' :
|
|
order.progress > 70 ? 'var(--color-info)' :
|
|
order.progress > 30 ? 'var(--color-warning)' : 'var(--color-error)'
|
|
} : undefined}
|
|
metadata={[
|
|
`⏰ Inicio: ${order.startTime}`,
|
|
`⏱️ Duración: ${formatDuration(order.estimatedDuration)}`,
|
|
...(order.ovenNumber ? [`🔥 Horno ${order.ovenNumber} - ${order.temperature}°C`] : []),
|
|
`📋 Ingredientes: ${availableIngredients}/${totalIngredients} ${ingredientsReady ? '✅' : '❌'}`,
|
|
...(order.notes ? [`📝 ${order.notes}`] : [])
|
|
]}
|
|
actions={[
|
|
...(order.status === 'pending' ? [{
|
|
label: 'Iniciar',
|
|
icon: Play,
|
|
variant: 'primary' as const,
|
|
onClick: () => onStartOrder?.(order.id),
|
|
priority: 'primary' as const
|
|
}] : []),
|
|
...(order.status === 'in_progress' ? [{
|
|
label: 'Pausar',
|
|
icon: Pause,
|
|
variant: 'outline' as const,
|
|
onClick: () => onPauseOrder?.(order.id),
|
|
priority: 'primary' as const,
|
|
destructive: true
|
|
}] : []),
|
|
{
|
|
label: 'Ver Detalles',
|
|
icon: ChevronRight,
|
|
variant: 'outline' as const,
|
|
onClick: () => onViewDetails?.(order.id),
|
|
priority: 'secondary' as const
|
|
}
|
|
]}
|
|
compact={true}
|
|
className="border-l-4"
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{displayOrders.length > 0 && (
|
|
<div
|
|
className="p-4 border-t"
|
|
style={{
|
|
borderColor: 'var(--border-primary)',
|
|
backgroundColor: 'var(--bg-secondary)'
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm">
|
|
<span style={{ color: 'var(--text-secondary)' }}>
|
|
{completedOrders} de {displayOrders.length} órdenes completadas
|
|
</span>
|
|
</div>
|
|
|
|
<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 ProductionPlansToday; |