Files
bakery-ia/frontend/src/pages/app/operations/production/ProductionPage.tsx
2025-08-30 19:11:15 +02:00

442 lines
16 KiB
TypeScript

import React, { useState } from 'react';
import { Plus, Download, Clock, Users, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Calendar, Zap } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { pagePresets } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
const ProductionPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('schedule');
const [searchQuery, setSearchQuery] = useState('');
const [selectedOrder, setSelectedOrder] = useState<typeof mockProductionOrders[0] | null>(null);
const [showForm, setShowForm] = useState(false);
const mockProductionStats = {
dailyTarget: 150,
completed: 85,
inProgress: 12,
pending: 53,
efficiency: 78,
quality: 94,
};
const mockProductionOrders = [
{
id: '1',
recipeName: 'Pan de Molde Integral',
quantity: 20,
status: 'in_progress',
priority: 'high',
assignedTo: 'Juan Panadero',
startTime: '2024-01-26T06:00:00Z',
estimatedCompletion: '2024-01-26T10:00:00Z',
progress: 65,
},
{
id: '2',
recipeName: 'Croissants de Mantequilla',
quantity: 50,
status: 'pending',
priority: 'medium',
assignedTo: 'María González',
startTime: '2024-01-26T08:00:00Z',
estimatedCompletion: '2024-01-26T12:00:00Z',
progress: 0,
},
{
id: '3',
recipeName: 'Baguettes Francesas',
quantity: 30,
status: 'completed',
priority: 'medium',
assignedTo: 'Carlos Ruiz',
startTime: '2024-01-26T04:00:00Z',
estimatedCompletion: '2024-01-26T08:00:00Z',
progress: 100,
},
{
id: '4',
recipeName: 'Tarta de Chocolate',
quantity: 5,
status: 'pending',
priority: 'low',
assignedTo: 'Ana Pastelera',
startTime: '2024-01-26T10:00:00Z',
estimatedCompletion: '2024-01-26T16:00:00Z',
progress: 0,
},
{
id: '5',
recipeName: 'Empanadas de Pollo',
quantity: 40,
status: 'in_progress',
priority: 'high',
assignedTo: 'Luis Hornero',
startTime: '2024-01-26T07:00:00Z',
estimatedCompletion: '2024-01-26T11:00:00Z',
progress: 45,
},
{
id: '6',
recipeName: 'Donuts Glaseados',
quantity: 60,
status: 'pending',
priority: 'urgent',
assignedTo: 'María González',
startTime: '2024-01-26T12:00:00Z',
estimatedCompletion: '2024-01-26T15:00:00Z',
progress: 0,
},
{
id: '7',
recipeName: 'Pan de Centeno',
quantity: 25,
status: 'completed',
priority: 'medium',
assignedTo: 'Juan Panadero',
startTime: '2024-01-26T05:00:00Z',
estimatedCompletion: '2024-01-26T09:00:00Z',
progress: 100,
},
{
id: '8',
recipeName: 'Muffins de Arándanos',
quantity: 36,
status: 'in_progress',
priority: 'medium',
assignedTo: 'Ana Pastelera',
startTime: '2024-01-26T08:30:00Z',
estimatedCompletion: '2024-01-26T12:30:00Z',
progress: 70,
},
];
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
in_progress: { color: 'info', text: 'En Proceso', icon: Timer },
completed: { color: 'success', text: 'Completado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle },
};
const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'outline', text: 'Baja' },
medium: { color: 'secondary', text: 'Media' },
high: { color: 'warning', text: 'Alta' },
urgent: { color: 'error', text: 'Urgente', icon: Zap },
};
const config = priorityConfig[priority as keyof typeof priorityConfig];
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || priority}
/>
);
};
const filteredOrders = mockProductionOrders.filter(order => {
const matchesSearch = order.recipeName.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.assignedTo.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.id.toLowerCase().includes(searchQuery.toLowerCase());
return matchesSearch;
});
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: "export",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => console.log('Export production orders')
},
{
id: "new",
label: "Nueva Orden de Producción",
variant: "primary" as const,
icon: Plus,
onClick: () => setShowForm(true)
}
]}
/>
{/* Production Stats */}
<StatsGrid
stats={pagePresets.production(mockProductionStats)}
columns={6}
/>
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('schedule')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'schedule'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Programación
</button>
<button
onClick={() => setActiveTab('batches')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'batches'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Lotes de Producción
</button>
<button
onClick={() => setActiveTab('quality')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Control de Calidad
</button>
</nav>
</div>
{/* Production Orders Tab */}
{activeTab === 'schedule' && (
<>
{/* Simplified Controls */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar órdenes por receta, asignado o ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full"
/>
</div>
<Button variant="outline" onClick={() => console.log('Export filtered')}>
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</Card>
{/* Production Orders Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => (
<Card key={order.id} className="p-4">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
<ChefHat className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div>
<div className="font-medium text-[var(--text-primary)]">
{order.recipeName}
</div>
<div className="text-sm text-[var(--text-secondary)]">
ID: {order.id}
</div>
</div>
</div>
{getStatusBadge(order.status)}
</div>
{/* Priority and Quantity */}
<div className="flex items-center justify-between">
<div>
{getPriorityBadge(order.priority)}
</div>
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{order.quantity}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
unidades
</div>
</div>
</div>
{/* Assigned Worker */}
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-sm text-[var(--text-primary)]">
{order.assignedTo}
</span>
</div>
{/* Time Information */}
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Inicio:</span>
<span className="text-[var(--text-primary)]">
{new Date(order.startTime).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Est. finalización:</span>
<span className="text-[var(--text-primary)]">
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
{/* Progress Bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Progreso</span>
<span className="text-[var(--text-primary)]">
{order.progress}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
order.progress === 100
? 'bg-[var(--color-success)]'
: order.progress > 50
? 'bg-[var(--color-info)]'
: order.progress > 0
? 'bg-[var(--color-warning)]'
: 'bg-[var(--bg-quaternary)]'
}`}
style={{ width: `${order.progress}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedOrder(order);
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedOrder(order);
setShowForm(true);
}}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
</div>
{/* Empty State */}
{filteredOrders.length === 0 && (
<div className="text-center py-12">
<ChefHat className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No se encontraron órdenes de producción
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Intenta ajustar la búsqueda o crear una nueva orden de producción
</p>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Producción
</Button>
</div>
)}
</>
)}
{activeTab === 'batches' && (
<BatchTracker />
)}
{activeTab === 'quality' && (
<QualityControl />
)}
{/* Production Order Form Modal - Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{selectedOrder ? 'Ver Orden de Producción' : 'Nueva Orden de Producción'}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
setShowForm(false);
setSelectedOrder(null);
}}
>
Cerrar
</Button>
</div>
{selectedOrder && (
<div className="space-y-4">
<h3 className="text-lg font-medium">{selectedOrder.recipeName}</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Cantidad:</span> {selectedOrder.quantity} unidades
</div>
<div>
<span className="font-medium">Asignado a:</span> {selectedOrder.assignedTo}
</div>
<div>
<span className="font-medium">Estado:</span> {selectedOrder.status}
</div>
<div>
<span className="font-medium">Progreso:</span> {selectedOrder.progress}%
</div>
<div>
<span className="font-medium">Inicio:</span> {new Date(selectedOrder.startTime).toLocaleString('es-ES')}
</div>
<div>
<span className="font-medium">Finalización:</span> {new Date(selectedOrder.estimatedCompletion).toLocaleString('es-ES')}
</div>
</div>
</div>
)}
</Card>
</div>
)}
</div>
);
};
export default ProductionPage;