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 toast from 'react-hot-toast'; const ProcurementPage: React.FC = () => { // State const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [priorityFilter, setPriorityFilter] = useState(''); const [showArchived, setShowArchived] = useState(false); const [showCreatePOModal, setShowCreatePOModal] = useState(false); const [showDetailsModal, setShowDetailsModal] = useState(false); const [selectedPOId, setSelectedPOId] = useState(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(); const triggerSchedulerMutation = useTriggerDailyScheduler(); // 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' } }); toast.success('Orden enviada al proveedor'); refetchPOs(); } catch (error) { console.error('Error sending PO to supplier:', error); toast.error('Error al enviar orden al proveedor'); } }; const handleConfirmPO = async (po: any) => { try { await updatePOMutation.mutateAsync({ tenantId, poId: po.id, data: { status: 'CONFIRMED' } }); toast.success('Orden confirmada'); refetchPOs(); } catch (error) { console.error('Error confirming PO:', error); toast.error('Error al confirmar orden'); } }; const handleApprovalSubmit = async () => { if (!selectedPOId) return; try { if (approvalAction === 'approve') { await approvePOMutation.mutateAsync({ tenantId, poId: selectedPOId, notes: approvalNotes || undefined }); toast.success('Orden aprobada exitosamente'); } else { if (!approvalNotes.trim()) { toast.error('Debes proporcionar una razón para rechazar'); return; } await rejectPOMutation.mutateAsync({ tenantId, poId: selectedPOId, reason: approvalNotes }); toast.success('Orden rechazada'); } setShowApprovalModal(false); setShowDetailsModal(false); setApprovalNotes(''); refetchPOs(); } catch (error) { console.error('Error in approval action:', error); toast.error('Error al procesar aprobación'); } }; const handleTriggerScheduler = async () => { try { await triggerSchedulerMutation.mutateAsync(tenantId); toast.success('Scheduler ejecutado exitosamente'); refetchPOs(); } catch (error) { console.error('Error triggering scheduler:', error); toast.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 = { 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?.contact_email || 'N/A', type: 'text' as const }, { label: 'Teléfono', value: po.supplier?.contact_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: , 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: , 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: , 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 (

No hay artículos en esta orden

); } 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 = typeof item.ordered_quantity === 'number' ? item.ordered_quantity : 0; return sum + (price * quantity); }, 0); return (
{/* 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 = typeof item.ordered_quantity === 'number' ? item.ordered_quantity : 0; const itemTotal = unitPrice * quantity; const productName = item.product_name || `Producto ${index + 1}`; return (
{/* Header with product name and total */}
{productName}

€{itemTotal.toFixed(2)}

Subtotal

{/* Product SKU */} {item.product_code && (

{item.product_code}

)} {/* Quantity and Price details */}

{quantity} {item.unit_of_measure || ''}

€{unitPrice.toFixed(2)}

{/* Optional quality requirements or notes */} {(item.quality_requirements || item.notes) && (
{item.quality_requirements && (

{item.quality_requirements}

)} {item.notes && (

{item.notes}

)}
)}
); })} {/* Total summary */} {items.length > 0 && (
Total: €{totalAmount.toFixed(2)}
)}
); }; // 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 (
{/* Header */} setShowCreatePOModal(true), variant: 'primary', size: 'md' } ]} /> {/* Stats */} {/* Search and Filters */} {/* Purchase Orders Grid */} {isPOsLoading ? (
) : filteredPOs.length === 0 ? ( setShowCreatePOModal(true)} /> ) : (
{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 ( handleViewDetails(po)} /> ); })}
)} {/* Create PO Modal */} {showCreatePOModal && ( setShowCreatePOModal(false)} requirements={[]} onSuccess={() => { setShowCreatePOModal(false); refetchPOs(); toast.success('Orden de compra creada exitosamente'); }} /> )} {/* PO Details Modal */} {showDetailsModal && poDetails && ( { 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 && (

{approvalAction === 'approve' ? 'Aprobar Orden de Compra' : 'Rechazar Orden de Compra'}