Files
bakery-ia/frontend/src/pages/orders/OrdersPage.tsx
2025-08-21 20:28:14 +02:00

620 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { Package, Plus, Edit, Trash2, Calendar, CheckCircle, AlertCircle, Clock, BarChart3, TrendingUp, Euro, Settings } from 'lucide-react';
// Import complex components
import WhatIfPlanner from '../../components/ui/WhatIfPlanner';
import DemandHeatmap from '../../components/ui/DemandHeatmap';
interface Order {
id: string;
supplier: string;
items: OrderItem[];
orderDate: string;
deliveryDate: string;
status: 'pending' | 'confirmed' | 'delivered' | 'cancelled';
total: number;
type: 'ingredients' | 'consumables';
}
interface OrderItem {
name: string;
quantity: number;
unit: string;
price: number;
suggested?: boolean;
}
interface OrdersPageProps {
view?: string;
}
const OrdersPage: React.FC<OrdersPageProps> = ({ view = 'incoming' }) => {
const [orders, setOrders] = useState<Order[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showNewOrder, setShowNewOrder] = useState(false);
const [activeTab, setActiveTab] = useState<'orders' | 'analytics' | 'forecasting' | 'suppliers'>('orders');
// Sample orders data
const sampleOrders: Order[] = [
{
id: '1',
supplier: 'Harinas Castellana',
items: [
{ name: 'Harina de trigo', quantity: 50, unit: 'kg', price: 0.85, suggested: true },
{ name: 'Levadura fresca', quantity: 2, unit: 'kg', price: 3.20 },
{ name: 'Sal marina', quantity: 5, unit: 'kg', price: 1.10 }
],
orderDate: '2024-11-03',
deliveryDate: '2024-11-05',
status: 'pending',
total: 52.50,
type: 'ingredients'
},
{
id: '2',
supplier: 'Distribuciones Madrid',
items: [
{ name: 'Vasos de café 250ml', quantity: 1000, unit: 'unidades', price: 0.08 },
{ name: 'Bolsas papel kraft', quantity: 500, unit: 'unidades', price: 0.12, suggested: true },
{ name: 'Servilletas', quantity: 20, unit: 'paquetes', price: 2.50 }
],
orderDate: '2024-11-02',
deliveryDate: '2024-11-04',
status: 'confirmed',
total: 190.00,
type: 'consumables'
},
{
id: '3',
supplier: 'Lácteos Frescos SA',
items: [
{ name: 'Leche entera', quantity: 20, unit: 'litros', price: 0.95 },
{ name: 'Mantequilla', quantity: 5, unit: 'kg', price: 4.20 },
{ name: 'Nata para montar', quantity: 3, unit: 'litros', price: 2.80 }
],
orderDate: '2024-11-01',
deliveryDate: '2024-11-03',
status: 'delivered',
total: 47.40,
type: 'ingredients'
}
];
useEffect(() => {
const loadOrders = async () => {
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 800));
setOrders(sampleOrders);
} catch (error) {
console.error('Error loading orders:', error);
} finally {
setIsLoading(false);
}
};
loadOrders();
}, []);
const getStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'bg-warning-100 text-warning-800 border-warning-200';
case 'confirmed':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'delivered':
return 'bg-success-100 text-success-800 border-success-200';
case 'cancelled':
return 'bg-red-100 text-red-800 border-red-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'pending':
return 'Pendiente';
case 'confirmed':
return 'Confirmado';
case 'delivered':
return 'Entregado';
case 'cancelled':
return 'Cancelado';
default:
return status;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'pending':
return <Clock className="h-4 w-4" />;
case 'confirmed':
return <AlertCircle className="h-4 w-4" />;
case 'delivered':
return <CheckCircle className="h-4 w-4" />;
case 'cancelled':
return <AlertCircle className="h-4 w-4" />;
default:
return <Clock className="h-4 w-4" />;
}
};
// Sample data for complex components
const orderDemandHeatmapData = [
{
weekStart: '2024-11-04',
days: [
{
date: '2024-11-04',
demand: 180,
isToday: true,
products: [
{ name: 'Harina de trigo', demand: 50, confidence: 'high' as const },
{ name: 'Levadura fresca', demand: 2, confidence: 'high' as const },
{ name: 'Mantequilla', demand: 5, confidence: 'medium' as const },
{ name: 'Vasos café', demand: 1000, confidence: 'medium' as const },
]
},
{ date: '2024-11-05', demand: 165, isForecast: true },
{ date: '2024-11-06', demand: 195, isForecast: true },
{ date: '2024-11-07', demand: 220, isForecast: true },
{ date: '2024-11-08', demand: 185, isForecast: true },
{ date: '2024-11-09', demand: 250, isForecast: true },
{ date: '2024-11-10', demand: 160, isForecast: true }
]
}
];
const baselineSupplyData = {
totalDemand: 180,
totalRevenue: 420,
products: [
{ name: 'Harina de trigo', demand: 50, price: 0.85 },
{ name: 'Levadura fresca', demand: 2, price: 3.20 },
{ name: 'Mantequilla', demand: 5, price: 4.20 },
{ name: 'Leche entera', demand: 20, price: 0.95 },
{ name: 'Vasos café', demand: 1000, price: 0.08 },
]
};
const filteredOrders = orders.filter(order => {
if (activeTab === 'orders') return true;
return false;
});
const handleDeleteOrder = (orderId: string) => {
setOrders(prev => prev.filter(order => order.id !== orderId));
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-48 bg-gray-200 rounded-xl"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Gestión de Pedidos</h1>
<p className="text-gray-600 mt-1">
Administra tus pedidos de ingredientes y consumibles
</p>
</div>
<button
onClick={() => setShowNewOrder(true)}
className="mt-4 sm:mt-0 inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all hover:shadow-lg"
>
<Plus className="h-5 w-5 mr-2" />
Nuevo Pedido
</button>
</div>
{/* Enhanced Tabs */}
<div className="bg-white rounded-xl shadow-soft p-1">
<div className="flex space-x-1">
{[
{ id: 'orders', label: 'Gestión de Pedidos', icon: Package, count: orders.length },
{ id: 'analytics', label: 'Análisis', icon: BarChart3 },
{ id: 'forecasting', label: 'Simulaciones', icon: TrendingUp },
{ id: 'suppliers', label: 'Proveedores', icon: Settings }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 py-3 px-4 text-sm font-medium rounded-lg transition-all flex items-center justify-center ${
activeTab === tab.id
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
<tab.icon className="h-4 w-4 mr-2" />
{tab.label}
{tab.count && (
<span className="ml-2 px-2 py-1 bg-gray-200 text-gray-700 rounded-full text-xs">
{tab.count}
</span>
)}
</button>
))}
</div>
</div>
{/* AI Suggestions */}
<div className="bg-gradient-to-r from-primary-50 to-orange-50 border border-primary-200 rounded-xl p-6">
<div className="flex items-start">
<div className="p-2 bg-primary-100 rounded-lg mr-4">
<Package className="h-6 w-6 text-primary-600" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-primary-900 mb-2">Sugerencias Inteligentes de Pedidos</h3>
<div className="space-y-2 text-sm text-primary-800">
<p> <strong>Harina de trigo:</strong> Stock bajo detectado. Recomendamos pedir 50kg para cubrir 2 semanas.</p>
<p> <strong>Bolsas de papel:</strong> Aumento del 15% en takeaway. Considera aumentar el pedido habitual.</p>
<p> <strong>Café en grano:</strong> Predicción de alta demanda por temperaturas bajas. +20% recomendado.</p>
</div>
<button className="mt-3 text-primary-700 hover:text-primary-600 font-medium text-sm">
Ver todas las sugerencias
</button>
</div>
</div>
</div>
{/* Tab Content */}
{activeTab === 'orders' && (
<>
{/* Orders Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredOrders.map((order) => (
<div key={order.id} className="bg-white rounded-xl shadow-soft hover:shadow-medium transition-shadow">
{/* Order Header */}
<div className="p-6 border-b border-gray-100">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-gray-900">{order.supplier}</h3>
<span className={`px-2 py-1 rounded-lg text-xs font-medium border flex items-center ${getStatusColor(order.status)}`}>
{getStatusIcon(order.status)}
<span className="ml-1">{getStatusLabel(order.status)}</span>
</span>
</div>
<div className="flex items-center text-sm text-gray-600 mb-2">
<Calendar className="h-4 w-4 mr-1" />
<span>Entrega: {new Date(order.deliveryDate).toLocaleDateString('es-ES')}</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-xs px-2 py-1 rounded ${
order.type === 'ingredients'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{order.type === 'ingredients' ? 'Ingredientes' : 'Consumibles'}
</span>
<span className="text-lg font-bold text-gray-900">{order.total.toFixed(2)}</span>
</div>
</div>
{/* Order Items */}
<div className="p-6">
<h4 className="text-sm font-medium text-gray-700 mb-3">Artículos ({order.items.length})</h4>
<div className="space-y-2 max-h-32 overflow-y-auto">
{order.items.map((item, index) => (
<div key={index} className="flex justify-between text-sm">
<div className="flex-1">
<span className="text-gray-900">{item.name}</span>
{item.suggested && (
<span className="ml-2 text-xs bg-primary-100 text-primary-700 px-1 py-0.5 rounded">
IA
</span>
)}
<div className="text-gray-500 text-xs">
{item.quantity} {item.unit} × {item.price.toFixed(2)}
</div>
</div>
<span className="text-gray-900 font-medium">
{(item.quantity * item.price).toFixed(2)}
</span>
</div>
))}
</div>
</div>
{/* Order Actions */}
<div className="px-6 pb-6">
<div className="flex space-x-2">
<button className="flex-1 py-2 px-3 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center justify-center">
<Edit className="h-4 w-4 mr-1" />
Editar
</button>
<button
onClick={() => handleDeleteOrder(order.id)}
className="py-2 px-3 text-sm bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{filteredOrders.length === 0 && (
<div className="text-center py-12">
<Package className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay pedidos</h3>
<p className="text-gray-600 mb-4">
{activeTab === 'orders'
? 'Aún no has creado ningún pedido'
: 'No hay datos disponibles para esta sección'
}
</p>
<button
onClick={() => setShowNewOrder(true)}
className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors"
>
<Plus className="h-5 w-5 mr-2" />
Crear primer pedido
</button>
</div>
)}
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-primary-100 rounded-lg">
<Package className="h-6 w-6 text-primary-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Pedidos</p>
<p className="text-2xl font-bold text-gray-900">{orders.length}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-warning-100 rounded-lg">
<Clock className="h-6 w-6 text-warning-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Pendientes</p>
<p className="text-2xl font-bold text-gray-900">
{orders.filter(o => o.status === 'pending' || o.status === 'confirmed').length}
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-soft">
<div className="flex items-center">
<div className="p-2 bg-success-100 rounded-lg">
<CheckCircle className="h-6 w-6 text-success-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Gasto Mensual</p>
<p className="text-2xl font-bold text-gray-900">
{orders.reduce((sum, order) => sum + order.total, 0).toFixed(0)}
</p>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white p-6 rounded-xl shadow-soft">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Acciones Rápidas
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Pedido Automático</div>
<div className="text-sm text-gray-500 mt-1">Basado en predicciones IA</div>
</button>
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Gestión de Proveedores</div>
<div className="text-sm text-gray-500 mt-1">Añadir o editar proveedores</div>
</button>
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Historial de Gastos</div>
<div className="text-sm text-gray-500 mt-1">Ver análisis de costos</div>
</button>
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
<div className="font-medium text-gray-900">Configurar Alertas</div>
<div className="text-sm text-gray-500 mt-1">Stock bajo y vencimientos</div>
</button>
</div>
</div>
</>
)}
{/* Analytics Tab */}
{activeTab === 'analytics' && (
<div className="space-y-6">
<DemandHeatmap
data={orderDemandHeatmapData}
selectedProduct="Ingredientes"
onDateClick={(date) => {
console.log('Selected date:', date);
}}
/>
{/* Cost Analysis Chart */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Euro className="h-5 w-5 mr-2 text-primary-600" />
Análisis de Costos
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800 font-semibold">Ahorro Mensual</div>
<div className="text-2xl font-bold text-green-900">124.50</div>
<div className="text-sm text-green-700">vs mes anterior</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-blue-800 font-semibold">Gasto Promedio</div>
<div className="text-2xl font-bold text-blue-900">289.95</div>
<div className="text-sm text-blue-700">por pedido</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-purple-800 font-semibold">Eficiencia</div>
<div className="text-2xl font-bold text-purple-900">94.2%</div>
<div className="text-sm text-purple-700">predicción IA</div>
</div>
</div>
<div className="mt-6 h-64 bg-gray-50 rounded-lg flex items-center justify-center">
<div className="text-center text-gray-500">
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-400" />
<p>Gráfico de tendencias de costos</p>
<p className="text-sm">Próximamente disponible</p>
</div>
</div>
</div>
</div>
)}
{/* Forecasting/Simulations Tab */}
{activeTab === 'forecasting' && (
<div className="space-y-6">
<WhatIfPlanner
baselineData={baselineSupplyData}
onScenarioRun={(scenario, result) => {
console.log('Scenario run:', scenario, result);
}}
/>
</div>
)}
{/* Suppliers Tab */}
{activeTab === 'suppliers' && (
<div className="space-y-6">
{/* Suppliers Management */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Settings className="h-5 w-5 mr-2 text-primary-600" />
Gestión de Proveedores
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
name: 'Harinas Castellana',
category: 'Ingredientes',
rating: 4.8,
reliability: 98,
nextDelivery: '2024-11-05',
status: 'active'
},
{
name: 'Distribuciones Madrid',
category: 'Consumibles',
rating: 4.5,
reliability: 95,
nextDelivery: '2024-11-04',
status: 'active'
},
{
name: 'Lácteos Frescos SA',
category: 'Ingredientes',
rating: 4.9,
reliability: 99,
nextDelivery: '2024-11-03',
status: 'active'
}
].map((supplier, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">{supplier.name}</h4>
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
</span>
</div>
<div className="space-y-2">
<div className="text-sm text-gray-600">
<span className="font-medium">Categoría:</span> {supplier.category}
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Calificación:</span> {supplier.rating}/5
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Confiabilidad:</span> {supplier.reliability}%
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">Próxima entrega:</span> {new Date(supplier.nextDelivery).toLocaleDateString('es-ES')}
</div>
</div>
<div className="mt-4 flex space-x-2">
<button className="flex-1 px-3 py-2 text-sm bg-primary-100 text-primary-700 rounded-lg hover:bg-primary-200 transition-colors">
Editar
</button>
<button className="px-3 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
Contactar
</button>
</div>
</div>
))}
</div>
<div className="mt-6 text-center">
<button className="inline-flex items-center px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors">
<Plus className="h-4 w-4 mr-2" />
Añadir Proveedor
</button>
</div>
</div>
</div>
)}
{/* New Order Modal Placeholder */}
{showNewOrder && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl p-6 max-w-md w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Nuevo Pedido</h3>
<p className="text-gray-600 mb-6">
Esta funcionalidad estará disponible próximamente. PanIA analizará tus necesidades
y creará pedidos automáticos basados en las predicciones de demanda.
</p>
<div className="flex space-x-3">
<button
onClick={() => setShowNewOrder(false)}
className="flex-1 py-2 px-4 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-colors"
>
Cerrar
</button>
<button
onClick={() => setShowNewOrder(false)}
className="flex-1 py-2 px-4 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors"
>
Entendido
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default OrdersPage;