POS vista lista, modulo Tickets para gerencia con cancelacion auditada, y saldo a favor (recargas de anticipo desde expediente y agenda, canje en POS)
This commit is contained in:
@@ -3,11 +3,13 @@ import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Search, Plus, Activity, ShoppingBag, User, Edit2, Phone, Mail, Calendar, Droplet, Heart,
|
||||
MapPin, Briefcase, Users, Baby, Stethoscope, FileText, AlertCircle, CheckCircle2, XCircle,
|
||||
Download, ChevronRight, AlertTriangle, ClipboardList, Camera,
|
||||
Download, ChevronRight, AlertTriangle, ClipboardList, Camera, Wallet,
|
||||
} from 'lucide-react';
|
||||
import Layout from '../components/Layout';
|
||||
import PatientAdjuntos from '../components/PatientAdjuntos';
|
||||
import EstadoCuentaPrint from '../components/EstadoCuentaPrint';
|
||||
import RecargarSaldoModal from '../components/RecargarSaldoModal';
|
||||
import ReciboPrint from '../components/ReciboPrint';
|
||||
import { usePatientDrawer } from '../components/PatientDrawer';
|
||||
import {
|
||||
Card,
|
||||
@@ -27,9 +29,12 @@ import {
|
||||
} from '../components/ui';
|
||||
import type { BadgeVariant } from '../components/ui/Badge';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { odooApi, type Patient, type Appointment, type Sale, type Visita, type Doctor, type EstadoCuenta } from '../services/odoo';
|
||||
import { odooApi, type Patient, type Appointment, type Sale, type Visita, type Doctor, type EstadoCuenta, type PatientWallet, type ReceiptData } from '../services/odoo';
|
||||
import { exportToExcel } from '../lib/exporter';
|
||||
|
||||
const fmtMoney = (n: number) =>
|
||||
(n || 0).toLocaleString('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 0 });
|
||||
|
||||
const genderOptions = [
|
||||
{ value: '', label: 'No especificado' },
|
||||
{ value: 'male', label: 'Masculino' },
|
||||
@@ -128,6 +133,10 @@ const Pacientes: FC = () => {
|
||||
const [selected, setSelected] = useState<Patient | null>(null);
|
||||
const [estadoCuentaOpen, setEstadoCuentaOpen] = useState(false);
|
||||
const [history, setHistory] = useState<{ appointments: Appointment[]; sales: Sale[]; visitas: Visita[]; estado_cuenta?: EstadoCuenta } | null>(null);
|
||||
// Saldo a favor (monedero, 1 pt = $1 MXN) del paciente abierto
|
||||
const [wallet, setWallet] = useState<PatientWallet | null>(null);
|
||||
const [recargaOpen, setRecargaOpen] = useState(false);
|
||||
const [reciboDeposito, setReciboDeposito] = useState<ReceiptData | null>(null);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [tab, setTab] = useState<'info' | 'clinical' | 'timeline' | 'docs'>('info');
|
||||
const [entryDetail, setEntryDetail] = useState<
|
||||
@@ -239,9 +248,13 @@ const Pacientes: FC = () => {
|
||||
setSelected(patient);
|
||||
setTab('info');
|
||||
setHistoryLoading(true);
|
||||
setWallet(null);
|
||||
try {
|
||||
const res = await odooApi.getPatientHistory(patient.id);
|
||||
if (res.status === 'success') setHistory({ appointments: res.appointments, sales: res.sales, visitas: res.visitas || [], estado_cuenta: res.estado_cuenta });
|
||||
odooApi.getPatientWallet(patient.id)
|
||||
.then((w) => { if (w.status === 'success') setWallet(w); })
|
||||
.catch(() => setWallet(null));
|
||||
} catch (err) {
|
||||
toast.error('Error al cargar historial');
|
||||
console.error(err);
|
||||
@@ -964,6 +977,44 @@ const Pacientes: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Saldo a favor: dinero a cuenta del paciente */}
|
||||
<div className="p-3 rounded-xl border border-emerald-200 bg-emerald-50/60">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-theme-muted uppercase mb-1 flex items-center gap-1">
|
||||
<Wallet size={12} /> Saldo a favor
|
||||
</p>
|
||||
<p className="text-2xl font-heading font-semibold text-theme-heading">
|
||||
{fmtMoney(wallet?.equivalent_mxn ?? 0)}
|
||||
</p>
|
||||
<p className="text-xs text-theme-muted">
|
||||
{wallet?.equivalent_usd != null
|
||||
? `≈ ${wallet.equivalent_usd.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} USD`
|
||||
: 'Sin tipo de cambio'}
|
||||
{(wallet?.depositado ?? 0) > 0 ? ` · recargado ${fmtMoney(wallet?.depositado ?? 0)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={() => setRecargaOpen(true)}>
|
||||
<Plus size={14} className="mr-1.5" /> Recargar saldo
|
||||
</Button>
|
||||
</div>
|
||||
{(wallet?.movimientos || []).length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-emerald-200 max-h-52 overflow-y-auto space-y-1.5">
|
||||
{wallet!.movimientos.slice(0, 25).map((m) => (
|
||||
<div key={m.id} className="flex items-start justify-between gap-2 text-[11px]">
|
||||
<span className="text-theme-muted min-w-0">
|
||||
<span className="block text-theme-heading">{m.narration}</span>
|
||||
<span className="block">{m.date}{m.operator ? ` · ${m.operator}` : ''}</span>
|
||||
</span>
|
||||
<span className={`shrink-0 font-semibold ${m.points > 0 ? 'text-emerald-700' : 'text-rose-600'}`}>
|
||||
{m.points > 0 ? '+' : ''}{m.points}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<InfoRow label="Folio" value={selected?.patient_id} icon={FileText} />
|
||||
<InfoRow label="Teléfono" value={selected?.phone} icon={Phone} />
|
||||
@@ -1263,6 +1314,23 @@ const Pacientes: FC = () => {
|
||||
onClose={() => setEstadoCuentaOpen(false)}
|
||||
paciente={selected}
|
||||
/>
|
||||
|
||||
<RecargarSaldoModal
|
||||
isOpen={recargaOpen}
|
||||
onClose={() => setRecargaOpen(false)}
|
||||
patientId={selected?.id ?? null}
|
||||
patientName={selected?.name}
|
||||
saldoActual={wallet?.equivalent_mxn ?? 0}
|
||||
onDone={(receipt) => {
|
||||
setReciboDeposito(receipt);
|
||||
if (selected?.id) {
|
||||
odooApi.getPatientWallet(selected.id)
|
||||
.then((w) => { if (w.status === 'success') setWallet(w); })
|
||||
.catch(() => {});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ReciboPrint receipt={reciboDeposito} onClose={() => setReciboDeposito(null)} />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user