Oleada UX 2: Agenda (mensajes de conflicto humanos, imprimir agenda del dia, confirmacion en lote, prefs persistentes) y Pacientes (acciones en ficha, timeline unificada, aviso de telefono duplicado) + fixes de precarga e impresion

This commit is contained in:
2026-09-17 18:47:35 +00:00
parent afc208a7b7
commit b4f91527f1
8 changed files with 1048 additions and 116 deletions

View File

@@ -8,6 +8,7 @@ import {
import Layout from '../components/Layout';
import PatientAdjuntos from '../components/PatientAdjuntos';
import EstadoCuentaPrint from '../components/EstadoCuentaPrint';
import { usePatientDrawer } from '../components/PatientDrawer';
import {
Card,
Button,
@@ -142,6 +143,9 @@ const Pacientes: FC = () => {
const [editing, setEditing] = useState<Patient | null>(null);
const [photo, setPhoto] = useState<string | null>(null);
const photoRef = useRef<HTMLInputElement>(null);
const { openPatient } = usePatientDrawer();
// Aviso de duplicado por teléfono/móvil en el formulario de alta/edición
const [phoneDup, setPhoneDup] = useState<{ id: number; name: string } | null>(null);
const emptyForm: PatientForm = {
name: '',
@@ -290,6 +294,36 @@ const Pacientes: FC = () => {
setForm((prev) => ({ ...prev, ...patch }));
};
// Duplicado de teléfono: se consulta con debounce al escribir (phone o mobile).
// Al editar se excluye el propio paciente para no auto-alertarse.
useEffect(() => {
if (!modalOpen) {
setPhoneDup(null);
return;
}
const digitsOf = (s: string) => (s || '').replace(/\D/g, '');
const phone = (form.phone || '').trim();
const mobile = (form.mobile || '').trim();
const checkVal = digitsOf(phone).length >= 7 ? phone : mobile;
if (digitsOf(checkVal).length < 7) {
setPhoneDup(null);
return;
}
let active = true;
const t = setTimeout(async () => {
try {
const res = await odooApi.checkPatientPhone(checkVal, editing?.id);
if (active) setPhoneDup(res.status === 'success' && res.exists && res.patient ? { id: res.patient.id, name: res.patient.name } : null);
} catch {
if (active) setPhoneDup(null);
}
}, 400);
return () => {
active = false;
clearTimeout(t);
};
}, [form.phone, form.mobile, editing?.id, modalOpen]);
const onPhotoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
e.target.value = '';
@@ -638,6 +672,21 @@ const Pacientes: FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input label="Nombre *" value={form.name} onChange={(e) => updateForm({ name: e.target.value })} />
<Input label="Teléfono *" value={form.phone} onChange={(e) => updateForm({ phone: e.target.value })} />
{phoneDup && (
<div className="sm:col-span-2 rounded-xl border border-amber-300 bg-amber-50 px-3 py-2.5 text-sm text-amber-800">
<p className="font-medium">
Este teléfono ya está registrado: {phoneDup.name}. ¿Es la misma persona?
</p>
<div className="flex flex-wrap gap-2 mt-2">
<Button size="sm" variant="outline" onClick={() => openPatient(phoneDup.id)}>
Abrir su ficha
</Button>
<Button size="sm" variant="ghost" onClick={() => setPhoneDup(null)}>
Continuar de todos modos
</Button>
</div>
</div>
)}
<Input label="Email" type="email" value={form.email} onChange={(e) => updateForm({ email: e.target.value })} />
<Input label="Celular" value={form.mobile} onChange={(e) => updateForm({ mobile: e.target.value })} />
<Input label="Teléfono casa" value={form.home_phone} onChange={(e) => updateForm({ home_phone: e.target.value })} />