Agenda: seccion Por atender (sala de espera) con pacientes en check-in, tiempo de espera y timestamp arrived_at

This commit is contained in:
2026-08-31 03:08:41 +00:00
parent 1c31eecb8c
commit 009f84ce4a
4 changed files with 57 additions and 1 deletions

View File

@@ -183,6 +183,25 @@ const Agenda: FC = () => {
return 'Sin confirmar por teléfono';
};
// ---- Sala de espera: citas con check-in (2da confirmación) por atender ----
// Para rol medico el servidor ya devuelve solo sus citas; para recepción/admin
// se respeta el filtro de médico seleccionado (filteredAppointments).
const waitSortKey = (a: Appointment) => a.arrived_at || `9999-12-31 ${a.time}`;
const waitingAppointments = filteredAppointments
.filter((a) => a.state === 'arrived')
.sort((a, b) => waitSortKey(a).localeCompare(waitSortKey(b)));
// arrived_at viene en UTC (datetime de Odoo)
const waitLabel = (apt: Appointment): string | null => {
if (!apt.arrived_at) return null;
const arrived = new Date(`${apt.arrived_at.replace(' ', 'T')}Z`);
if (Number.isNaN(arrived.getTime())) return null;
const mins = Math.max(0, Math.round((Date.now() - arrived.getTime()) / 60000));
if (mins < 1) return 'Recién llegó';
if (mins < 60) return `${mins} min en espera`;
return `${Math.floor(mins / 60)} h ${String(mins % 60).padStart(2, '0')} min en espera`;
};
// ---- Vista por médico (grid doctores x bloques de 15 min) ----
const GRID_START = 8; // horario clínica: 8:00
const GRID_END = 19; // horario clínica: 19:00 (último bloque inicia 18:45)
@@ -944,6 +963,37 @@ const Agenda: FC = () => {
)}
</div>
{/* Sala de espera: pacientes con check-in (2da confirmación) por atender */}
<div className="mb-4 sm:mb-5 rounded-2xl border-2 border-sky-700 bg-brand-sky/40 p-3">
<div className="flex items-center gap-2 mb-2">
<UserCheck size={16} className="text-sky-700" />
<p className="text-xs font-semibold text-theme-heading uppercase">Por atender</p>
<span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full text-[10px] bg-theme-surface text-theme-heading border border-theme-border">
{waitingAppointments.length}
</span>
</div>
{waitingAppointments.length === 0 ? (
<p className="text-xs text-theme-muted">Sin pacientes en espera.</p>
) : (
<div className="flex flex-wrap gap-2">
{waitingAppointments.map((apt) => (
<button
key={apt.id}
type="button"
onClick={() => openAptDetail(apt)}
className="rounded-xl border border-sky-700 bg-theme-surface px-3 py-2 text-left shadow-card transition-shadow hover:shadow-soft"
>
<p className="text-sm font-semibold text-theme-heading">{apt.time} · {apt.patient}</p>
<p className="text-xs text-theme-muted truncate max-w-[240px]">
{apt.service}{!isMedico && apt.doctor ? ` · ${apt.doctor}` : ''}
</p>
<p className="text-xs font-medium text-sky-700">{waitLabel(apt) || 'En espera'}</p>
</button>
))}
</div>
)}
</div>
{error && <p className="text-rose-600 text-sm mb-4">{error}</p>}
{loading ? (

View File

@@ -72,6 +72,8 @@ export interface Appointment {
phone_confirmed?: boolean;
phone_confirmed_at?: string | null;
phone_confirmed_by?: string | null;
// Timestamp del check-in de llegada (2da confirmación)
arrived_at?: string | null;
}
export interface Bloqueo {