Add frontend procurement implementation
This commit is contained in:
@@ -1,239 +1,209 @@
|
||||
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 { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader } from 'lucide-react';
|
||||
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import {
|
||||
useProcurementDashboard,
|
||||
useProcurementPlans,
|
||||
useCurrentProcurementPlan,
|
||||
useCriticalRequirements,
|
||||
useGenerateProcurementPlan,
|
||||
useUpdateProcurementPlanStatus,
|
||||
useTriggerDailyScheduler
|
||||
} from '../../../../api';
|
||||
import { useTenantStore } from '../../../../stores/tenant.store';
|
||||
|
||||
const ProcurementPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('orders');
|
||||
const [activeTab, setActiveTab] = useState('plans');
|
||||
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 [selectedPlan, setSelectedPlan] = useState<any>(null);
|
||||
|
||||
const { currentTenant } = useTenantStore();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
// Real API data hooks
|
||||
const { data: dashboardData, isLoading: isDashboardLoading } = useProcurementDashboard(tenantId);
|
||||
const { data: procurementPlans, isLoading: isPlansLoading } = useProcurementPlans({
|
||||
tenant_id: tenantId,
|
||||
limit: 50,
|
||||
offset: 0
|
||||
});
|
||||
const { data: currentPlan, isLoading: isCurrentPlanLoading } = useCurrentProcurementPlan(tenantId);
|
||||
const { data: criticalRequirements, isLoading: isCriticalLoading } = useCriticalRequirements(tenantId);
|
||||
|
||||
const generatePlanMutation = useGenerateProcurementPlan();
|
||||
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
|
||||
const triggerSchedulerMutation = useTriggerDailyScheduler();
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
if (!tenantId) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||
No hay tenant seleccionado
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Selecciona un tenant para ver los datos de procurement
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getPurchaseStatusConfig = (status: string, paymentStatus: string) => {
|
||||
|
||||
const getPlanStatusConfig = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { text: 'Pendiente', icon: Clock },
|
||||
draft: { text: 'Borrador', icon: Clock },
|
||||
pending_approval: { text: 'Pendiente Aprobación', icon: Clock },
|
||||
approved: { text: 'Aprobado', icon: CheckCircle },
|
||||
in_transit: { text: 'En Tránsito', icon: Truck },
|
||||
delivered: { text: 'Entregado', icon: CheckCircle },
|
||||
in_execution: { text: 'En Ejecución', icon: Truck },
|
||||
completed: { text: 'Completado', 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),
|
||||
color: getStatusColor(status === 'in_execution' ? 'inTransit' : status === 'pending_approval' ? 'pending' : status),
|
||||
text: config?.text || status,
|
||||
icon: Icon,
|
||||
isCritical: isOverdue,
|
||||
isHighlight: isPaymentPending
|
||||
isCritical: status === 'cancelled',
|
||||
isHighlight: status === 'pending_approval'
|
||||
};
|
||||
};
|
||||
|
||||
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());
|
||||
const filteredPlans = procurementPlans?.plans?.filter(plan => {
|
||||
const matchesSearch = plan.plan_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
plan.status.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(plan.special_requirements && plan.special_requirements.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 stats = {
|
||||
totalPlans: dashboardData?.summary?.total_plans || 0,
|
||||
activePlans: dashboardData?.summary?.active_plans || 0,
|
||||
pendingRequirements: dashboardData?.summary?.pending_requirements || 0,
|
||||
criticalRequirements: dashboardData?.summary?.critical_requirements || 0,
|
||||
totalEstimatedCost: dashboardData?.summary?.total_estimated_cost || 0,
|
||||
totalApprovedCost: dashboardData?.summary?.total_approved_cost || 0,
|
||||
};
|
||||
|
||||
const purchaseOrderStats = [
|
||||
const procurementStats = [
|
||||
{
|
||||
title: 'Total Órdenes',
|
||||
value: mockPurchaseStats.totalOrders,
|
||||
title: 'Planes Totales',
|
||||
value: stats.totalPlans,
|
||||
variant: 'default' as const,
|
||||
icon: ShoppingCart,
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
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,
|
||||
title: 'Planes Activos',
|
||||
value: stats.activePlans,
|
||||
variant: 'success' as const,
|
||||
icon: CheckCircle,
|
||||
},
|
||||
{
|
||||
title: 'Gasto Total',
|
||||
value: formatters.currency(mockPurchaseStats.totalSpent),
|
||||
variant: 'success' as const,
|
||||
title: 'Requerimientos Pendientes',
|
||||
value: stats.pendingRequirements,
|
||||
variant: 'warning' as const,
|
||||
icon: Clock,
|
||||
},
|
||||
{
|
||||
title: 'Críticos',
|
||||
value: stats.criticalRequirements,
|
||||
variant: 'warning' as const,
|
||||
icon: AlertCircle,
|
||||
},
|
||||
{
|
||||
title: 'Costo Estimado',
|
||||
value: formatters.currency(stats.totalEstimatedCost),
|
||||
variant: 'info' as const,
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
title: 'Proveedores',
|
||||
value: mockPurchaseStats.activeSuppliers,
|
||||
variant: 'info' as const,
|
||||
icon: Package,
|
||||
title: 'Costo Aprobado',
|
||||
value: formatters.currency(stats.totalApprovedCost),
|
||||
variant: 'success' as const,
|
||||
icon: DollarSign,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Compras"
|
||||
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
|
||||
title="Planificación de Compras"
|
||||
description="Administra planes de compras, requerimientos y análisis de procurement"
|
||||
actions={[
|
||||
{
|
||||
id: "export",
|
||||
label: "Exportar",
|
||||
variant: "outline" as const,
|
||||
icon: Download,
|
||||
onClick: () => console.log('Export purchase orders')
|
||||
onClick: () => console.log('Export procurement data')
|
||||
},
|
||||
{
|
||||
id: "new",
|
||||
label: "Nueva Orden de Compra",
|
||||
id: "generate",
|
||||
label: "Generar Plan",
|
||||
variant: "primary" as const,
|
||||
icon: Plus,
|
||||
onClick: () => console.log('New purchase order')
|
||||
onClick: () => generatePlanMutation.mutate({
|
||||
tenantId,
|
||||
request: {
|
||||
force_regenerate: false,
|
||||
planning_horizon_days: 14,
|
||||
include_safety_stock: true,
|
||||
safety_stock_percentage: 20
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "trigger",
|
||||
label: "Ejecutar Programador",
|
||||
variant: "outline" as const,
|
||||
icon: Calendar,
|
||||
onClick: () => triggerSchedulerMutation.mutate(tenantId)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<StatsGrid
|
||||
stats={purchaseOrderStats}
|
||||
columns={3}
|
||||
/>
|
||||
{isDashboardLoading ? (
|
||||
<div className="flex justify-center items-center h-32">
|
||||
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
||||
</div>
|
||||
) : (
|
||||
<StatsGrid
|
||||
stats={procurementStats}
|
||||
columns={3}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-[var(--border-primary)]">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('orders')}
|
||||
onClick={() => setActiveTab('plans')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'orders'
|
||||
activeTab === 'plans'
|
||||
? '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
|
||||
Planes de Compra
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('suppliers')}
|
||||
onClick={() => setActiveTab('requirements')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'suppliers'
|
||||
activeTab === 'requirements'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Proveedores
|
||||
Requerimientos Críticos
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
@@ -248,12 +218,12 @@ const ProcurementPage: React.FC = () => {
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{activeTab === 'orders' && (
|
||||
{activeTab === 'plans' && (
|
||||
<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..."
|
||||
placeholder="Buscar planes por número, estado o notas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full"
|
||||
@@ -267,148 +237,151 @@ const ProcurementPage: React.FC = () => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Purchase Orders Grid */}
|
||||
{activeTab === 'orders' && (
|
||||
{/* Procurement Plans Grid */}
|
||||
{activeTab === 'plans' && (
|
||||
<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);
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isPlansLoading ? (
|
||||
<div className="col-span-full flex justify-center items-center h-32">
|
||||
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
||||
</div>
|
||||
) : (
|
||||
filteredPlans.map((plan) => {
|
||||
const statusConfig = getPlanStatusConfig(plan.status);
|
||||
|
||||
return (
|
||||
<StatusCard
|
||||
key={plan.id}
|
||||
id={plan.plan_number}
|
||||
statusIndicator={statusConfig}
|
||||
title={`Plan ${plan.plan_number}`}
|
||||
subtitle={new Date(plan.plan_date).toLocaleDateString('es-ES')}
|
||||
primaryValue={formatters.currency(plan.total_estimated_cost)}
|
||||
primaryValueLabel={`${plan.total_requirements} requerimientos`}
|
||||
secondaryInfo={{
|
||||
label: 'Período',
|
||||
value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}`
|
||||
}}
|
||||
metadata={[
|
||||
`${plan.planning_horizon_days} días de horizonte`,
|
||||
`Estrategia: ${plan.procurement_strategy}`,
|
||||
...(plan.special_requirements ? [`"${plan.special_requirements}"`] : [])
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Ver',
|
||||
icon: Eye,
|
||||
variant: 'outline',
|
||||
onClick: () => {
|
||||
setSelectedPlan(plan);
|
||||
setModalMode('view');
|
||||
setShowForm(true);
|
||||
}
|
||||
},
|
||||
...(plan.status === 'pending_approval' ? [{
|
||||
label: 'Aprobar',
|
||||
icon: CheckCircle,
|
||||
variant: 'outline' as const,
|
||||
onClick: () => {
|
||||
updatePlanStatusMutation.mutate({
|
||||
tenant_id: tenantId,
|
||||
plan_id: plan.id,
|
||||
status: 'approved'
|
||||
});
|
||||
}
|
||||
}] : [])
|
||||
]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State for Purchase Orders */}
|
||||
{activeTab === 'orders' && filteredOrders.length === 0 && (
|
||||
{/* Empty State for Procurement Plans */}
|
||||
{activeTab === 'plans' && !isPlansLoading && filteredPlans.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<ShoppingCart className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
|
||||
<Package 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
|
||||
No se encontraron planes de compra
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Intenta ajustar la búsqueda o crear una nueva orden de compra
|
||||
Intenta ajustar la búsqueda o generar un nuevo plan de compra
|
||||
</p>
|
||||
<Button onClick={() => console.log('New purchase order')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Orden de Compra
|
||||
<Button
|
||||
onClick={() => generatePlanMutation.mutate({
|
||||
tenantId,
|
||||
request: {
|
||||
force_regenerate: false,
|
||||
planning_horizon_days: 14,
|
||||
include_safety_stock: true,
|
||||
safety_stock_percentage: 20
|
||||
}
|
||||
})}
|
||||
disabled={generatePlanMutation.isPending}
|
||||
>
|
||||
{generatePlanMutation.isPending ? (
|
||||
<Loader className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Generar Plan 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>
|
||||
))}
|
||||
{activeTab === 'requirements' && (
|
||||
<div className="space-y-4">
|
||||
{isCriticalLoading ? (
|
||||
<div className="flex justify-center items-center h-32">
|
||||
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
||||
</div>
|
||||
) : criticalRequirements && criticalRequirements.length > 0 ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{criticalRequirements.map((requirement) => (
|
||||
<StatusCard
|
||||
key={requirement.id}
|
||||
id={requirement.requirement_number}
|
||||
statusIndicator={{
|
||||
color: getStatusColor('danger'),
|
||||
text: 'Crítico',
|
||||
icon: AlertCircle,
|
||||
isCritical: true
|
||||
}}
|
||||
title={requirement.product_name}
|
||||
subtitle={requirement.requirement_number}
|
||||
primaryValue={`${requirement.required_quantity} ${requirement.unit_of_measure}`}
|
||||
primaryValueLabel="Cantidad requerida"
|
||||
secondaryInfo={{
|
||||
label: 'Fecha límite',
|
||||
value: new Date(requirement.required_by_date).toLocaleDateString('es-ES')
|
||||
}}
|
||||
metadata={[
|
||||
`Stock actual: ${requirement.current_stock_level} ${requirement.unit_of_measure}`,
|
||||
`Proveedor: ${requirement.supplier_name || 'No asignado'}`,
|
||||
`Costo estimado: ${formatters.currency(requirement.estimated_total_cost || 0)}`
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Ver Detalles',
|
||||
icon: Eye,
|
||||
variant: 'outline',
|
||||
onClick: () => console.log('View requirement details')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<CheckCircle className="mx-auto h-12 w-12 text-green-500 mb-4" />
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||
No hay requerimientos críticos
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Todos los requerimientos están bajo control
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -416,100 +389,139 @@ const ProcurementPage: React.FC = () => {
|
||||
<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>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Costos de Procurement</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Costo Estimado Total</span>
|
||||
<span className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{formatters.currency(stats.totalEstimatedCost)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Costo Aprobado</span>
|
||||
<span className="text-lg font-semibold text-green-600">
|
||||
{formatters.currency(stats.totalApprovedCost)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Varianza</span>
|
||||
<span className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{formatters.currency(stats.totalEstimatedCost - stats.totalApprovedCost)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Top Proveedores</h3>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Alertas Críticas</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>
|
||||
{dashboardData?.low_stock_alerts?.slice(0, 5).map((alert: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-red-50 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<AlertCircle className="w-4 h-4 text-red-500 mr-2" />
|
||||
<span className="text-sm text-[var(--text-primary)]">{alert.product_name || `Alerta ${index + 1}`}</span>
|
||||
</div>
|
||||
))}
|
||||
<span className="text-xs text-red-600 font-medium">
|
||||
Stock Bajo
|
||||
</span>
|
||||
</div>
|
||||
)) || (
|
||||
<div className="text-center py-8">
|
||||
<CheckCircle className="mx-auto h-8 w-8 text-green-500 mb-2" />
|
||||
<p className="text-sm text-[var(--text-secondary)]">No hay alertas críticas</p>
|
||||
</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>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Resumen de Performance</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.totalPlans}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">Planes Totales</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-green-600">{stats.activePlans}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">Planes Activos</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-yellow-600">{stats.pendingRequirements}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">Pendientes</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-red-600">{stats.criticalRequirements}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">Críticos</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Purchase Order Modal */}
|
||||
{showForm && selectedOrder && (
|
||||
{/* Procurement Plan Modal */}
|
||||
{showForm && selectedPlan && (
|
||||
<StatusModal
|
||||
isOpen={showForm}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedOrder(null);
|
||||
setSelectedPlan(null);
|
||||
setModalMode('view');
|
||||
}}
|
||||
mode={modalMode}
|
||||
onModeChange={setModalMode}
|
||||
title={selectedOrder.supplier}
|
||||
subtitle={`Orden de Compra ${selectedOrder.id}`}
|
||||
statusIndicator={getPurchaseStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)}
|
||||
title={`Plan de Compra ${selectedPlan.plan_number}`}
|
||||
subtitle={new Date(selectedPlan.plan_date).toLocaleDateString('es-ES')}
|
||||
statusIndicator={getPlanStatusConfig(selectedPlan.status)}
|
||||
size="lg"
|
||||
sections={[
|
||||
{
|
||||
title: 'Información del Proveedor',
|
||||
title: 'Información del Plan',
|
||||
icon: Package,
|
||||
fields: [
|
||||
{
|
||||
label: 'Proveedor',
|
||||
value: selectedOrder.supplier,
|
||||
highlight: true,
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: 'Nombre del proveedor'
|
||||
label: 'Número de Plan',
|
||||
value: selectedPlan.plan_number,
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'ID de Orden',
|
||||
value: selectedOrder.id
|
||||
label: 'Tipo de Plan',
|
||||
value: selectedPlan.plan_type
|
||||
},
|
||||
{
|
||||
label: 'Estado de Pago',
|
||||
value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : selectedOrder.paymentStatus === 'pending' ? 'Pendiente' : 'Vencido',
|
||||
label: 'Estrategia',
|
||||
value: selectedPlan.procurement_strategy
|
||||
},
|
||||
{
|
||||
label: 'Prioridad',
|
||||
value: selectedPlan.priority,
|
||||
type: 'status'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Fechas Importantes',
|
||||
title: 'Fechas y Períodos',
|
||||
icon: Calendar,
|
||||
fields: [
|
||||
{
|
||||
label: 'Fecha de pedido',
|
||||
value: selectedOrder.orderDate,
|
||||
type: 'date',
|
||||
editable: true
|
||||
label: 'Fecha del Plan',
|
||||
value: selectedPlan.plan_date,
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
label: 'Fecha de entrega',
|
||||
value: selectedOrder.deliveryDate,
|
||||
label: 'Período de Inicio',
|
||||
value: selectedPlan.plan_period_start,
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
label: 'Período de Fin',
|
||||
value: selectedPlan.plan_period_end,
|
||||
type: 'date',
|
||||
highlight: true,
|
||||
editable: true,
|
||||
required: true
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'Horizonte de Planificación',
|
||||
value: `${selectedPlan.planning_horizon_days} días`
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -518,47 +530,60 @@ const ProcurementPage: React.FC = () => {
|
||||
icon: DollarSign,
|
||||
fields: [
|
||||
{
|
||||
label: 'Importe total',
|
||||
value: selectedOrder.totalAmount,
|
||||
label: 'Costo Estimado Total',
|
||||
value: selectedPlan.total_estimated_cost,
|
||||
type: 'currency',
|
||||
highlight: true,
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: '0.00'
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'Número de artículos',
|
||||
value: `${selectedOrder.items?.length} productos`
|
||||
label: 'Costo Aprobado Total',
|
||||
value: selectedPlan.total_approved_cost,
|
||||
type: 'currency'
|
||||
},
|
||||
{
|
||||
label: 'Varianza de Costo',
|
||||
value: selectedPlan.cost_variance,
|
||||
type: 'currency'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Artículos Pedidos',
|
||||
title: 'Estadísticas',
|
||||
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
|
||||
label: 'Total de Requerimientos',
|
||||
value: `${selectedPlan.total_requirements} requerimientos`
|
||||
},
|
||||
{
|
||||
label: 'Demanda Total (Cantidad)',
|
||||
value: `${selectedPlan.total_demand_quantity} unidades`
|
||||
},
|
||||
{
|
||||
label: 'Proveedores Primarios',
|
||||
value: `${selectedPlan.primary_suppliers_count} proveedores`
|
||||
},
|
||||
{
|
||||
label: 'Proveedores de Respaldo',
|
||||
value: `${selectedPlan.backup_suppliers_count} proveedores`
|
||||
}
|
||||
]
|
||||
},
|
||||
...(selectedOrder.notes ? [{
|
||||
title: 'Notas Adicionales',
|
||||
...(selectedPlan.special_requirements ? [{
|
||||
title: 'Requerimientos Especiales',
|
||||
fields: [
|
||||
{
|
||||
label: 'Observaciones',
|
||||
value: selectedOrder.notes,
|
||||
value: selectedPlan.special_requirements,
|
||||
span: 2 as const,
|
||||
editable: true,
|
||||
placeholder: 'Añadir notas sobre la orden de compra...'
|
||||
placeholder: 'Añadir requerimientos especiales para el plan...'
|
||||
}
|
||||
]
|
||||
}] : [])
|
||||
]}
|
||||
onEdit={() => {
|
||||
console.log('Editing purchase order:', selectedOrder.id);
|
||||
console.log('Editing procurement plan:', selectedPlan.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user