Files
bakery-ia/frontend/src/pages/app/operations/production/ProductionPage.tsx

376 lines
12 KiB
TypeScript
Raw Normal View History

2025-08-28 10:41:04 +02:00
import React, { useState } from 'react';
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);
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
];
const getProductionStatusConfig = (status: string, priority: string) => {
2025-08-28 10:41:04 +02:00
const statusConfig = {
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;
const isUrgent = priority === 'urgent';
2025-08-28 10:41:04 +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">
{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);
setModalMode('view');
2025-08-30 19:11:15 +02:00
setShowForm(true);
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
2025-08-30 19:11:15 +02:00
setSelectedOrder(order);
setModalMode('edit');
2025-08-30 19:11:15 +02:00
setShowForm(true);
}
}
]}
/>
);
})}
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
{/* 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;