Fix IA (max_tokens del modelo de razonamiento + fallback al gateway), sidebar con toggle arriba, busqueda de un solo campo (paciente/global), editar expediente, marketing en visitas, responsive de agenda; doc de diagnostico WhatsApp
This commit is contained in:
@@ -3,7 +3,7 @@ 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, Wallet,
|
||||
Download, ChevronRight, AlertTriangle, ClipboardList, Camera, Wallet, X,
|
||||
} from 'lucide-react';
|
||||
import Layout from '../components/Layout';
|
||||
import PatientAdjuntos from '../components/PatientAdjuntos';
|
||||
@@ -124,8 +124,9 @@ const Pacientes: FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [search, setSearch] = useState(() => searchParams.get('q') || '');
|
||||
const [searchInput, setSearchInput] = useState(() => searchParams.get('q') || '');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -234,6 +235,12 @@ const Pacientes: FC = () => {
|
||||
setPage(1);
|
||||
}, [search, onlyIncomplete, onlyVip, onlyRecent, onlyStale, doctorFilter]);
|
||||
|
||||
// Búsqueda con debounce: el listado se refresca solo, sin botón "Buscar".
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setSearch(searchInput.trim()), 250);
|
||||
return () => clearTimeout(t);
|
||||
}, [searchInput]);
|
||||
|
||||
useEffect(() => {
|
||||
odooApi.getDoctors()
|
||||
.then((res) => { if (res.status === 'success') setDoctors(res.doctors); })
|
||||
@@ -308,6 +315,33 @@ const Pacientes: FC = () => {
|
||||
setForm((prev) => ({ ...prev, ...patch }));
|
||||
};
|
||||
|
||||
// Abrir el formulario de edición desde otra pantalla (p. ej. el botón
|
||||
// "Editar" del drawer del expediente): /pacientes?edit=<id>
|
||||
const editPatientId = searchParams.get('edit');
|
||||
useEffect(() => {
|
||||
if (!editPatientId) return;
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await odooApi.getPatient(parseInt(editPatientId, 10));
|
||||
if (active && res.status === 'success') openEdit(res.patient);
|
||||
} catch (err) {
|
||||
toast.error('No se pudo abrir el paciente');
|
||||
console.error(err);
|
||||
} finally {
|
||||
if (active) {
|
||||
setSearchParams((prev) => {
|
||||
const next = new URLSearchParams(prev);
|
||||
next.delete('edit');
|
||||
return next;
|
||||
}, { replace: true });
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => { active = false; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editPatientId, setSearchParams]);
|
||||
|
||||
// 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(() => {
|
||||
@@ -500,13 +534,26 @@ const Pacientes: FC = () => {
|
||||
<Card.Body>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-4 sm:mb-6">
|
||||
<div className="relative w-full sm:max-w-sm">
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-theme-muted" />
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-theme-muted pointer-events-none" />
|
||||
<Input
|
||||
placeholder="Buscar por nombre o teléfono..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Buscar por nombre, apellidos o teléfono..."
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') setSearch(searchInput.trim()); if (e.key === 'Escape') { setSearchInput(''); setSearch(''); } }}
|
||||
className="pl-10"
|
||||
inputClassName={searchInput ? 'pr-9' : undefined}
|
||||
/>
|
||||
{searchInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSearchInput(''); setSearch(''); }}
|
||||
title="Limpiar búsqueda"
|
||||
aria-label="Limpiar búsqueda"
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-1 rounded-lg text-theme-muted hover:text-theme-heading hover:bg-theme-bg"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
options={[
|
||||
@@ -855,7 +902,18 @@ const Pacientes: FC = () => {
|
||||
<FileText size={16} className="mr-2" />
|
||||
Estado de cuenta
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => selected && openEdit(selected)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// El modal de detalle se cierra antes de abrir el de edición:
|
||||
// ambos comparten z-index y el de detalle, al estar después en
|
||||
// el DOM, tapaba el formulario (parecía que "Editar" no hacía nada).
|
||||
if (!selected) return;
|
||||
const p = selected;
|
||||
setSelected(null);
|
||||
openEdit(p);
|
||||
}}
|
||||
>
|
||||
<Edit2 size={16} className="mr-2" />
|
||||
Editar paciente
|
||||
</Button>
|
||||
@@ -920,7 +978,11 @@ const Pacientes: FC = () => {
|
||||
{selected.expediente_missing && selected.expediente_missing.length > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<p className="text-xs text-theme-muted">Falta: {selected.expediente_missing.join(', ')}</p>
|
||||
<Button variant="outline" size="sm" onClick={() => openEdit(selected)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { const p = selected; setSelected(null); openEdit(p); }}
|
||||
>
|
||||
Completar expediente
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user