180 lines
8.9 KiB
TypeScript
180 lines
8.9 KiB
TypeScript
import type { FC } from 'react';
|
|
import { Send, PackageCheck, CreditCard, Ban, Pencil } from 'lucide-react';
|
|
import { Modal, Button, Badge } from '../ui';
|
|
import type { Compra } from '../../services/odoo';
|
|
import { ESTADO_META, fmtMoney, fmtNum } from './shared';
|
|
|
|
interface Props {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
compra: Compra | null;
|
|
puedeEditar: boolean;
|
|
puedeRecibir: boolean;
|
|
puedePagar: boolean;
|
|
puedeCancelar: boolean;
|
|
puedeCrear: boolean;
|
|
onEdit: (c: Compra) => void;
|
|
onEnviar: (c: Compra) => void;
|
|
onRecibir: (c: Compra) => void;
|
|
onPagar: (c: Compra) => void;
|
|
onCancelar: (c: Compra) => void;
|
|
}
|
|
|
|
const CompraDetalleModal: FC<Props> = ({
|
|
isOpen, onClose, compra, puedeEditar, puedeRecibir, puedePagar, puedeCancelar, puedeCrear,
|
|
onEdit, onEnviar, onRecibir, onPagar, onCancelar,
|
|
}) => {
|
|
if (!compra) return null;
|
|
const meta = ESTADO_META[compra.estado];
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title={`Compra ${compra.folio}`}
|
|
maxWidth="full"
|
|
footer={(
|
|
<div className="flex flex-wrap justify-end gap-2 w-full">
|
|
{puedeEditar && ['borrador', 'enviada'].includes(compra.estado) && (
|
|
<Button variant="outline" onClick={() => onEdit(compra)}><Pencil size={14} className="mr-1.5" />Editar</Button>
|
|
)}
|
|
{puedeCrear && compra.estado === 'borrador' && (
|
|
<Button variant="outline" onClick={() => onEnviar(compra)}><Send size={14} className="mr-1.5" />Enviar</Button>
|
|
)}
|
|
{puedeRecibir && compra.puede_recibir && (
|
|
<Button onClick={() => onRecibir(compra)}><PackageCheck size={14} className="mr-1.5" />Recibir</Button>
|
|
)}
|
|
{puedePagar && compra.estado !== 'cancelada' && compra.saldo > 0 && (
|
|
<Button onClick={() => onPagar(compra)}><CreditCard size={14} className="mr-1.5" />Pagar</Button>
|
|
)}
|
|
{puedeCancelar && compra.puede_cancelar && (
|
|
<Button variant="danger" onClick={() => onCancelar(compra)}><Ban size={14} className="mr-1.5" />Cancelar</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
>
|
|
<div className="space-y-5">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${meta.cls}`}>{meta.label}</span>
|
|
<span className="text-sm text-theme-heading">{compra.proveedor}</span>
|
|
<span className="text-xs text-theme-muted">Fecha {compra.fecha}</span>
|
|
{compra.fecha_entrega_esperada && <span className="text-xs text-theme-muted">Entrega esperada {compra.fecha_entrega_esperada}</span>}
|
|
<span className="text-xs text-theme-muted">Moneda {compra.moneda}</span>
|
|
{compra.recibida_at && <span className="text-xs text-theme-muted">Recibida {compra.recibida_at}</span>}
|
|
</div>
|
|
|
|
{compra.cancel_reason && (
|
|
<p className="text-xs text-rose-700">Cancelada: {compra.cancel_reason}</p>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<div className="rounded-xl bg-theme-bg p-3">
|
|
<p className="text-[11px] text-theme-muted">Subtotal</p>
|
|
<p className="text-theme-heading font-medium tabular-nums">{fmtMoney(compra.subtotal, compra.moneda)}</p>
|
|
</div>
|
|
<div className="rounded-xl bg-theme-bg p-3">
|
|
<p className="text-[11px] text-theme-muted">Impuestos</p>
|
|
<p className="text-theme-heading font-medium tabular-nums">{fmtMoney(compra.impuestos, compra.moneda)}</p>
|
|
</div>
|
|
<div className="rounded-xl bg-theme-bg p-3">
|
|
<p className="text-[11px] text-theme-muted">Total</p>
|
|
<p className="text-theme-heading font-medium tabular-nums">{fmtMoney(compra.total, compra.moneda)}</p>
|
|
</div>
|
|
<div className="rounded-xl bg-theme-bg p-3">
|
|
<p className="text-[11px] text-theme-muted">Pagado / Saldo</p>
|
|
<p className="text-theme-heading font-medium tabular-nums">
|
|
{fmtMoney(compra.pagado, compra.moneda)} <span className="text-theme-muted">/ {fmtMoney(compra.saldo, compra.moneda)}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-sm font-medium text-theme-heading mb-2">Líneas</p>
|
|
<div className="overflow-x-auto border border-theme-border rounded-xl">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-theme-bg text-theme-muted text-[11px] uppercase">
|
|
<tr>
|
|
<th className="text-left px-3 py-2">Artículo</th>
|
|
<th className="text-right px-3 py-2">Pedida</th>
|
|
<th className="text-right px-3 py-2">Recibida</th>
|
|
<th className="text-right px-3 py-2">Pendiente</th>
|
|
<th className="text-right px-3 py-2">Costo</th>
|
|
<th className="text-right px-3 py-2">Subtotal</th>
|
|
<th className="text-left px-3 py-2">Lote / Cad.</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-theme-border">
|
|
{(compra.lineas || []).map((l) => (
|
|
<tr key={l.id}>
|
|
<td className="px-3 py-2 text-theme-heading">{l.item}</td>
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtNum(l.cantidad_pedida)}</td>
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtNum(l.cantidad_recibida)}</td>
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtNum(l.cantidad_pendiente)}</td>
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtMoney(l.costo_unitario, compra.moneda)}</td>
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtMoney(l.subtotal, compra.moneda)}</td>
|
|
<td className="px-3 py-2 text-[11px] text-theme-muted">
|
|
{l.lote || '—'}{l.fecha_caducidad ? ` · ${l.fecha_caducidad}` : ''}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-sm font-medium text-theme-heading mb-2">Recepciones</p>
|
|
{(compra.recepciones || []).length === 0 ? (
|
|
<p className="text-sm text-theme-muted">Sin recepciones todavía.</p>
|
|
) : (
|
|
<ul className="divide-y divide-theme-border border border-theme-border rounded-xl">
|
|
{(compra.recepciones || []).map((r) => (
|
|
<li key={r.id} className="px-3 py-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-theme-heading">{r.fecha}</span>
|
|
<span className="text-theme-muted tabular-nums">{fmtNum(r.total_unidades)} u · {fmtMoney(r.total_costo, compra.moneda)}</span>
|
|
</div>
|
|
<p className="text-[11px] text-theme-muted">{r.usuario || '—'}{r.notas ? ` · ${r.notas}` : ''}</p>
|
|
<ul className="mt-1 space-y-0.5">
|
|
{r.lineas.map((ln) => (
|
|
<li key={ln.id} className="text-[11px] text-theme-muted">
|
|
{ln.item}: {fmtNum(ln.cantidad)} u{ln.lote ? ` · lote ${ln.lote}` : ''}{ln.fecha_caducidad ? ` · cad. ${ln.fecha_caducidad}` : ''}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-theme-heading mb-2">Pagos</p>
|
|
{(compra.pagos || []).length === 0 ? (
|
|
<p className="text-sm text-theme-muted">Sin pagos registrados.</p>
|
|
) : (
|
|
<ul className="divide-y divide-theme-border border border-theme-border rounded-xl">
|
|
{(compra.pagos || []).map((p) => (
|
|
<li key={p.id} className="px-3 py-2 text-sm flex justify-between">
|
|
<div>
|
|
<p className="text-theme-heading">{fmtMoney(p.monto, p.moneda)}</p>
|
|
<p className="text-[11px] text-theme-muted">{p.fecha} · {p.metodo_label}{p.referencia ? ` · ${p.referencia}` : ''}</p>
|
|
</div>
|
|
{p.moneda !== compra.moneda && (
|
|
<span className="text-[11px] text-theme-muted tabular-nums">= {fmtMoney(p.monto_en_compra, compra.moneda)}</span>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{compra.notas && <p className="text-sm text-theme-muted">Observaciones: {compra.notas}</p>}
|
|
<Badge variant="info">Los pagos a proveedores son egresos y no se mezclan con los cobros a pacientes.</Badge>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default CompraDetalleModal;
|