diff --git a/frontend/src/pages/Pos.tsx b/frontend/src/pages/Pos.tsx index 358b39f..c3094e6 100644 --- a/frontend/src/pages/Pos.tsx +++ b/frontend/src/pages/Pos.tsx @@ -20,6 +20,9 @@ import { odooApi, type Service, type Patient, type InventoryItem, type PosChecko const fmtMoney = (n: number) => (n || 0).toLocaleString('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 2 }); +const fmtUsd = (n: number) => + `$${(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD`; + const METODO_OPTIONS = [ { value: 'cash', label: 'Efectivo' }, { value: 'card', label: 'Tarjeta' }, @@ -62,9 +65,11 @@ const Pos: FC = () => { // Cobro const [cobroOpen, setCobroOpen] = useState(false); const [metodo, setMetodo] = useState('cash'); + const [moneda, setMoneda] = useState<'MXN' | 'USD'>('MXN'); const [recibido, setRecibido] = useState(''); const [conPuntos, setConPuntos] = useState(false); const [cobrando, setCobrando] = useState(false); + const [tcRate, setTcRate] = useState(null); // Resultado const [resultado, setResultado] = useState(null); @@ -118,6 +123,18 @@ const Pos: FC = () => { .catch(() => {}); }, []); + // Tipo de cambio USD/MXN: al montar y refresco cada 5 min + useEffect(() => { + const fetchTc = () => { + odooApi.getExchangeRate() + .then((res) => { if (res.current) setTcRate(res.current.rate); }) + .catch(() => {}); + }; + fetchTc(); + const interval = setInterval(fetchTc, 5 * 60 * 1000); + return () => clearInterval(interval); + }, []); + // Búsqueda server-side de paciente useEffect(() => { if (paciente || altaRapida) return; @@ -183,7 +200,15 @@ const Pos: FC = () => { const numItems = lines.reduce((a, l) => a + l.qty, 0); const puntosPaciente = paciente?.wallet_points ?? 0; const puedePuntos = puntosPaciente >= total && total > 0; - const cambio = metodo === 'cash' && recibido ? Math.max(0, (parseFloat(recibido) || 0) - total) : 0; + // Equivalentes USD (si hay tipo de cambio configurado) + const toUsd = (mxn: number): number | null => (tcRate ? mxn / tcRate : null); + const recibidoMxn = recibido + ? moneda === 'USD' && tcRate + ? (parseFloat(recibido) || 0) * tcRate + : parseFloat(recibido) || 0 + : 0; + const cambio = metodo === 'cash' && recibido ? Math.max(0, recibidoMxn - total) : 0; + const cambioUsd = moneda === 'USD' && tcRate ? cambio / tcRate : 0; const crearPacienteRapido = async () => { if (!altaForm.name.trim() || !altaForm.phone.trim()) { @@ -214,6 +239,7 @@ const Pos: FC = () => { return; } setMetodo('cash'); + setMoneda('MXN'); setRecibido(''); setConPuntos(false); setCobroOpen(true); @@ -235,6 +261,7 @@ const Pos: FC = () => { payment_method: metodo, amount_received: metodo === 'cash' && recibido ? parseFloat(recibido) : undefined, pay_with_points: conPuntos, + currency: moneda, }); if (res.status === 'success') { setResultado(res); @@ -372,23 +399,41 @@ const Pos: FC = () => { <>
Subtotal - {fmtMoney(subtotal)} + + {fmtMoney(subtotal)} + + {toUsd(subtotal) !== null ? `≈ ${fmtUsd(toUsd(subtotal)!)}` : '—'} + +
Descuento - setDiscount(e.target.value)} - placeholder="0" - className="w-28 border border-theme-border-strong rounded-lg px-2 py-1 text-sm text-right" - /> + + {discountNum > 0 && toUsd(discountNum) !== null && ( + ≈ {fmtUsd(toUsd(discountNum)!)} + )} + setDiscount(e.target.value)} + placeholder="0" + className="w-28 border border-theme-border-strong rounded-lg px-2 py-1 text-sm text-right" + /> +
Total - {fmtMoney(total)} + + {fmtMoney(total)} + + {toUsd(total) !== null ? `≈ ${fmtUsd(toUsd(total)!)}` : '—'} + +