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:
2026-09-18 04:25:33 +00:00
parent cebf8d663d
commit d75411f0d9
19 changed files with 1278 additions and 30 deletions

View File

@@ -1,9 +1,11 @@
import type { FC } from 'react';
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { Clock, CheckCircle, XCircle, CheckCheck, UserCheck, UserX, Plus, PackageCheck, Sparkles, RotateCcw, LayoutGrid, List, Rows3, CalendarRange, CalendarClock, ChevronLeft, ChevronRight, Ban, Trash2, Phone, Cake, CreditCard, Pencil, AlertTriangle, Printer, PhoneCall } from 'lucide-react';
import { Clock, CheckCircle, XCircle, CheckCheck, UserCheck, UserX, Plus, PackageCheck, Sparkles, RotateCcw, LayoutGrid, List, Rows3, CalendarRange, CalendarClock, ChevronLeft, ChevronRight, Ban, Trash2, Phone, Cake, CreditCard, Pencil, AlertTriangle, Printer, PhoneCall, Wallet } from 'lucide-react';
import Layout from '../components/Layout';
import { usePatientDrawer } from '../components/PatientDrawer';
import AgendaPrint from '../components/AgendaPrint';
import RecargarSaldoModal from '../components/RecargarSaldoModal';
import ReciboPrint from '../components/ReciboPrint';
import {
Card,
Button,
@@ -20,7 +22,7 @@ import {
badgeForAppointmentState,
} from '../components/ui';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { odooApi, type Appointment, type Patient, type Service, type Doctor, type Bloqueo } from '../services/odoo';
import { odooApi, type Appointment, type Patient, type Service, type Doctor, type Bloqueo, type PatientWallet, type ReceiptData } from '../services/odoo';
import { useAuth } from '../lib/auth';
const stateOptions = [
@@ -790,8 +792,20 @@ const Agenda: FC = () => {
}
};
// Saldo a favor del paciente de la cita abierta (para verlo al llegar)
const [saldoPaciente, setSaldoPaciente] = useState<PatientWallet | null>(null);
const [recargaAptOpen, setRecargaAptOpen] = useState(false);
const [reciboApt, setReciboApt] = useState<ReceiptData | null>(null);
const openAptDetail = (apt: Appointment) => {
setDetailApt(apt);
setSaldoPaciente(null);
setRecargaAptOpen(false);
if (apt.patient_id) {
odooApi.getPatientWallet(apt.patient_id)
.then((w) => { if (w.status === 'success') setSaldoPaciente(w); })
.catch(() => setSaldoPaciente(null));
}
setEditMode(false);
setEditForm({
date: apt.date,
@@ -2503,6 +2517,24 @@ const Agenda: FC = () => {
<p className="text-xs text-theme-muted mb-1">Precio</p>
<p className="text-sm font-medium text-theme-heading">${detailApt.price}</p>
</div>
<div className="p-3 rounded-xl border border-emerald-200 bg-emerald-50/60 col-span-2 flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-xs text-theme-muted mb-1 flex items-center gap-1">
<Wallet size={11} /> Saldo a favor del paciente
</p>
<p className="text-sm font-semibold text-theme-heading">
${(saldoPaciente?.equivalent_mxn ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{saldoPaciente?.equivalent_usd != null && (
<span className="ml-1.5 text-xs font-normal text-theme-muted">
≈ {saldoPaciente.equivalent_usd.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} USD
</span>
)}
</p>
</div>
<Button size="sm" variant="outline" onClick={() => setRecargaAptOpen(true)}>
<Wallet size={14} className="mr-1.5" /> Recargar saldo
</Button>
</div>
</div>
{detailApt.notes && (
<div className="p-3 bg-theme-bg rounded-xl">
@@ -2630,6 +2662,26 @@ const Agenda: FC = () => {
)}
</Modal>
{/* Recarga de saldo a favor del paciente de la cita: se monta después del
modal de detalle para quedar por encima (mismo z-index). */}
<RecargarSaldoModal
isOpen={recargaAptOpen}
onClose={() => setRecargaAptOpen(false)}
patientId={detailApt?.patient_id ?? null}
patientName={detailApt?.patient}
citaId={detailApt?.id ?? null}
saldoActual={saldoPaciente?.equivalent_mxn ?? 0}
onDone={(receipt) => {
setReciboApt(receipt);
if (detailApt?.patient_id) {
odooApi.getPatientWallet(detailApt.patient_id)
.then((w) => { if (w.status === 'success') setSaldoPaciente(w); })
.catch(() => {});
}
}}
/>
<ReciboPrint receipt={reciboApt} onClose={() => setReciboApt(null)} />
{/* Modal de conflicto de agenda: mensaje humano + ver la cita que choca */}
<Modal
isOpen={!!conflict}