Agenda: seccion Por atender (sala de espera) con pacientes en check-in, tiempo de espera y timestamp arrived_at
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user