Files
bakery-ia/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
2025-08-31 10:46:13 +02:00

569 lines
20 KiB
TypeScript

import React, { useState } from 'react';
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Edit } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
const ProcurementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('orders');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedOrder, setSelectedOrder] = useState<typeof mockPurchaseOrders[0] | null>(null);
const mockPurchaseOrders = [
{
id: 'PO-2024-001',
supplier: 'Molinos del Sur',
status: 'pending',
orderDate: '2024-01-25',
deliveryDate: '2024-01-28',
totalAmount: 1250.00,
items: [
{ name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 },
{ name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 },
],
paymentStatus: 'pending',
notes: 'Entrega en horario de mañana',
},
{
id: 'PO-2024-002',
supplier: 'Levaduras SA',
status: 'delivered',
orderDate: '2024-01-20',
deliveryDate: '2024-01-23',
totalAmount: 425.50,
items: [
{ name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 },
{ name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 },
],
paymentStatus: 'paid',
notes: '',
},
{
id: 'PO-2024-003',
supplier: 'Lácteos Frescos',
status: 'in_transit',
orderDate: '2024-01-24',
deliveryDate: '2024-01-26',
totalAmount: 320.75,
items: [
{ name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 },
{ name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 },
],
paymentStatus: 'pending',
notes: 'Producto refrigerado',
},
];
const mockSuppliers = [
{
id: '1',
name: 'Molinos del Sur',
contact: 'Juan Pérez',
email: 'juan@molinosdelsur.com',
phone: '+34 91 234 5678',
category: 'Harinas',
rating: 4.8,
totalOrders: 24,
totalSpent: 15600.00,
paymentTerms: '30 días',
leadTime: '2-3 días',
location: 'Sevilla',
status: 'active',
},
{
id: '2',
name: 'Levaduras SA',
contact: 'María González',
email: 'maria@levaduras.com',
phone: '+34 93 456 7890',
category: 'Levaduras',
rating: 4.6,
totalOrders: 18,
totalSpent: 8450.00,
paymentTerms: '15 días',
leadTime: '1-2 días',
location: 'Barcelona',
status: 'active',
},
{
id: '3',
name: 'Lácteos Frescos',
contact: 'Carlos Ruiz',
email: 'carlos@lacteosfrescos.com',
phone: '+34 96 789 0123',
category: 'Lácteos',
rating: 4.4,
totalOrders: 32,
totalSpent: 12300.00,
paymentTerms: '20 días',
leadTime: '1 día',
location: 'Valencia',
status: 'active',
},
];
const getPurchaseStatusConfig = (status: string, paymentStatus: string) => {
const statusConfig = {
pending: { text: 'Pendiente', icon: Clock },
approved: { text: 'Aprobado', icon: CheckCircle },
in_transit: { text: 'En Tránsito', icon: Truck },
delivered: { text: 'Entregado', icon: CheckCircle },
cancelled: { text: 'Cancelado', icon: AlertCircle },
};
const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon;
const isPaymentPending = paymentStatus === 'pending';
const isOverdue = paymentStatus === 'overdue';
return {
color: getStatusColor(status === 'in_transit' ? 'inTransit' : status),
text: config?.text || status,
icon: Icon,
isCritical: isOverdue,
isHighlight: isPaymentPending
};
};
const filteredOrders = mockPurchaseOrders.filter(order => {
const matchesSearch = order.supplier.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.notes.toLowerCase().includes(searchTerm.toLowerCase());
return matchesSearch;
});
const mockPurchaseStats = {
totalOrders: mockPurchaseOrders.length,
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
inTransit: mockPurchaseOrders.filter(o => o.status === 'in_transit').length,
delivered: mockPurchaseOrders.filter(o => o.status === 'delivered').length,
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
};
const purchaseOrderStats = [
{
title: 'Total Órdenes',
value: mockPurchaseStats.totalOrders,
variant: 'default' as const,
icon: ShoppingCart,
},
{
title: 'Pendientes',
value: mockPurchaseStats.pendingOrders,
variant: 'warning' as const,
icon: Clock,
},
{
title: 'En Tránsito',
value: mockPurchaseStats.inTransit,
variant: 'info' as const,
icon: Truck,
},
{
title: 'Entregadas',
value: mockPurchaseStats.delivered,
variant: 'success' as const,
icon: CheckCircle,
},
{
title: 'Gasto Total',
value: formatters.currency(mockPurchaseStats.totalSpent),
variant: 'success' as const,
icon: DollarSign,
},
{
title: 'Proveedores',
value: mockPurchaseStats.activeSuppliers,
variant: 'info' as const,
icon: Package,
},
];
return (
<div className="space-y-6">
<PageHeader
title="Gestión de Compras"
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
actions={[
{
id: "export",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => console.log('Export purchase orders')
},
{
id: "new",
label: "Nueva Orden de Compra",
variant: "primary" as const,
icon: Plus,
onClick: () => console.log('New purchase order')
}
]}
/>
{/* Stats Grid */}
<StatsGrid
stats={purchaseOrderStats}
columns={3}
/>
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('orders')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'orders'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Órdenes de Compra
</button>
<button
onClick={() => setActiveTab('suppliers')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'suppliers'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Proveedores
</button>
<button
onClick={() => setActiveTab('analytics')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'analytics'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Análisis
</button>
</nav>
</div>
{activeTab === 'orders' && (
<Card className="p-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar órdenes por proveedor, ID o notas..."
value={searchTerm}
onChange={(e) => setSearchTerm(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>
)}
{/* Purchase Orders Grid */}
{activeTab === 'orders' && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => {
const statusConfig = getPurchaseStatusConfig(order.status, order.paymentStatus);
const paymentNote = order.paymentStatus === 'pending' ? 'Pago pendiente' : order.paymentStatus === 'overdue' ? 'Pago vencido' : '';
return (
<StatusCard
key={order.id}
id={order.id}
statusIndicator={statusConfig}
title={order.supplier}
subtitle={order.id}
primaryValue={formatters.currency(order.totalAmount)}
primaryValueLabel={`${order.items?.length} artículos`}
secondaryInfo={{
label: 'Entrega',
value: `${new Date(order.deliveryDate).toLocaleDateString('es-ES')} (pedido: ${new Date(order.orderDate).toLocaleDateString('es-ES')})`
}}
metadata={[
...(order.notes ? [`"${order.notes}"`] : []),
...(paymentNote ? [paymentNote] : [])
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('view');
setShowForm(true);
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedOrder(order);
setModalMode('edit');
setShowForm(true);
}
}
]}
/>
);
})}
</div>
)}
{/* Empty State for Purchase Orders */}
{activeTab === 'orders' && filteredOrders.length === 0 && (
<div className="text-center py-12">
<ShoppingCart 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 compra
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Intenta ajustar la búsqueda o crear una nueva orden de compra
</p>
<Button onClick={() => console.log('New purchase order')}>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Compra
</Button>
</div>
)}
{activeTab === 'suppliers' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{mockSuppliers.map((supplier) => (
<Card key={supplier.id} className="p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{supplier.name}</h3>
<p className="text-sm text-[var(--text-secondary)]">{supplier.category}</p>
</div>
<Badge variant="green">Activo</Badge>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Contacto:</span>
<span className="font-medium">{supplier.contact}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Email:</span>
<span className="font-medium">{supplier.email}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Teléfono:</span>
<span className="font-medium">{supplier.phone}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-[var(--text-secondary)]">Ubicación:</span>
<span className="font-medium">{supplier.location}</span>
</div>
</div>
<div className="border-t pt-4">
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-[var(--text-secondary)]">Valoración</p>
<p className="text-sm font-medium flex items-center">
<span className="text-yellow-500"></span>
<span className="ml-1">{supplier.rating}</span>
</p>
</div>
<div>
<p className="text-xs text-[var(--text-secondary)]">Pedidos</p>
<p className="text-sm font-medium">{supplier.totalOrders}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-[var(--text-secondary)]">Total Gastado</p>
<p className="text-sm font-medium">{supplier.totalSpent.toLocaleString()}</p>
</div>
<div>
<p className="text-xs text-[var(--text-secondary)]">Tiempo Entrega</p>
<p className="text-sm font-medium">{supplier.leadTime}</p>
</div>
</div>
<div className="mb-4">
<p className="text-xs text-[var(--text-secondary)]">Condiciones de Pago</p>
<p className="text-sm font-medium">{supplier.paymentTerms}</p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Detalles
</Button>
<Button size="sm" className="flex-1">
Nuevo Pedido
</Button>
</div>
</div>
</Card>
))}
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Mes</h3>
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">Gráfico de gastos mensuales</p>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Top Proveedores</h3>
<div className="space-y-3">
{mockSuppliers
.sort((a, b) => b.totalSpent - a.totalSpent)
.slice(0, 5)
.map((supplier, index) => (
<div key={supplier.id} className="flex items-center justify-between">
<div className="flex items-center">
<span className="text-sm font-medium text-[var(--text-tertiary)] w-4">
{index + 1}.
</span>
<span className="ml-3 text-sm text-[var(--text-primary)]">{supplier.name}</span>
</div>
<span className="text-sm font-medium text-[var(--text-primary)]">
{supplier.totalSpent.toLocaleString()}
</span>
</div>
))}
</div>
</Card>
</div>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Categoría</h3>
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">Gráfico de gastos por categoría</p>
</div>
</Card>
</div>
)}
{/* Purchase Order Modal */}
{showForm && selectedOrder && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
title={selectedOrder.supplier}
subtitle={`Orden de Compra ${selectedOrder.id}`}
statusIndicator={getPurchaseStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)}
size="lg"
sections={[
{
title: 'Información del Proveedor',
icon: Package,
fields: [
{
label: 'Proveedor',
value: selectedOrder.supplier,
highlight: true,
editable: true,
required: true,
placeholder: 'Nombre del proveedor'
},
{
label: 'ID de Orden',
value: selectedOrder.id
},
{
label: 'Estado de Pago',
value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : selectedOrder.paymentStatus === 'pending' ? 'Pendiente' : 'Vencido',
type: 'status'
}
]
},
{
title: 'Fechas Importantes',
icon: Calendar,
fields: [
{
label: 'Fecha de pedido',
value: selectedOrder.orderDate,
type: 'date',
editable: true
},
{
label: 'Fecha de entrega',
value: selectedOrder.deliveryDate,
type: 'date',
highlight: true,
editable: true,
required: true
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Importe total',
value: selectedOrder.totalAmount,
type: 'currency',
highlight: true,
editable: true,
required: true,
placeholder: '0.00'
},
{
label: 'Número de artículos',
value: `${selectedOrder.items?.length} productos`
}
]
},
{
title: 'Artículos Pedidos',
icon: ShoppingCart,
fields: [
{
label: 'Lista de productos',
value: selectedOrder.items?.map(item => `${item.name}: ${item.quantity} ${item.unit} - ${formatters.currency(item.total)}`),
type: 'list',
span: 2
}
]
},
...(selectedOrder.notes ? [{
title: 'Notas Adicionales',
fields: [
{
label: 'Observaciones',
value: selectedOrder.notes,
span: 2 as const,
editable: true,
placeholder: 'Añadir notas sobre la orden de compra...'
}
]
}] : [])
]}
onEdit={() => {
console.log('Editing purchase order:', selectedOrder.id);
}}
/>
)}
</div>
);
};
export default ProcurementPage;