449 lines
18 KiB
TypeScript
449 lines
18 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react';
|
|
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
|
import { PageHeader } from '../../../../components/layout';
|
|
|
|
const ProcurementPage: React.FC = () => {
|
|
const [activeTab, setActiveTab] = useState('orders');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
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 getStatusBadge = (status: string) => {
|
|
const statusConfig = {
|
|
pending: { color: 'yellow', text: 'Pendiente' },
|
|
approved: { color: 'blue', text: 'Aprobado' },
|
|
in_transit: { color: 'purple', text: 'En Tránsito' },
|
|
delivered: { color: 'green', text: 'Entregado' },
|
|
cancelled: { color: 'red', text: 'Cancelado' },
|
|
};
|
|
|
|
const config = statusConfig[status as keyof typeof statusConfig];
|
|
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
|
};
|
|
|
|
const getPaymentStatusBadge = (status: string) => {
|
|
const statusConfig = {
|
|
pending: { color: 'yellow', text: 'Pendiente' },
|
|
paid: { color: 'green', text: 'Pagado' },
|
|
overdue: { color: 'red', text: 'Vencido' },
|
|
};
|
|
|
|
const config = statusConfig[status as keyof typeof statusConfig];
|
|
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
|
};
|
|
|
|
const stats = {
|
|
totalOrders: mockPurchaseOrders.length,
|
|
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
|
|
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
|
|
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<PageHeader
|
|
title="Gestión de Compras"
|
|
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
|
|
action={
|
|
<Button>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Nueva Orden de Compra
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Órdenes Totales</p>
|
|
<p className="text-3xl font-bold text-[var(--text-primary)]">{stats.totalOrders}</p>
|
|
</div>
|
|
<ShoppingCart className="h-12 w-12 text-[var(--color-info)]" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Órdenes Pendientes</p>
|
|
<p className="text-3xl font-bold text-[var(--color-primary)]">{stats.pendingOrders}</p>
|
|
</div>
|
|
<Calendar className="h-12 w-12 text-[var(--color-primary)]" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Gasto Total</p>
|
|
<p className="text-3xl font-bold text-[var(--color-success)]">€{stats.totalSpent.toLocaleString()}</p>
|
|
</div>
|
|
<DollarSign className="h-12 w-12 text-[var(--color-success)]" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Proveedores Activos</p>
|
|
<p className="text-3xl font-bold text-purple-600">{stats.activeSuppliers}</p>
|
|
</div>
|
|
<Truck className="h-12 w-12 text-purple-600" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 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>
|
|
|
|
{/* Search and Filters */}
|
|
<Card className="p-6">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
|
<Input
|
|
placeholder={`Buscar ${activeTab === 'orders' ? 'órdenes' : 'proveedores'}...`}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button variant="outline">
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
Filtros
|
|
</Button>
|
|
<Button variant="outline">
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Exportar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'orders' && (
|
|
<Card>
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-[var(--bg-secondary)]">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Orden
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Proveedor
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Estado
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Fecha Pedido
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Fecha Entrega
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Monto Total
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Pago
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
|
Acciones
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{mockPurchaseOrders.map((order) => (
|
|
<tr key={order.id} className="hover:bg-[var(--bg-secondary)]">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-[var(--text-primary)]">{order.id}</div>
|
|
{order.notes && (
|
|
<div className="text-xs text-[var(--text-tertiary)]">{order.notes}</div>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
|
{order.supplier}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
{getStatusBadge(order.status)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
|
{new Date(order.orderDate).toLocaleDateString('es-ES')}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
|
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--text-primary)]">
|
|
€{order.totalAmount.toLocaleString()}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
{getPaymentStatusBadge(order.paymentStatus)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div className="flex space-x-2">
|
|
<Button variant="outline" size="sm">Ver</Button>
|
|
<Button variant="outline" size="sm">Editar</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{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>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProcurementPage; |