Add frontend imporvements

This commit is contained in:
Urtzi Alfaro
2025-09-09 21:39:12 +02:00
parent 23e088dcb4
commit 2a05048912
16 changed files with 1761 additions and 1233 deletions

View File

@@ -0,0 +1,435 @@
import React, { useState } from 'react';
import { Plus, Download, Building2, Phone, Mail, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, DollarSign } 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';
import { SupplierStatus, SupplierType, PaymentTerms } from '../../../../api/types/suppliers';
const SuppliersPage: React.FC = () => {
const [activeTab] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedSupplier, setSelectedSupplier] = useState<typeof mockSuppliers[0] | null>(null);
const mockSuppliers = [
{
id: 'SUP-2024-001',
supplier_code: 'HAR001',
name: 'Harinas del Norte S.L.',
supplier_type: SupplierType.INGREDIENTS,
status: SupplierStatus.ACTIVE,
contact_person: 'María González',
email: 'maria@harinasdelnorte.es',
phone: '+34 987 654 321',
city: 'León',
country: 'España',
payment_terms: PaymentTerms.NET_30,
credit_limit: 5000,
currency: 'EUR',
standard_lead_time: 5,
minimum_order_amount: 200,
total_orders: 45,
total_spend: 12750.50,
last_order_date: '2024-01-25T14:30:00Z',
performance_score: 92,
notes: 'Proveedor principal de harinas. Excelente calidad y puntualidad.'
},
{
id: 'SUP-2024-002',
supplier_code: 'EMB002',
name: 'Embalajes Biodegradables SA',
supplier_type: SupplierType.PACKAGING,
status: SupplierStatus.ACTIVE,
contact_person: 'Carlos Ruiz',
email: 'carlos@embalajes-bio.com',
phone: '+34 600 123 456',
city: 'Valencia',
country: 'España',
payment_terms: PaymentTerms.NET_15,
credit_limit: 2500,
currency: 'EUR',
standard_lead_time: 3,
minimum_order_amount: 150,
total_orders: 28,
total_spend: 4280.75,
last_order_date: '2024-01-24T10:15:00Z',
performance_score: 88,
notes: 'Especialista en packaging sostenible.'
},
{
id: 'SUP-2024-003',
supplier_code: 'MAN003',
name: 'Maquinaria Industrial López',
supplier_type: SupplierType.EQUIPMENT,
status: SupplierStatus.PENDING_APPROVAL,
contact_person: 'Ana López',
email: 'ana@maquinaria-lopez.es',
phone: '+34 655 987 654',
city: 'Madrid',
country: 'España',
payment_terms: PaymentTerms.NET_45,
credit_limit: 15000,
currency: 'EUR',
standard_lead_time: 14,
minimum_order_amount: 500,
total_orders: 0,
total_spend: 0,
last_order_date: null,
performance_score: null,
notes: 'Nuevo proveedor de equipamiento industrial. Pendiente de aprobación.'
},
];
const getSupplierStatusConfig = (status: SupplierStatus) => {
const statusConfig = {
[SupplierStatus.ACTIVE]: { text: 'Activo', icon: CheckCircle },
[SupplierStatus.INACTIVE]: { text: 'Inactivo', icon: Timer },
[SupplierStatus.PENDING_APPROVAL]: { text: 'Pendiente Aprobación', icon: AlertCircle },
[SupplierStatus.SUSPENDED]: { text: 'Suspendido', icon: AlertCircle },
[SupplierStatus.BLACKLISTED]: { text: 'Lista Negra', icon: AlertCircle },
};
const config = statusConfig[status];
const Icon = config?.icon;
return {
color: getStatusColor(status === SupplierStatus.ACTIVE ? 'completed' :
status === SupplierStatus.PENDING_APPROVAL ? 'pending' : 'cancelled'),
text: config?.text || status,
icon: Icon,
isCritical: status === SupplierStatus.BLACKLISTED,
isHighlight: status === SupplierStatus.PENDING_APPROVAL
};
};
const getSupplierTypeText = (type: SupplierType): string => {
const typeMap = {
[SupplierType.INGREDIENTS]: 'Ingredientes',
[SupplierType.PACKAGING]: 'Embalajes',
[SupplierType.EQUIPMENT]: 'Equipamiento',
[SupplierType.SERVICES]: 'Servicios',
[SupplierType.UTILITIES]: 'Servicios Públicos',
[SupplierType.MULTI]: 'Múltiple',
};
return typeMap[type] || type;
};
const getPaymentTermsText = (terms: PaymentTerms): string => {
const termsMap = {
[PaymentTerms.CASH_ON_DELIVERY]: 'Pago Contraentrega',
[PaymentTerms.NET_15]: 'Neto 15 días',
[PaymentTerms.NET_30]: 'Neto 30 días',
[PaymentTerms.NET_45]: 'Neto 45 días',
[PaymentTerms.NET_60]: 'Neto 60 días',
[PaymentTerms.PREPAID]: 'Prepago',
};
return termsMap[terms] || terms;
};
const filteredSuppliers = mockSuppliers.filter(supplier => {
const matchesSearch = supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
supplier.supplier_code.toLowerCase().includes(searchTerm.toLowerCase()) ||
supplier.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
supplier.contact_person?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesTab = activeTab === 'all' || supplier.status === activeTab;
return matchesSearch && matchesTab;
});
const mockSupplierStats = {
total: mockSuppliers.length,
active: mockSuppliers.filter(s => s.status === SupplierStatus.ACTIVE).length,
pendingApproval: mockSuppliers.filter(s => s.status === SupplierStatus.PENDING_APPROVAL).length,
suspended: mockSuppliers.filter(s => s.status === SupplierStatus.SUSPENDED).length,
totalSpend: mockSuppliers.reduce((sum, supplier) => sum + supplier.total_spend, 0),
averageScore: mockSuppliers
.filter(s => s.performance_score !== null)
.reduce((sum, supplier, _, arr) => sum + (supplier.performance_score || 0) / arr.length, 0),
totalOrders: mockSuppliers.reduce((sum, supplier) => sum + supplier.total_orders, 0),
};
const stats = [
{
title: 'Total Proveedores',
value: mockSupplierStats.total,
variant: 'default' as const,
icon: Building2,
},
{
title: 'Activos',
value: mockSupplierStats.active,
variant: 'success' as const,
icon: CheckCircle,
},
{
title: 'Pendientes',
value: mockSupplierStats.pendingApproval,
variant: 'warning' as const,
icon: AlertCircle,
},
{
title: 'Gasto Total',
value: formatters.currency(mockSupplierStats.totalSpend),
variant: 'info' as const,
icon: DollarSign,
},
{
title: 'Total Pedidos',
value: mockSupplierStats.totalOrders,
variant: 'default' as const,
icon: Building2,
},
{
title: 'Puntuación Media',
value: mockSupplierStats.averageScore.toFixed(1),
variant: 'success' as const,
icon: CheckCircle,
},
];
return (
<div className="space-y-6">
<PageHeader
title="Gestión de Proveedores"
description="Administra y supervisa todos los proveedores de la panadería"
actions={[
{
id: "export",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => console.log('Export suppliers')
},
{
id: "new",
label: "Nuevo Proveedor",
variant: "primary" as const,
icon: Plus,
onClick: () => setShowForm(true)
}
]}
/>
{/* Stats Grid */}
<StatsGrid
stats={stats}
columns={3}
/>
{/* Simplified Controls */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar proveedores por nombre, código, email o contacto..."
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>
{/* Suppliers Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredSuppliers.map((supplier) => {
const statusConfig = getSupplierStatusConfig(supplier.status);
const performanceNote = supplier.performance_score
? `Puntuación: ${supplier.performance_score}/100`
: 'Sin evaluación';
return (
<StatusCard
key={supplier.id}
id={supplier.id}
statusIndicator={statusConfig}
title={supplier.name}
subtitle={supplier.supplier_code}
primaryValue={formatters.currency(supplier.total_spend)}
primaryValueLabel={`${supplier.total_orders} pedidos`}
secondaryInfo={{
label: 'Tipo',
value: getSupplierTypeText(supplier.supplier_type)
}}
metadata={[
supplier.contact_person || 'Sin contacto',
supplier.email || 'Sin email',
supplier.phone || 'Sin teléfono',
performanceNote
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedSupplier(supplier);
setModalMode('view');
setShowForm(true);
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedSupplier(supplier);
setModalMode('edit');
setShowForm(true);
}
}
]}
/>
);
})}
</div>
{/* Empty State */}
{filteredSuppliers.length === 0 && (
<div className="text-center py-12">
<Building2 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 proveedores
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Intenta ajustar la búsqueda o crear un nuevo proveedor
</p>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Proveedor
</Button>
</div>
)}
{/* Supplier Details Modal */}
{showForm && selectedSupplier && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedSupplier(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
title={selectedSupplier.name}
subtitle={`Proveedor ${selectedSupplier.supplier_code}`}
statusIndicator={getSupplierStatusConfig(selectedSupplier.status)}
size="lg"
sections={[
{
title: 'Información de Contacto',
icon: Users,
fields: [
{
label: 'Nombre',
value: selectedSupplier.name,
highlight: true
},
{
label: 'Persona de Contacto',
value: selectedSupplier.contact_person || 'No especificado'
},
{
label: 'Email',
value: selectedSupplier.email || 'No especificado'
},
{
label: 'Teléfono',
value: selectedSupplier.phone || 'No especificado'
},
{
label: 'Ciudad',
value: selectedSupplier.city || 'No especificado'
},
{
label: 'País',
value: selectedSupplier.country || 'No especificado'
}
]
},
{
title: 'Información Comercial',
icon: Building2,
fields: [
{
label: 'Código de Proveedor',
value: selectedSupplier.supplier_code,
highlight: true
},
{
label: 'Tipo de Proveedor',
value: getSupplierTypeText(selectedSupplier.supplier_type)
},
{
label: 'Condiciones de Pago',
value: getPaymentTermsText(selectedSupplier.payment_terms)
},
{
label: 'Tiempo de Entrega',
value: `${selectedSupplier.standard_lead_time} días`
},
{
label: 'Pedido Mínimo',
value: selectedSupplier.minimum_order_amount,
type: 'currency'
},
{
label: 'Límite de Crédito',
value: selectedSupplier.credit_limit,
type: 'currency'
}
]
},
{
title: 'Rendimiento y Estadísticas',
icon: DollarSign,
fields: [
{
label: 'Total de Pedidos',
value: selectedSupplier.total_orders.toString()
},
{
label: 'Gasto Total',
value: selectedSupplier.total_spend,
type: 'currency',
highlight: true
},
{
label: 'Puntuación de Rendimiento',
value: selectedSupplier.performance_score ? `${selectedSupplier.performance_score}/100` : 'No evaluado'
},
{
label: 'Último Pedido',
value: selectedSupplier.last_order_date || 'Nunca',
type: selectedSupplier.last_order_date ? 'datetime' : undefined
}
]
},
...(selectedSupplier.notes ? [{
title: 'Notas',
fields: [
{
label: 'Observaciones',
value: selectedSupplier.notes,
span: 2 as const
}
]
}] : [])
]}
onEdit={() => {
console.log('Editing supplier:', selectedSupplier.id);
}}
/>
)}
</div>
);
};
export default SuppliersPage;

View File

@@ -0,0 +1 @@
export { default as SuppliersPage } from './SuppliersPage';