Files
bakery-ia/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx
2025-10-31 18:57:58 +01:00

964 lines
32 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { Plus, ShoppingCart, Euro, Calendar, CheckCircle, AlertCircle, Package, Eye, X, Send, Building2, Play, FileText, Star, TrendingUp, TrendingDown, Minus } from 'lucide-react';
import { Button, Card, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, Input, type FilterConfig, EmptyState } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { CreatePurchaseOrderModal } from '../../../../components/domain/procurement/CreatePurchaseOrderModal';
import {
usePurchaseOrders,
usePurchaseOrder,
useApprovePurchaseOrder,
useRejectPurchaseOrder,
useUpdatePurchaseOrder
} from '../../../../api/hooks/purchase-orders';
import { useTriggerDailyScheduler } from '../../../../api';
import type { PurchaseOrderStatus, PurchaseOrderPriority, PurchaseOrderDetail } from '../../../../api/services/purchase_orders';
import { useTenantStore } from '../../../../stores/tenant.store';
import { useUserById } from '../../../../api/hooks/user';
import { showToast } from '../../../../utils/toast';
const ProcurementPage: React.FC = () => {
// State
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<PurchaseOrderStatus | ''>('');
const [priorityFilter, setPriorityFilter] = useState<PurchaseOrderPriority | ''>('');
const [showArchived, setShowArchived] = useState(false);
const [showCreatePOModal, setShowCreatePOModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [selectedPOId, setSelectedPOId] = useState<string | null>(null);
const [showApprovalModal, setShowApprovalModal] = useState(false);
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>('approve');
const [approvalNotes, setApprovalNotes] = useState('');
const { currentTenant } = useTenantStore();
const tenantId = currentTenant?.id || '';
// API Hooks
const { data: purchaseOrdersData = [], isLoading: isPOsLoading, refetch: refetchPOs } = usePurchaseOrders(
tenantId,
{
status: statusFilter || undefined,
priority: priorityFilter || undefined,
search_term: searchTerm || undefined,
limit: 100,
offset: 0
},
{
enabled: !!tenantId
}
);
const { data: poDetails, isLoading: isLoadingDetails } = usePurchaseOrder(
tenantId,
selectedPOId || '',
{
enabled: !!tenantId && !!selectedPOId && showDetailsModal
}
);
const approvePOMutation = useApprovePurchaseOrder();
const rejectPOMutation = useRejectPurchaseOrder();
const updatePOMutation = useUpdatePurchaseOrder();
// Filter POs
const filteredPOs = useMemo(() => {
return purchaseOrdersData.filter(po => {
// Hide archived POs by default
if (!showArchived && (po.status === 'COMPLETED' || po.status === 'CANCELLED')) {
return false;
}
return true;
});
}, [purchaseOrdersData, showArchived]);
// Calculate stats
const poStats = useMemo(() => {
const total = filteredPOs.length;
// API returns lowercase status values (e.g., 'pending_approval' not 'PENDING_APPROVAL')
const pendingApproval = filteredPOs.filter(po => po.status === 'pending_approval').length;
const approved = filteredPOs.filter(po => po.status === 'approved').length;
const inProgress = filteredPOs.filter(po =>
['sent_to_supplier', 'confirmed'].includes(po.status)
).length;
const received = filteredPOs.filter(po => po.status === 'received').length;
const totalAmount = filteredPOs.reduce((sum, po) => {
// API returns total_amount as a string, so parse it
const amount = typeof po.total_amount === 'string'
? parseFloat(po.total_amount) || 0
: typeof po.total_amount === 'number'
? po.total_amount
: 0;
return sum + amount;
}, 0);
return {
total,
pendingApproval,
approved,
inProgress,
received,
totalAmount
};
}, [filteredPOs]);
// Handlers
const handleViewDetails = (po: any) => {
setSelectedPOId(po.id);
setShowDetailsModal(true);
};
const handleApprovePO = (po: any) => {
setSelectedPOId(po.id);
setApprovalAction('approve');
setApprovalNotes('');
setShowApprovalModal(true);
};
const handleRejectPO = (po: any) => {
setSelectedPOId(po.id);
setApprovalAction('reject');
setApprovalNotes('');
setShowApprovalModal(true);
};
const handleSendToSupplier = async (po: any) => {
try {
await updatePOMutation.mutateAsync({
tenantId,
poId: po.id,
data: { status: 'SENT_TO_SUPPLIER' }
});
showToast.success('Orden enviada al proveedor');
refetchPOs();
} catch (error) {
console.error('Error sending PO to supplier:', error);
showToast.error('Error al enviar orden al proveedor');
}
};
const handleConfirmPO = async (po: any) => {
try {
await updatePOMutation.mutateAsync({
tenantId,
poId: po.id,
data: { status: 'CONFIRMED' }
});
showToast.success('Orden confirmada');
refetchPOs();
} catch (error) {
console.error('Error confirming PO:', error);
showToast.error('Error al confirmar orden');
}
};
const handleApprovalSubmit = async () => {
if (!selectedPOId) return;
try {
if (approvalAction === 'approve') {
await approvePOMutation.mutateAsync({
tenantId,
poId: selectedPOId,
notes: approvalNotes || undefined
});
showToast.success('Orden aprobada exitosamente');
} else {
if (!approvalNotes.trim()) {
showToast.error('Debes proporcionar una razón para rechazar');
return;
}
await rejectPOMutation.mutateAsync({
tenantId,
poId: selectedPOId,
reason: approvalNotes
});
showToast.success('Orden rechazada');
}
setShowApprovalModal(false);
setShowDetailsModal(false);
setApprovalNotes('');
refetchPOs();
} catch (error) {
console.error('Error in approval action:', error);
showToast.error('Error al procesar aprobación');
}
};
const handleTriggerScheduler = async () => {
try {
await triggerSchedulerMutation.mutateAsync(tenantId);
showToast.success('Scheduler ejecutado exitosamente');
refetchPOs();
} catch (error) {
console.error('Error triggering scheduler:', error);
showToast.error('Error al ejecutar scheduler');
}
};
// Get PO status configuration
const getPOStatusConfig = (status: PurchaseOrderStatus | string) => {
// API returns lowercase status, normalize it
const normalizedStatus = status?.toUpperCase().replace(/_/g, '_') as PurchaseOrderStatus;
const configs: Record<string, any> = {
DRAFT: {
color: getStatusColor('neutral'),
text: 'Borrador',
icon: FileText,
isCritical: false
},
PENDING_APPROVAL: {
color: getStatusColor('warning'),
text: 'Pendiente de Aprobación',
icon: AlertCircle,
isCritical: true
},
APPROVED: {
color: getStatusColor('approved'),
text: 'Aprobado',
icon: CheckCircle,
isCritical: false
},
SENT_TO_SUPPLIER: {
color: getStatusColor('inProgress'),
text: 'Enviado al Proveedor',
icon: Send,
isCritical: false
},
CONFIRMED: {
color: getStatusColor('approved'),
text: 'Confirmado',
icon: CheckCircle,
isCritical: false
},
RECEIVED: {
color: getStatusColor('delivered'),
text: 'Recibido',
icon: Package,
isCritical: false
},
COMPLETED: {
color: getStatusColor('completed'),
text: 'Completado',
icon: CheckCircle,
isCritical: false
},
CANCELLED: {
color: getStatusColor('cancelled'),
text: 'Cancelado',
icon: X,
isCritical: false
},
DISPUTED: {
color: getStatusColor('expired'),
text: 'En Disputa',
icon: AlertCircle,
isCritical: true
}
};
// Return config or default if status is undefined/invalid
return configs[normalizedStatus] || {
color: getStatusColor('neutral'),
text: status || 'Desconocido',
icon: FileText,
isCritical: false
};
};
// Get quick actions for a PO based on status
const getPOQuickActions = (po: any) => {
const actions = [
{
label: 'Ver Detalles',
icon: Eye,
onClick: () => handleViewDetails(po),
priority: 'primary' as const
}
];
if (po.status === 'pending_approval') {
actions.push(
{
label: 'Aprobar',
icon: CheckCircle,
onClick: () => handleApprovePO(po),
priority: 'primary' as const,
variant: 'primary' as const
},
{
label: 'Rechazar',
icon: X,
onClick: () => handleRejectPO(po),
priority: 'secondary' as const,
variant: 'outline' as const,
destructive: true
}
);
} else if (po.status === 'approved') {
actions.push({
label: 'Enviar al Proveedor',
icon: Send,
onClick: () => handleSendToSupplier(po),
priority: 'primary' as const,
variant: 'primary' as const
});
} else if (po.status === 'sent_to_supplier') {
actions.push({
label: 'Confirmar',
icon: CheckCircle,
onClick: () => handleConfirmPO(po),
priority: 'primary' as const,
variant: 'primary' as const
});
}
return actions;
};
// Component to display user name with data fetching
const UserName: React.FC<{ userId: string | undefined | null }> = ({ userId }) => {
// Handle null/undefined
if (!userId) return <>N/A</>;
// Check for system/demo UUID patterns
if (userId === '00000000-0000-0000-0000-000000000001' || userId === '00000000-0000-0000-0000-000000000000') {
return <>Sistema</>;
}
// Fetch user data
const { data: user, isLoading } = useUserById(userId, {
retry: 1,
staleTime: 10 * 60 * 1000, // 10 minutes
});
if (isLoading) return <>Cargando...</>;
if (!user) return <>Usuario Desconocido</>;
return <>{user.full_name || user.email || 'Usuario'}</>;
};
// Build details sections for EditViewModal
const buildPODetailsSections = (po: PurchaseOrderDetail) => {
const sections = [
{
title: 'Información General',
icon: FileText,
fields: [
{
label: 'Número de Orden',
value: po.po_number,
type: 'text' as const
},
{
label: 'Estado',
value: getPOStatusConfig(po.status).text,
type: 'badge' as const,
badgeColor: getPOStatusConfig(po.status).color
},
{
label: 'Prioridad',
value: po.priority === 'urgent' ? 'Urgente' : po.priority === 'high' ? 'Alta' : po.priority === 'low' ? 'Baja' : 'Normal',
type: 'text' as const
},
{
label: 'Fecha de Creación',
value: new Date(po.created_at).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}),
type: 'text' as const
}
]
},
{
title: 'Información del Proveedor',
icon: Building2,
fields: [
{
label: 'Proveedor',
value: po.supplier?.name || 'N/A',
type: 'text' as const
},
{
label: 'Código de Proveedor',
value: po.supplier?.supplier_code || 'N/A',
type: 'text' as const
},
{
label: 'Email',
value: po.supplier?.email || 'N/A',
type: 'text' as const
},
{
label: 'Teléfono',
value: po.supplier?.phone || 'N/A',
type: 'text' as const
}
]
},
{
title: 'Resumen Financiero',
icon: Euro,
fields: [
{
label: 'Subtotal',
value: `${(() => {
const val = typeof po.subtotal === 'string' ? parseFloat(po.subtotal) : typeof po.subtotal === 'number' ? po.subtotal : 0;
return val.toFixed(2);
})()}`,
type: 'text' as const
},
{
label: 'Impuestos',
value: `${(() => {
const val = typeof po.tax_amount === 'string' ? parseFloat(po.tax_amount) : typeof po.tax_amount === 'number' ? po.tax_amount : 0;
return val.toFixed(2);
})()}`,
type: 'text' as const
},
{
label: 'Descuentos',
value: `${(() => {
const val = typeof po.discount_amount === 'string' ? parseFloat(po.discount_amount) : typeof po.discount_amount === 'number' ? po.discount_amount : 0;
return val.toFixed(2);
})()}`,
type: 'text' as const
},
{
label: 'TOTAL',
value: `${(() => {
const val = typeof po.total_amount === 'string' ? parseFloat(po.total_amount) : typeof po.total_amount === 'number' ? po.total_amount : 0;
return val.toFixed(2);
})()}`,
type: 'text' as const,
valueClassName: 'text-xl font-bold text-primary-600'
}
]
},
{
title: 'Artículos del Pedido',
icon: Package,
fields: [
{
label: '',
value: <PurchaseOrderItemsTable items={po.items || []} />,
type: 'component' as const,
span: 2
}
]
},
{
title: 'Entrega',
icon: Calendar,
fields: [
{
label: 'Fecha de Entrega Requerida',
value: po.required_delivery_date
? new Date(po.required_delivery_date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })
: 'No especificada',
type: 'text' as const
},
{
label: 'Fecha de Entrega Esperada',
value: po.expected_delivery_date
? new Date(po.expected_delivery_date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })
: 'No especificada',
type: 'text' as const
},
{
label: 'Fecha de Entrega Real',
value: po.actual_delivery_date
? new Date(po.actual_delivery_date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })
: 'Pendiente',
type: 'text' as const
}
]
},
{
title: 'Aprobación',
icon: CheckCircle,
fields: [
{
label: 'Aprobado Por',
value: <UserName userId={po.approved_by} />,
type: 'component' as const
},
{
label: 'Fecha de Aprobación',
value: po.approved_at
? new Date(po.approved_at).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })
: 'N/A',
type: 'text' as const
},
{
label: 'Notas de Aprobación',
value: po.approval_notes || 'N/A',
type: 'textarea' as const
}
]
},
{
title: 'Notas',
icon: FileText,
fields: [
{
label: 'Notas de la Orden',
value: po.notes || 'Sin notas',
type: 'textarea' as const
},
{
label: 'Notas Internas',
value: po.internal_notes || 'Sin notas internas',
type: 'textarea' as const
}
]
},
{
title: 'Auditoría',
icon: FileText,
fields: [
{
label: 'Creado Por',
value: <UserName userId={po.created_by} />,
type: 'component' as const
},
{
label: 'Última Actualización',
value: new Date(po.updated_at).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}),
type: 'text' as const
}
]
}
];
return sections;
};
// Items cards component - Mobile-friendly redesign
const PurchaseOrderItemsTable: React.FC<{ items: any[] }> = ({ items }) => {
if (!items || items.length === 0) {
return (
<div className="text-center py-8 text-[var(--text-secondary)] border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<Package className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No hay artículos en esta orden</p>
</div>
);
}
const totalAmount = items.reduce((sum, item) => {
const price = typeof item.unit_price === 'string' ? parseFloat(item.unit_price) : typeof item.unit_price === 'number' ? item.unit_price : 0;
const quantity = (() => {
if (typeof item.ordered_quantity === 'number') {
return item.ordered_quantity;
} else if (typeof item.ordered_quantity === 'string') {
const parsed = parseFloat(item.ordered_quantity);
return isNaN(parsed) ? 0 : parsed;
} else if (typeof item.ordered_quantity === 'object' && item.ordered_quantity !== null) {
// Handle if it's a decimal object or similar
return parseFloat(item.ordered_quantity.toString()) || 0;
}
return 0;
})();
return sum + (price * quantity);
}, 0);
return (
<div className="space-y-3">
{/* Items as cards */}
{items.map((item, index) => {
const unitPrice = typeof item.unit_price === 'string' ? parseFloat(item.unit_price) : typeof item.unit_price === 'number' ? item.unit_price : 0;
const quantity = (() => {
if (typeof item.ordered_quantity === 'number') {
return item.ordered_quantity;
} else if (typeof item.ordered_quantity === 'string') {
const parsed = parseFloat(item.ordered_quantity);
return isNaN(parsed) ? 0 : parsed;
} else if (typeof item.ordered_quantity === 'object' && item.ordered_quantity !== null) {
// Handle if it's a decimal object or similar
return parseFloat(item.ordered_quantity.toString()) || 0;
}
return 0;
})();
const itemTotal = unitPrice * quantity;
const productName = item.product_name || item.ingredient_name || `Producto ${index + 1}`;
return (
<div
key={index}
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/50 space-y-3"
>
{/* Header with product name and total */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-[var(--text-primary)]">
{productName}
</span>
<div className="text-right">
<p className="text-sm font-bold text-[var(--color-primary)]">
{itemTotal.toFixed(2)}
</p>
<p className="text-xs text-[var(--text-secondary)]">Subtotal</p>
</div>
</div>
{/* Product SKU */}
{item.product_code && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
SKU
</label>
<p className="text-sm text-[var(--text-primary)]">
{item.product_code}
</p>
</div>
</div>
)}
{/* Quantity and Price details */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Cantidad
</label>
<p className="text-sm font-medium text-[var(--text-primary)]">
{quantity} {item.unit_of_measure || ''}
</p>
</div>
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Precio Unitario
</label>
<p className="text-sm font-medium text-[var(--text-primary)]">
{unitPrice.toFixed(2)}
</p>
</div>
</div>
{/* Optional quality requirements or notes */}
{(item.quality_requirements || item.notes) && (
<div className="pt-3 border-t border-[var(--border-primary)] space-y-2">
{item.quality_requirements && (
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Requisitos de Calidad
</label>
<p className="text-sm text-[var(--text-primary)]">
{item.quality_requirements}
</p>
</div>
)}
{item.notes && (
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Notas
</label>
<p className="text-sm text-[var(--text-primary)]">
{item.notes}
</p>
</div>
)}
</div>
)}
</div>
);
})}
{/* Total summary */}
{items.length > 0 && (
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
<span className="text-lg font-semibold text-[var(--text-primary)]">
Total: {totalAmount.toFixed(2)}
</span>
</div>
)}
</div>
);
};
// Filters configuration
const filterConfig: FilterConfig[] = [
{
key: 'status',
type: 'dropdown',
label: 'Estado',
value: statusFilter,
onChange: (value) => setStatusFilter(value as PurchaseOrderStatus | ''),
placeholder: 'Todos los estados',
options: [
{ value: 'DRAFT', label: 'Borrador' },
{ value: 'PENDING_APPROVAL', label: 'Pendiente de Aprobación' },
{ value: 'APPROVED', label: 'Aprobado' },
{ value: 'SENT_TO_SUPPLIER', label: 'Enviado al Proveedor' },
{ value: 'CONFIRMED', label: 'Confirmado' },
{ value: 'RECEIVED', label: 'Recibido' },
{ value: 'COMPLETED', label: 'Completado' },
{ value: 'CANCELLED', label: 'Cancelado' },
{ value: 'DISPUTED', label: 'En Disputa' }
]
},
{
key: 'priority',
type: 'dropdown',
label: 'Prioridad',
value: priorityFilter,
onChange: (value) => setPriorityFilter(value as PurchaseOrderPriority | ''),
placeholder: 'Todas las prioridades',
options: [
{ value: 'urgent', label: 'Urgente' },
{ value: 'high', label: 'Alta' },
{ value: 'normal', label: 'Normal' },
{ value: 'low', label: 'Baja' }
]
},
{
key: 'archived',
type: 'checkbox',
label: 'Mostrar Archivados',
value: showArchived,
onChange: setShowArchived
}
];
return (
<div className="space-y-6">
{/* Header */}
<PageHeader
title="Órdenes de Compra"
description="Gestiona órdenes de compra y aprovisionamiento"
actions={[
{
id: 'create-po',
label: 'Nueva Orden',
icon: Plus,
onClick: () => setShowCreatePOModal(true),
variant: 'primary',
size: 'md'
}
]}
/>
{/* Stats */}
<StatsGrid
columns={3}
stats={[
{
title: 'Total de Órdenes',
value: poStats.total,
formatValue: formatters.number,
icon: ShoppingCart,
variant: 'info'
},
{
title: 'Pendientes de Aprobación',
value: poStats.pendingApproval,
formatValue: formatters.number,
icon: AlertCircle,
variant: 'warning'
},
{
title: 'Aprobadas',
value: poStats.approved,
formatValue: formatters.number,
icon: CheckCircle,
variant: 'success'
},
{
title: 'En Proceso',
value: poStats.inProgress,
formatValue: formatters.number,
icon: Package,
variant: 'purple'
},
{
title: 'Recibidas',
value: poStats.received,
formatValue: formatters.number,
icon: CheckCircle,
variant: 'success'
},
{
title: 'Valor Total',
value: poStats.totalAmount,
formatValue: formatters.currency,
icon: Euro,
variant: 'success'
}
]}
/>
{/* Search and Filters */}
<SearchAndFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="Buscar por número de orden, proveedor..."
filters={filterConfig}
/>
{/* Purchase Orders Grid */}
{isPOsLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
) : filteredPOs.length === 0 ? (
<EmptyState
icon={ShoppingCart}
title="No hay órdenes de compra"
description="Comienza creando una nueva orden de compra"
actionLabel="Nueva Orden"
actionIcon={Plus}
onAction={() => setShowCreatePOModal(true)}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredPOs.map((po) => {
const statusConfig = getPOStatusConfig(po.status);
const priorityText = String(po.priority === 'urgent' ? 'Urgente' : po.priority === 'high' ? 'Alta' : po.priority === 'low' ? 'Baja' : 'Normal');
// Parse total_amount - API returns it as a string
const totalAmountNum = typeof po.total_amount === 'string'
? parseFloat(po.total_amount)
: typeof po.total_amount === 'number'
? po.total_amount
: 0;
const totalAmount = String(totalAmountNum.toFixed(2));
// items_count is not in the API response, so we don't show it
const deliveryDate = String(po.required_delivery_date
? new Date(po.required_delivery_date).toLocaleDateString('es-ES', { month: 'short', day: 'numeric' })
: 'Sin especificar');
return (
<StatusCard
key={po.id}
id={String(po.id)}
title={String(po.po_number || 'Sin número')}
subtitle={String(po.supplier_name || po.supplier?.name || 'Proveedor desconocido')}
statusIndicator={statusConfig}
primaryValue={`${totalAmount}`}
primaryValueLabel="Total"
metadata={[
`Prioridad: ${priorityText}`,
`Entrega: ${deliveryDate}`,
`Proveedor: ${po.supplier_name || 'Desconocido'}`
]}
actions={getPOQuickActions(po)}
onClick={() => handleViewDetails(po)}
/>
);
})}
</div>
)}
{/* Create PO Modal */}
{showCreatePOModal && (
<CreatePurchaseOrderModal
isOpen={showCreatePOModal}
onClose={() => setShowCreatePOModal(false)}
requirements={[]}
onSuccess={() => {
setShowCreatePOModal(false);
refetchPOs();
showToast.success('Orden de compra creada exitosamente');
}}
/>
)}
{/* PO Details Modal */}
{showDetailsModal && poDetails && (
<EditViewModal
isOpen={showDetailsModal}
onClose={() => {
setShowDetailsModal(false);
setSelectedPOId(null);
}}
title={`Orden de Compra: ${poDetails.po_number}`}
mode="view"
data={poDetails}
sections={buildPODetailsSections(poDetails)}
isLoading={isLoadingDetails}
actions={
poDetails.status === 'PENDING_APPROVAL' ? [
{
label: 'Aprobar',
onClick: () => {
setApprovalAction('approve');
setApprovalNotes('');
setShowApprovalModal(true);
},
variant: 'primary' as const,
icon: CheckCircle
},
{
label: 'Rechazar',
onClick: () => {
setApprovalAction('reject');
setApprovalNotes('');
setShowApprovalModal(true);
},
variant: 'outline' as const,
icon: X
}
] : undefined
}
/>
)}
{/* Approval Modal */}
{showApprovalModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="p-6">
<h3 className="text-lg font-semibold mb-4">
{approvalAction === 'approve' ? 'Aprobar Orden de Compra' : 'Rechazar Orden de Compra'}
</h3>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
{approvalAction === 'approve' ? 'Notas (opcional)' : 'Razón del rechazo (requerido)'}
</label>
<textarea
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
rows={4}
value={approvalNotes}
onChange={(e) => setApprovalNotes(e.target.value)}
placeholder={approvalAction === 'approve'
? 'Agrega notas sobre la aprobación...'
: 'Explica por qué se rechaza esta orden...'}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setShowApprovalModal(false);
setApprovalNotes('');
}}
>
Cancelar
</Button>
<Button
onClick={handleApprovalSubmit}
disabled={approvePOMutation.isPending || rejectPOMutation.isPending}
>
{approvalAction === 'approve' ? 'Aprobar' : 'Rechazar'}
</Button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default ProcurementPage;