Files
bakery-ia/frontend/src/pages/app/operations/suppliers/SuppliersPage.tsx

480 lines
16 KiB
TypeScript
Raw Normal View History

2025-09-09 21:39:12 +02:00
import React, { useState } from 'react';
import { Plus, Building2, Phone, Mail, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Euro, Loader } from 'lucide-react';
2025-09-09 21:39:12 +02:00
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';
import { useSuppliers, useSupplierStatistics } from '../../../../api/hooks/suppliers';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { useSupplierEnums } from '../../../../utils/enumHelpers';
2025-09-09 21:39:12 +02:00
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<any>(null);
const [isCreating, setIsCreating] = useState(false);
2025-09-09 21:39:12 +02:00
// Get tenant ID from tenant store (preferred) or auth user (fallback)
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id || '';
// API hooks
const {
data: suppliersData,
isLoading: suppliersLoading,
error: suppliersError
} = useSuppliers(tenantId, {
search_term: searchTerm || undefined,
status: activeTab !== 'all' ? activeTab as any : undefined,
limit: 100
});
const {
data: statisticsData,
isLoading: statisticsLoading,
error: statisticsError
} = useSupplierStatistics(tenantId);
const suppliers = suppliersData || [];
const supplierEnums = useSupplierEnums();
2025-09-09 21:39:12 +02:00
const getSupplierStatusConfig = (status: SupplierStatus) => {
const statusConfig = {
[SupplierStatus.ACTIVE]: { text: supplierEnums.getSupplierStatusLabel(status), icon: CheckCircle },
[SupplierStatus.INACTIVE]: { text: supplierEnums.getSupplierStatusLabel(status), icon: Timer },
[SupplierStatus.PENDING_APPROVAL]: { text: supplierEnums.getSupplierStatusLabel(status), icon: AlertCircle },
[SupplierStatus.SUSPENDED]: { text: supplierEnums.getSupplierStatusLabel(status), icon: AlertCircle },
[SupplierStatus.BLACKLISTED]: { text: supplierEnums.getSupplierStatusLabel(status), icon: AlertCircle },
2025-09-09 21:39:12 +02:00
};
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 => {
return supplierEnums.getSupplierTypeLabel(type);
2025-09-09 21:39:12 +02:00
};
const getPaymentTermsText = (terms: PaymentTerms): string => {
return supplierEnums.getPaymentTermsLabel(terms);
2025-09-09 21:39:12 +02:00
};
// Filtering is now handled by the API query parameters
const filteredSuppliers = suppliers;
2025-09-09 21:39:12 +02:00
const supplierStats = statisticsData || {
total_suppliers: 0,
active_suppliers: 0,
pending_suppliers: 0,
avg_quality_rating: 0,
avg_delivery_rating: 0,
total_spend: 0
2025-09-09 21:39:12 +02:00
};
const stats = [
{
title: 'Total Proveedores',
value: supplierStats.total_suppliers,
2025-09-09 21:39:12 +02:00
variant: 'default' as const,
icon: Building2,
},
{
title: 'Activos',
value: supplierStats.active_suppliers,
2025-09-09 21:39:12 +02:00
variant: 'success' as const,
icon: CheckCircle,
},
{
title: 'Pendientes',
value: supplierStats.pending_suppliers,
2025-09-09 21:39:12 +02:00
variant: 'warning' as const,
icon: AlertCircle,
},
{
title: 'Gasto Total',
value: formatters.currency(supplierStats.total_spend),
2025-09-09 21:39:12 +02:00
variant: 'info' as const,
icon: Euro,
2025-09-09 21:39:12 +02:00
},
{
title: 'Calidad Media',
value: supplierStats.avg_quality_rating?.toFixed(1) || '0.0',
2025-09-09 21:39:12 +02:00
variant: 'success' as const,
icon: CheckCircle,
},
{
title: 'Entrega Media',
value: supplierStats.avg_delivery_rating?.toFixed(1) || '0.0',
variant: 'info' as const,
icon: Building2,
},
2025-09-09 21:39:12 +02:00
];
// Loading state
if (suppliersLoading || statisticsLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<Loader className="h-8 w-8 animate-spin mx-auto mb-4 text-[var(--primary)]" />
<p className="text-[var(--text-secondary)]">Cargando proveedores...</p>
</div>
</div>
);
}
// Error state
if (suppliersError || statisticsError) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<AlertCircle className="h-8 w-8 mx-auto mb-4 text-red-500" />
<p className="text-red-600 mb-2">Error al cargar los proveedores</p>
<p className="text-[var(--text-secondary)] text-sm">
{(suppliersError as any)?.message || (statisticsError as any)?.message || 'Error desconocido'}
</p>
</div>
</div>
);
}
2025-09-09 21:39:12 +02:00
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: "new",
label: "Nuevo Proveedor",
2025-09-09 21:39:12 +02:00
variant: "primary" as const,
icon: Plus,
onClick: () => {
setSelectedSupplier({
name: '',
contact_person: '',
email: '',
phone: '',
city: '',
country: '',
supplier_code: '',
supplier_type: SupplierType.INGREDIENTS,
payment_terms: PaymentTerms.NET_30,
standard_lead_time: 3,
minimum_order_amount: 0,
credit_limit: 0,
currency: 'EUR'
});
setIsCreating(true);
setModalMode('edit');
setShowForm(true);
}
2025-09-09 21:39:12 +02:00
}
]}
/>
{/* 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>
</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);
2025-09-09 21:39:12 +02:00
return (
<StatusCard
key={supplier.id}
id={supplier.id}
statusIndicator={statusConfig}
title={supplier.name}
2025-09-19 11:44:38 +02:00
subtitle={`${getSupplierTypeText(supplier.supplier_type)}${supplier.city || 'Sin ubicación'}`}
primaryValue={supplier.standard_lead_time || 0}
primaryValueLabel="días"
2025-09-09 21:39:12 +02:00
secondaryInfo={{
2025-09-19 11:44:38 +02:00
label: 'Pedido Min.',
value: `${formatters.compact(supplier.minimum_order_amount || 0)}`
2025-09-09 21:39:12 +02:00
}}
metadata={[
supplier.contact_person || 'Sin contacto',
supplier.email || 'Sin email',
supplier.phone || 'Sin teléfono',
`Creado: ${new Date(supplier.created_at).toLocaleDateString('es-ES')}`
2025-09-09 21:39:12 +02:00
]}
actions={[
// Primary action - View supplier details
2025-09-09 21:39:12 +02:00
{
label: 'Ver Detalles',
2025-09-09 21:39:12 +02:00
icon: Eye,
variant: 'primary',
priority: 'primary',
2025-09-09 21:39:12 +02:00
onClick: () => {
setSelectedSupplier(supplier);
setIsCreating(false);
2025-09-09 21:39:12 +02:00
setModalMode('view');
setShowForm(true);
}
},
// Secondary action - Edit supplier
2025-09-09 21:39:12 +02:00
{
label: 'Editar',
icon: Edit,
priority: 'secondary',
2025-09-09 21:39:12 +02:00
onClick: () => {
setSelectedSupplier(supplier);
setIsCreating(false);
2025-09-09 21:39:12 +02:00
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 && (() => {
const sections = [
{
title: 'Información de Contacto',
icon: Users,
fields: [
{
label: 'Nombre',
value: selectedSupplier.name || '',
type: 'text',
highlight: true,
editable: true,
required: true,
placeholder: 'Nombre del proveedor'
},
{
label: 'Persona de Contacto',
value: selectedSupplier.contact_person || '',
type: 'text',
editable: true,
placeholder: 'Nombre del contacto'
},
{
label: 'Email',
value: selectedSupplier.email || '',
type: 'email',
editable: true,
placeholder: 'email@ejemplo.com'
},
{
label: 'Teléfono',
value: selectedSupplier.phone || '',
type: 'tel',
editable: true,
placeholder: '+34 123 456 789'
},
{
label: 'Ciudad',
value: selectedSupplier.city || '',
type: 'text',
editable: true,
placeholder: 'Ciudad'
},
{
label: 'País',
value: selectedSupplier.country || '',
type: 'text',
editable: true,
placeholder: 'País'
}
]
},
{
title: 'Información Comercial',
icon: Building2,
fields: [
{
label: 'Código de Proveedor',
value: selectedSupplier.supplier_code || '',
type: 'text',
highlight: true,
editable: true,
placeholder: 'Código único'
},
{
label: 'Tipo de Proveedor',
value: selectedSupplier.supplier_type || SupplierType.INGREDIENTS,
type: 'select',
editable: true,
options: supplierEnums.getSupplierTypeOptions()
},
{
label: 'Condiciones de Pago',
value: selectedSupplier.payment_terms || PaymentTerms.NET_30,
type: 'select',
editable: true,
options: supplierEnums.getPaymentTermsOptions()
},
{
label: 'Tiempo de Entrega (días)',
value: selectedSupplier.standard_lead_time || 3,
type: 'number',
editable: true,
placeholder: '3'
},
{
label: 'Pedido Mínimo',
value: selectedSupplier.minimum_order_amount || 0,
type: 'currency',
editable: true,
placeholder: '0.00'
},
{
label: 'Límite de Crédito',
value: selectedSupplier.credit_limit || 0,
type: 'currency',
editable: true,
placeholder: '0.00'
}
]
},
{
title: 'Rendimiento y Estadísticas',
icon: Euro,
fields: [
{
label: 'Moneda',
value: selectedSupplier.currency || 'EUR',
type: 'text',
editable: true,
placeholder: 'EUR'
},
{
label: 'Fecha de Creación',
value: selectedSupplier.created_at,
type: 'datetime',
highlight: true
},
{
label: 'Última Actualización',
value: selectedSupplier.updated_at,
type: 'datetime'
}
]
},
...(selectedSupplier.notes ? [{
title: 'Notas',
fields: [
{
label: 'Observaciones',
value: selectedSupplier.notes,
type: 'list',
span: 2 as const,
editable: true,
placeholder: 'Notas sobre el proveedor'
}
]
}] : [])
];
return (
<StatusModal
2025-09-09 21:39:12 +02:00
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedSupplier(null);
setModalMode('view');
setIsCreating(false);
2025-09-09 21:39:12 +02:00
}}
mode={modalMode}
onModeChange={setModalMode}
title={isCreating ? 'Nuevo Proveedor' : selectedSupplier.name || 'Proveedor'}
subtitle={isCreating ? 'Crear nuevo proveedor' : `Proveedor ${selectedSupplier.supplier_code || ''}`}
statusIndicator={isCreating ? undefined : getSupplierStatusConfig(selectedSupplier.status)}
2025-09-09 21:39:12 +02:00
size="lg"
sections={sections}
showDefaultActions={true}
onSave={async () => {
// TODO: Implement save functionality
console.log('Saving supplier:', selectedSupplier);
}}
onFieldChange={(sectionIndex, fieldIndex, value) => {
// Update the selectedSupplier state when fields change
const newSupplier = { ...selectedSupplier };
const section = sections[sectionIndex];
const field = section.fields[fieldIndex];
// Map field labels to supplier properties
const fieldMapping: { [key: string]: string } = {
'Nombre': 'name',
'Persona de Contacto': 'contact_person',
'Email': 'email',
'Teléfono': 'phone',
'Ciudad': 'city',
'País': 'country',
'Código de Proveedor': 'supplier_code',
'Tipo de Proveedor': 'supplier_type',
'Condiciones de Pago': 'payment_terms',
'Tiempo de Entrega (días)': 'standard_lead_time',
'Pedido Mínimo': 'minimum_order_amount',
'Límite de Crédito': 'credit_limit',
'Moneda': 'currency',
'Observaciones': 'notes'
};
const propertyName = fieldMapping[field.label];
if (propertyName) {
newSupplier[propertyName] = value;
setSelectedSupplier(newSupplier);
}
2025-09-09 21:39:12 +02:00
}}
/>
);
})()}
2025-09-09 21:39:12 +02:00
</div>
);
};
export default SuppliersPage;