2025-08-28 10:41:04 +02:00
|
|
|
import React, { useState } from 'react';
|
2025-08-31 10:46:13 +02:00
|
|
|
import { Plus, Download, Clock, Users, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Calendar, Zap, Package } from 'lucide-react';
|
|
|
|
|
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
2025-08-30 19:11:15 +02:00
|
|
|
import { pagePresets } from '../../../../components/ui/Stats/StatsPresets';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { PageHeader } from '../../../../components/layout';
|
|
|
|
|
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
|
|
|
|
|
|
|
|
|
|
const ProductionPage: React.FC = () => {
|
|
|
|
|
const [activeTab, setActiveTab] = useState('schedule');
|
2025-08-28 23:40:44 +02:00
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
2025-08-30 19:11:15 +02:00
|
|
|
const [selectedOrder, setSelectedOrder] = useState<typeof mockProductionOrders[0] | null>(null);
|
|
|
|
|
const [showForm, setShowForm] = useState(false);
|
2025-08-31 10:46:13 +02:00
|
|
|
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
2025-08-28 23:40:44 +02:00
|
|
|
{
|
|
|
|
|
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,
|
|
|
|
|
},
|
2025-08-28 10:41:04 +02:00
|
|
|
];
|
|
|
|
|
|
2025-08-31 10:46:13 +02:00
|
|
|
const getProductionStatusConfig = (status: string, priority: string) => {
|
2025-08-28 10:41:04 +02:00
|
|
|
const statusConfig = {
|
2025-08-31 10:46:13 +02:00
|
|
|
pending: { text: 'Pendiente', icon: Clock },
|
|
|
|
|
in_progress: { text: 'En Proceso', icon: Timer },
|
|
|
|
|
completed: { text: 'Completado', icon: CheckCircle },
|
|
|
|
|
cancelled: { text: 'Cancelado', icon: AlertCircle },
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const config = statusConfig[status as keyof typeof statusConfig];
|
2025-08-30 19:11:15 +02:00
|
|
|
const Icon = config?.icon;
|
2025-08-31 10:46:13 +02:00
|
|
|
const isUrgent = priority === 'urgent';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-08-31 10:46:13 +02:00
|
|
|
return {
|
|
|
|
|
color: getStatusColor(status),
|
|
|
|
|
text: config?.text || status,
|
|
|
|
|
icon: Icon,
|
|
|
|
|
isCritical: isUrgent,
|
|
|
|
|
isHighlight: false
|
|
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-30 19:11:15 +02:00
|
|
|
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;
|
|
|
|
|
});
|
2025-08-28 18:07:16 +02:00
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
return (
|
2025-08-30 19:11:15 +02:00
|
|
|
<div className="space-y-6">
|
2025-08-28 10:41:04 +02:00
|
|
|
<PageHeader
|
|
|
|
|
title="Gestión de Producción"
|
|
|
|
|
description="Planifica y controla la producción diaria de tu panadería"
|
2025-08-30 19:11:15 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
]}
|
2025-08-28 10:41:04 +02:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Production Stats */}
|
2025-08-30 19:11:15 +02:00
|
|
|
<StatsGrid
|
|
|
|
|
stats={pagePresets.production(mockProductionStats)}
|
2025-08-30 19:21:15 +02:00
|
|
|
columns={3}
|
2025-08-30 19:11:15 +02:00
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
|
2025-08-30 19:11:15 +02:00
|
|
|
{/* Production Orders Tab */}
|
2025-08-28 10:41:04 +02:00
|
|
|
{activeTab === 'schedule' && (
|
2025-08-30 19:11:15 +02:00
|
|
|
<>
|
|
|
|
|
{/* 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"
|
|
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-08-30 19:11:15 +02:00
|
|
|
<Button variant="outline" onClick={() => console.log('Export filtered')}>
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
Exportar
|
|
|
|
|
</Button>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-08-30 19:11:15 +02:00
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Production Orders Grid */}
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
2025-08-31 10:46:13 +02:00
|
|
|
{filteredOrders.map((order) => {
|
|
|
|
|
const statusConfig = getProductionStatusConfig(order.status, order.priority);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StatusCard
|
|
|
|
|
key={order.id}
|
|
|
|
|
id={order.id}
|
|
|
|
|
statusIndicator={statusConfig}
|
|
|
|
|
title={order.recipeName}
|
|
|
|
|
subtitle={`Asignado a: ${order.assignedTo}`}
|
|
|
|
|
primaryValue={order.quantity}
|
|
|
|
|
primaryValueLabel="unidades"
|
|
|
|
|
secondaryInfo={{
|
|
|
|
|
label: 'Horario',
|
|
|
|
|
value: `${new Date(order.startTime).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} → ${new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}`
|
|
|
|
|
}}
|
|
|
|
|
progress={{
|
|
|
|
|
label: 'Progreso',
|
|
|
|
|
percentage: order.progress,
|
|
|
|
|
color: statusConfig.color
|
|
|
|
|
}}
|
|
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
label: 'Ver',
|
|
|
|
|
icon: Eye,
|
|
|
|
|
variant: 'outline',
|
|
|
|
|
onClick: () => {
|
2025-08-30 19:11:15 +02:00
|
|
|
setSelectedOrder(order);
|
2025-08-31 10:46:13 +02:00
|
|
|
setModalMode('view');
|
2025-08-30 19:11:15 +02:00
|
|
|
setShowForm(true);
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Editar',
|
|
|
|
|
icon: Edit,
|
|
|
|
|
variant: 'outline',
|
|
|
|
|
onClick: () => {
|
2025-08-30 19:11:15 +02:00
|
|
|
setSelectedOrder(order);
|
2025-08-31 10:46:13 +02:00
|
|
|
setModalMode('edit');
|
2025-08-30 19:11:15 +02:00
|
|
|
setShowForm(true);
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-08-30 19:11:15 +02:00
|
|
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeTab === 'batches' && (
|
|
|
|
|
<BatchTracker />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeTab === 'quality' && (
|
|
|
|
|
<QualityControl />
|
|
|
|
|
)}
|
2025-08-30 19:11:15 +02:00
|
|
|
|
2025-08-31 10:46:13 +02:00
|
|
|
{/* Production Order Modal */}
|
|
|
|
|
{showForm && selectedOrder && (
|
|
|
|
|
<StatusModal
|
|
|
|
|
isOpen={showForm}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowForm(false);
|
|
|
|
|
setSelectedOrder(null);
|
|
|
|
|
setModalMode('view');
|
|
|
|
|
}}
|
|
|
|
|
mode={modalMode}
|
|
|
|
|
onModeChange={setModalMode}
|
|
|
|
|
title={selectedOrder.recipeName}
|
|
|
|
|
subtitle={`Orden de Producción #${selectedOrder.id}`}
|
|
|
|
|
statusIndicator={getProductionStatusConfig(selectedOrder.status, selectedOrder.priority)}
|
|
|
|
|
size="lg"
|
|
|
|
|
sections={[
|
|
|
|
|
{
|
|
|
|
|
title: 'Información General',
|
|
|
|
|
icon: Package,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
label: 'Cantidad',
|
|
|
|
|
value: `${selectedOrder.quantity} unidades`,
|
|
|
|
|
highlight: true
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Asignado a',
|
|
|
|
|
value: selectedOrder.assignedTo,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Prioridad',
|
|
|
|
|
value: selectedOrder.priority,
|
|
|
|
|
type: 'status'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Progreso',
|
|
|
|
|
value: selectedOrder.progress,
|
|
|
|
|
type: 'percentage',
|
|
|
|
|
highlight: true
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Cronograma',
|
|
|
|
|
icon: Clock,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
label: 'Hora de inicio',
|
|
|
|
|
value: selectedOrder.startTime,
|
|
|
|
|
type: 'datetime'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Finalización estimada',
|
|
|
|
|
value: selectedOrder.estimatedCompletion,
|
|
|
|
|
type: 'datetime'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
onEdit={() => {
|
|
|
|
|
// Handle edit mode
|
|
|
|
|
console.log('Editing production order:', selectedOrder.id);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-08-30 19:11:15 +02:00
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ProductionPage;
|