442 lines
16 KiB
TypeScript
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; |