import type { FC } from 'react'; import { useCallback, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Plus, Search, RefreshCw, Eye, Pencil, Send, PackageCheck, CreditCard, Ban, Download, Truck } from 'lucide-react'; import Layout from '../components/Layout'; import { Card, Button, Input, Modal, PageHeader, EmptyState, toast } from '../components/ui'; import { odooApi, type Compra, type CompraResumen } from '../services/odoo'; import { useAuth } from '../lib/auth'; import { exportToExcel } from '../lib/exporter'; import { ESTADO_META, fmtMoney } from '../components/compras/shared'; import CompraFormModal from '../components/compras/CompraFormModal'; import CompraDetalleModal from '../components/compras/CompraDetalleModal'; import RecepcionModal from '../components/compras/RecepcionModal'; import PagoCompraModal from '../components/compras/PagoCompraModal'; import CuentasPorPagarPanel from '../components/compras/CuentasPorPagarPanel'; import ProveedoresPanel from '../components/compras/ProveedoresPanel'; const TABS = [ { key: 'ordenes', label: 'Órdenes de compra' }, { key: 'cuentas', label: 'Cuentas por pagar' }, { key: 'proveedores', label: 'Proveedores' }, ] as const; const CHIPS = [ { key: '', label: 'Todas' }, { key: 'borrador', label: 'Borradores' }, { key: 'enviada', label: 'Enviadas' }, { key: 'por_recibir', label: 'Por recibir' }, { key: 'por_pagar', label: 'Cuentas por pagar' }, ]; const Compras: FC = () => { const { can, brand } = useAuth(); const [searchParams, setSearchParams] = useSearchParams(); const tab = (searchParams.get('tab') || 'ordenes') as typeof TABS[number]['key']; const setTab = (t: string) => setSearchParams({ tab: t }); const [rows, setRows] = useState([]); const [resumen, setResumen] = useState(null); const [loading, setLoading] = useState(true); const [estado, setEstado] = useState(''); const [q, setQ] = useState(''); const [formOpen, setFormOpen] = useState(false); const [formCompra, setFormCompra] = useState(null); const [detalle, setDetalle] = useState(null); const [detalleOpen, setDetalleOpen] = useState(false); const [recepcion, setRecepcion] = useState(null); const [pago, setPago] = useState(null); const [cancelar, setCancelar] = useState(null); const [cancelMotivo, setCancelMotivo] = useState(''); const [busy, setBusy] = useState(false); const load = useCallback(async () => { if (tab !== 'ordenes') return; setLoading(true); try { const res = await odooApi.getCompras({ ...(estado ? { estado } : {}), ...(q ? { q } : {}), page_size: 100, }); setRows(res.compras || []); setResumen(res.resumen || null); } catch (err) { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; toast.error(msg || 'No se pudieron cargar las compras'); } finally { setLoading(false); } }, [tab, estado, q]); useEffect(() => { load(); }, [load]); const abrirDetalle = async (c: Compra) => { try { const res = await odooApi.getCompra(c.id); if (res.status === 'success') { setDetalle(res.compra); setDetalleOpen(true); } } catch (err) { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; toast.error(msg || 'No se pudo abrir la compra'); } }; const enviar = async (c: Compra) => { try { await odooApi.enviarCompra(c.id); toast.success('Compra enviada'); load(); if (detalle?.id === c.id) setDetalleOpen(false); } catch (err) { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; toast.error(msg || 'No se pudo enviar'); } }; const confirmarCancelar = async () => { if (!cancelar) return; setBusy(true); try { await odooApi.cancelarCompra(cancelar.id, cancelMotivo); toast.success('Compra cancelada'); setCancelar(null); setCancelMotivo(''); load(); if (detalle?.id === cancelar.id) setDetalleOpen(false); } catch (err) { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; toast.error(msg || 'No se pudo cancelar'); } finally { setBusy(false); } }; const exportar = async () => { try { await exportToExcel({ filename: `compras-${new Date().toISOString().slice(0, 10)}`, sheetName: 'Compras', title: `Compras · ${brand.nombre}`, subtitle: `Filtro: ${CHIPS.find((c) => c.key === estado)?.label || 'Todas'}`, columns: [ { header: 'Folio', key: 'folio' }, { header: 'Proveedor', key: 'proveedor' }, { header: 'Fecha', key: 'fecha', format: 'date' }, { header: 'Estado', key: 'estado_label' }, { header: 'Moneda', key: 'moneda' }, { header: 'Total', key: 'total', format: 'currency' }, { header: 'Pagado', key: 'pagado', format: 'currency' }, { header: 'Saldo', key: 'saldo', format: 'currency' }, ], rows: rows as unknown as Record[], totals: { folio: '', proveedor: 'Totales', fecha: '', estado_label: '', moneda: '', total: rows.reduce((s, r) => s + r.total, 0), pagado: rows.reduce((s, r) => s + r.pagado, 0), saldo: rows.reduce((s, r) => s + r.saldo, 0), }, }); toast.success('Excel descargado'); } catch { toast.error('No se pudo exportar'); } }; return ( {tab === 'ordenes' && can('compras.crear') && ( )} {tab === 'ordenes' && can('reportes.exportar') && ( )}
{TABS.map((t) => ( ))}
{tab === 'cuentas' ? ( ) : tab === 'proveedores' ? ( ) : (
{resumen && (

Gasto del mes

{fmtMoney(resumen.gasto_mes)}

{resumen.compras_mes} compra(s)

Abiertas (por recibir)

{resumen.abiertas}

Compras con saldo

{resumen.por_pagar}

Saldo por pagar

{fmtMoney(resumen.saldo_total)}

)}
setQ(e.target.value)} />
{CHIPS.map((c) => ( ))}
{loading ? (
Cargando compras…
) : rows.length === 0 ? (
} />
) : (
{rows.map((c) => { const meta = ESTADO_META[c.estado]; return ( ); })}
Folio Proveedor Fecha Total Pagado Saldo Estado Acciones
{c.folio} {c.proveedor} {c.fecha} {fmtMoney(c.total, c.moneda)} {fmtMoney(c.pagado, c.moneda)} {fmtMoney(c.saldo, c.moneda)} {meta.label}
{can('compras.editar') && ['borrador', 'enviada'].includes(c.estado) && ( )} {can('compras.crear') && c.estado === 'borrador' && ( )} {can('compras.recibir') && c.puede_recibir && ( )} {can('compras.pagar') && c.estado !== 'cancelada' && c.saldo > 0 && ( )} {can('compras.cancelar') && c.puede_cancelar && ( )}
)}
)} setFormOpen(false)} compra={formCompra} onDone={() => { load(); if (detalleOpen) setDetalleOpen(false); }} /> setDetalleOpen(false)} compra={detalle} puedeEditar={can('compras.editar')} puedeRecibir={can('compras.recibir')} puedePagar={can('compras.pagar')} puedeCancelar={can('compras.cancelar')} puedeCrear={can('compras.crear')} onEdit={(c) => { setDetalleOpen(false); setFormCompra(c); setFormOpen(true); }} onEnviar={(c) => enviar(c)} onRecibir={(c) => setRecepcion(c)} onPagar={(c) => setPago(c)} onCancelar={(c) => { setCancelar(c); setCancelMotivo(''); }} /> setRecepcion(null)} compra={recepcion} onDone={() => { load(); }} /> setPago(null)} compraId={pago?.id ?? null} folio={pago?.folio || ''} saldo={pago?.saldo || 0} moneda={pago?.moneda || 'MXN'} onDone={() => load()} /> setCancelar(null)} title={`Cancelar compra ${cancelar?.folio || ''}`} footer={(
)} >

Solo se pueden cancelar compras sin recepciones. Si la mercancía ya entró, registra una baja o un ajuste de inventario.

setCancelMotivo(e.target.value)} />
); }; export default Compras;