Add new page designs
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, Table } from '../../../../components/ui';
|
||||
import type { TableColumn } from '../../../../components/ui';
|
||||
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Edit } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const ProcurementPage: React.FC = () => {
|
||||
@@ -103,152 +103,126 @@ const ProcurementPage: React.FC = () => {
|
||||
|
||||
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' },
|
||||
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
|
||||
approved: { color: 'info', text: 'Aprobado', icon: CheckCircle },
|
||||
in_transit: { color: 'secondary', text: 'En Tránsito', icon: Truck },
|
||||
delivered: { color: 'success', text: 'Entregado', icon: CheckCircle },
|
||||
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
const Icon = config?.icon;
|
||||
return (
|
||||
<Badge
|
||||
variant={config?.color as any}
|
||||
icon={Icon && <Icon size={12} />}
|
||||
text={config?.text || status}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getPaymentStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
paid: { color: 'green', text: 'Pagado' },
|
||||
overdue: { color: 'red', text: 'Vencido' },
|
||||
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
|
||||
paid: { color: 'success', text: 'Pagado', icon: CheckCircle },
|
||||
overdue: { color: 'error', text: 'Vencido', icon: AlertCircle },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
const Icon = config?.icon;
|
||||
return (
|
||||
<Badge
|
||||
variant={config?.color as any}
|
||||
icon={Icon && <Icon size={12} />}
|
||||
text={config?.text || status}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
key: 'id',
|
||||
title: 'Orden',
|
||||
dataIndex: 'id',
|
||||
render: (value, record: any) => (
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">{value}</div>
|
||||
{record.notes && (
|
||||
<div className="text-xs text-[var(--text-tertiary)]">{record.notes}</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'supplier',
|
||||
title: 'Proveedor',
|
||||
dataIndex: 'supplier',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: 'Estado',
|
||||
dataIndex: 'status',
|
||||
render: (value) => getStatusBadge(value),
|
||||
},
|
||||
{
|
||||
key: 'orderDate',
|
||||
title: 'Fecha Pedido',
|
||||
dataIndex: 'orderDate',
|
||||
render: (value) => new Date(value).toLocaleDateString('es-ES'),
|
||||
},
|
||||
{
|
||||
key: 'deliveryDate',
|
||||
title: 'Fecha Entrega',
|
||||
dataIndex: 'deliveryDate',
|
||||
render: (value) => new Date(value).toLocaleDateString('es-ES'),
|
||||
},
|
||||
{
|
||||
key: 'totalAmount',
|
||||
title: 'Monto Total',
|
||||
dataIndex: 'totalAmount',
|
||||
render: (value) => `€${value.toLocaleString()}`,
|
||||
},
|
||||
{
|
||||
key: 'paymentStatus',
|
||||
title: 'Pago',
|
||||
dataIndex: 'paymentStatus',
|
||||
render: (value) => getPaymentStatusBadge(value),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: 'Acciones',
|
||||
align: 'right' as const,
|
||||
render: () => (
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">Ver</Button>
|
||||
<Button variant="outline" size="sm">Editar</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
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 stats = {
|
||||
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="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>
|
||||
}
|
||||
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 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>
|
||||
{/* Stats Grid */}
|
||||
<StatsGrid
|
||||
stats={purchaseOrderStats}
|
||||
columns={6}
|
||||
/>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-[var(--border-primary)]">
|
||||
@@ -286,48 +260,128 @@ const ProcurementPage: React.FC = () => {
|
||||
</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" />
|
||||
{activeTab === 'orders' && (
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder={`Buscar ${activeTab === 'orders' ? 'órdenes' : 'proveedores'}...`}
|
||||
placeholder="Buscar órdenes por proveedor, ID o notas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Button variant="outline" onClick={() => console.log('Export filtered')}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'orders' && (
|
||||
<Card className="p-6">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={mockPurchaseOrders}
|
||||
rowKey="id"
|
||||
hover={true}
|
||||
variant="default"
|
||||
size="md"
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Purchase Orders Grid */}
|
||||
{activeTab === 'orders' && (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredOrders.map((order) => (
|
||||
<Card key={order.id} className="p-4">
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
|
||||
<ShoppingCart className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-mono text-sm font-semibold text-[var(--color-primary)]">
|
||||
{order.id}
|
||||
</div>
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
{order.supplier}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge(order.status)}
|
||||
</div>
|
||||
|
||||
{/* Key Info */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-[var(--text-primary)]">
|
||||
{formatters.currency(order.totalAmount)}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
{order.items?.length} artículos
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-[var(--text-primary)]">
|
||||
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
Entrega prevista
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
Estado del pago:
|
||||
</div>
|
||||
{getPaymentStatusBadge(order.paymentStatus)}
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
{order.notes && (
|
||||
<div className="bg-[var(--bg-secondary)] p-2 rounded text-xs text-[var(--text-secondary)] italic">
|
||||
"{order.notes}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => console.log('View order', order.id)}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
Ver
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => console.log('Edit order', order.id)}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-1" />
|
||||
Editar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</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) => (
|
||||
|
||||
Reference in New Issue
Block a user