import { useEffect, useMemo, useState } from 'react'; import { Stethoscope, Plus, ChevronLeft, ChevronRight } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAuth } from '../../context/auth-context.jsx'; import { useDraftForm } from '../../hooks/useDraftForm.js'; import { useGuardedClose } from '../../components/NavigationGuard.jsx'; import { DraftNotice } from '../../components/DraftNotice.jsx'; import { Button } from '../../components/Button.jsx'; import { Field } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { CheckListDropdown } from '../../components/CheckListDropdown.jsx'; import { DateTimeEditor } from '../../components/DateTimeEditor.jsx'; import { RecurrenceEditor } from '../../components/RecurrenceEditor.jsx'; import { DetailPopup } from '../../components/DetailPopup.jsx'; import { Tabs } from '../../components/Tabs.jsx'; import { InfoBar } from '../../components/InfoBar.jsx'; import { DatePicker } from '../../components/DatePicker.jsx'; import { MonthGrid, monthGridDays } from '../../components/MonthGrid.jsx'; import * as api from '../../services/clinic.js'; import { listUsers } from '../../services/users.js'; import './clinic.css'; const dayKey = d => `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`; const today = () => { const parts = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone: 'America/Guayaquil', year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(new Date()).map(p => [p.type, p.value])); return `${parts.year}-${parts.month}-${parts.day}`; }; const move = (day, n) => dayKey(new Date(new Date(`${day}T12:00:00Z`).getTime() + n * 86400000)); const at = (day, hour = '09:00') => `${day} ${hour}:00`; const plus = (s, n) => { const d = new Date(`${s.replace(' ', 'T')}Z`); return new Date(d.getTime() + n * 60000).toISOString().slice(0, 19).replace('T', ' '); }; const initialAppointment = (day, doctorId, slot) => ({ doctorId: doctorId || '', patientId: '', typeId: '', startsAt: slot || at(day), endsAt: plus(slot || at(day), 30), channel: 'phone', notes: '' }); const initialPatient = () => ({ firstNames: '', lastNames: '', identification: '', phone: '', email: '', birthDate: '', notes: '' }); const initialBlock = (day, doctorId) => ({ doctorId: doctorId || '', kind: 'surgery', title: '', startsAt: at(day), endsAt: plus(at(day), 60), recurrence: null }); const formAppointment = a => ({ doctorId: a.doctor_user_id, patientId: a.patient_id, typeId: a.type_id, startsAt: a.starts_at, endsAt: a.ends_at, channel: a.channel, notes: a.notes || '', editVersion: a.edit_version }); const formPatient = p => ({ firstNames: p.first_names, lastNames: p.last_names, identification: p.identification || '', phone: p.phone, email: p.email || '', birthDate: p.birth_date || '', notes: p.notes || '', editVersion: p.edit_version }); const formBlock = b => ({ doctorId: b.doctor_user_id, kind: b.kind, title: b.title, startsAt: b.starts_at, endsAt: b.ends_at, recurrence: b.recurrence_json ? JSON.parse(b.recurrence_json) : null, editVersion: b.edit_version }); const readStored = (key, fallback) => { try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; } }; export default function AgendaMedicaPage() { const { t, lang, openDialog, pushToast } = useUi(); const { user } = useAuth(); const guardedClose = useGuardedClose(); const [me, setMe] = useState(null), [error, setError] = useState(''), [doctors, setDoctors] = useState([]), [types, setTypes] = useState([]), [allTypes, setAllTypes] = useState([]); const [tab, setTab] = useState('agenda'), [view, setView] = useState(() => { const v = readStored('plc_clinic_view_v2', 'month'); return ['day', 'week', 'month', 'list'].includes(v) ? v : 'month'; }), [day, setDay] = useState(today); const [doctorFilter, setDoctorFilter] = useState(() => { const value = readStored('plc_clinic_doctors', []); return Array.isArray(value) ? value : []; }); const [statusFilter, setStatusFilter] = useState(() => { const value = readStored('plc_clinic_status', ['scheduled', 'confirmed', 'attended', 'no_show']); return Array.isArray(value) ? value : ['scheduled', 'confirmed', 'attended', 'no_show']; }); const [appointments, setAppointments] = useState([]), [blocks, setBlocks] = useState([]), [patients, setPatients] = useState([]), [search, setSearch] = useState(''), [page, setPage] = useState(1), [patientTotal, setPatientTotal] = useState(0); const [popup, setPopup] = useState(null), [editing, setEditing] = useState(false), [conflict, setConflict] = useState(''), [affected, setAffected] = useState([]), [slots, setSlots] = useState([]), [cancelReason, setCancelReason] = useState(''), [cancelMode, setCancelMode] = useState(false), [patientHistory, setPatientHistory] = useState([]), [quickPatientId, setQuickPatientId] = useState(null), [reprogram, setReprogram] = useState([]); const [allUsers, setAllUsers] = useState([]), [staffRows, setStaffRows] = useState([]), [staffUser, setStaffUser] = useState(''), [staffInitial, setStaffInitial] = useState({ scope: 'own', isDoctor: true, specialty: '', color: '#0078d4', isActive: true }); const [typeInitial, setTypeInitial] = useState({ name: '', durationMinutes: 30, color: '#0078d4', isActive: true }); const [scheduleDoctor, setScheduleDoctor] = useState(''), [scheduleInitial, setScheduleInitial] = useState({ rows: [] }); const staffDraft = useDraftForm({ formId: 'clinic-staff', entityId: staffUser || null, initial: staffInitial, label: t('clinic_staff'), enabled: tab === 'config' }); const typeDraft = useDraftForm({ formId: 'clinic-type', entityId: typeInitial.id || null, initial: typeInitial, label: t('clinic_types'), enabled: tab === 'config' }); const scheduleDraft = useDraftForm({ formId: 'clinic-schedule', entityId: scheduleDoctor || null, initial: scheduleInitial, label: t('clinic_schedules'), enabled: tab === 'config' }); const staffForm = staffDraft.values, typeForm = typeDraft.values, scheduleRows = scheduleDraft.values.rows || []; const setStaffForm = staffDraft.setValues, setTypeForm = typeDraft.setValues; const setScheduleRows = rows => scheduleDraft.setField('rows', rows); const isReception = me?.scope === 'all', isAdmin = ['admin', 'superadmin'].includes(user?.roleCode); const singleDoctor = doctorFilter.length ? doctorFilter[0] : doctors[0]?.id; const selectedDoctors = doctors.filter(d => !doctorFilter.length || doctorFilter.includes(d.id)); const period = useMemo(() => { let from = day, to = move(day, 1); if (view === 'week') { const d = new Date(`${day}T12:00:00Z`), offset = (d.getUTCDay() + 6) % 7; from = move(day, -offset); to = move(from, 7); } if (view === 'month') { const cells = monthGridDays(day); from = cells[0]; to = move(cells.at(-1), 1); } if (view === 'list') to = move(day, 30); return { from: at(from, '00:00'), to: at(to, '00:00') }; }, [day, view]); async function load() { if (!me) return; try { const [a, b] = await Promise.all([api.clinicAppointments(period.from, period.to), api.clinicBlocks(period.from, period.to)]); setAppointments(a); setBlocks(b); setError(''); } catch (e) { setError(e.message); } } useEffect(() => { if (!user?.modules?.includes('clinic')) return; // Admin/superadmin sin fila de personal: solo puede configurar la agenda (pestana Configuracion). api.clinicMe().then(async staff => { setMe(staff); const data = await api.clinicDoctors(); setDoctors(data.doctors); setTypes(data.types); }).catch(e => { if (isAdmin) setTab('config'); else setError(e.message); }); }, [user?.id]); useEffect(() => { void load(); }, [me?.user_id, period.from, period.to]); useEffect(() => { try { localStorage.setItem('plc_clinic_view_v2', JSON.stringify(view)); } catch {} }, [view]); useEffect(() => { try { localStorage.setItem('plc_clinic_doctors', JSON.stringify(doctorFilter)); } catch {} }, [doctorFilter]); useEffect(() => { try { localStorage.setItem('plc_clinic_status', JSON.stringify(statusFilter)); } catch {} }, [statusFilter]); useEffect(() => { if (tab === 'patients' && isReception) api.clinicPatients(search, page).then(x => { setPatients(x.items); setPatientTotal(x.total); }).catch(e => setError(e.message)); }, [tab, search, page, isReception]); useEffect(() => { if (tab === 'config' && isAdmin) Promise.all([api.clinicStaff(), listUsers(), api.clinicTypes()]).then(([rows, users, kindRows]) => { setStaffRows(rows); setAllUsers(users); setAllTypes(kindRows); }).catch(e => setError(e.message)); }, [tab, isAdmin]); const formId = popup?.kind === 'appointment' ? 'clinic-appointment' : popup?.kind === 'patient' ? 'clinic-patient' : 'clinic-block'; const initial = popup?.kind === 'appointment' ? popup.item ? formAppointment(popup.item) : initialAppointment(day, popup.doctorId, popup.slot) : popup?.kind === 'patient' ? popup.item ? formPatient(popup.item) : initialPatient() : popup?.kind === 'block' ? popup.item ? formBlock(popup.item) : initialBlock(day, popup.doctorId) : {}; const draft = useDraftForm({ formId, entityId: popup?.item?.id, initial, label: t(`clinic_${popup?.kind || 'appointment'}`), enabled: !!popup && editing, onDiscard: () => setEditing(false) }); const v = draft.values; useEffect(() => { if (quickPatientId && popup?.kind === 'appointment' && editing) { draft.setField('patientId', quickPatientId); setQuickPatientId(null); } }, [quickPatientId, popup?.kind, editing]); useEffect(() => { if (popup?.kind !== 'appointment' || !editing || !v.doctorId || !v.typeId || !v.startsAt) return; api.clinicAvailability(v.doctorId, v.startsAt.slice(0, 10), v.typeId).then(setSlots).catch(() => setSlots([])); }, [popup?.kind, editing, v.doctorId, v.typeId, v.startsAt?.slice(0, 10)]); function open(kind, item = null, extra = {}) { setPopup({ kind, item, ...extra }); setEditing(!item); setConflict(''); setAffected([]); setCancelMode(false); if (kind === 'patient' && item) api.clinicPatientHistory(item.id).then(setPatientHistory).catch(e => setConflict(e.message)); if (kind === 'appointment' && isReception) api.clinicPatients('', 1).then(x => setPatients(x.items)).catch(() => {}); } function close() { void guardedClose(editing ? draft : null, () => { setPopup(null); setEditing(false); setConflict(''); }); } async function save(confirmAffected = false) { if (popup.item && !draft.isDirty) return; try { let result; if (popup.kind === 'appointment') result = await api.clinicSaveAppointment(v, popup.item?.id); if (popup.kind === 'patient') result = await api.clinicSavePatient(v, popup.item?.id); if (popup.kind === 'block') result = await api.clinicSaveBlock({ ...v, confirmAffected }, popup.item?.id); if (result?.requiresConfirmation) { setAffected(result.affected); return; } draft.clear(); if (popup.returnTo) { setQuickPatientId(result.id); setPopup(popup.returnTo); setEditing(true); } else { setPopup(null); setEditing(false); } setConflict(''); setAffected([]); if (result?.affected?.length) setReprogram(result.affected); await load(); if (tab === 'patients') { const list = await api.clinicPatients(search, page); setPatients(list.items); } pushToast({ tone: 'success', message: t('common_saved') }); } catch (e) { setConflict(e.message); } } async function actionStatus(status) { try { await api.clinicAppointmentStatus(popup.item.id, { status, editVersion: popup.item.edit_version }); setPopup(null); await load(); pushToast({ tone: 'success', message: t('common_saved') }); } catch (e) { setConflict(e.message); } } function cancelAppointment() { if (!cancelReason.trim()) return; openDialog({ tone: 'warning', title: t('clinic_cancel'), message: t('clinic_cancel_confirm'), actions: [{ label: t('common_cancel') }, { label: t('clinic_cancel'), variant: 'danger', onClick: async () => { try { await api.clinicCancelAppointment(popup.item.id, { reason: cancelReason, editVersion: popup.item.edit_version }); setPopup(null); setCancelMode(false); await load(); } catch (e) { setConflict(e.message); } } }] }); } function appointmentMenu(item) { const actions = [{ label: t('common_close') }]; if (isReception) { actions.push({ label: t('clinic_confirm'), onClick: () => api.clinicAppointmentStatus(item.id, { status: 'confirmed', editVersion: item.edit_version }).then(load).catch(e => setError(e.message)) }); actions.push({ label: t('clinic_reprogram'), onClick: () => { open('appointment', item); setEditing(true); } }); actions.push({ label: t('clinic_cancel'), variant: 'danger', onClick: () => { open('appointment', item); setCancelMode(true); } }); } for (const status of ['attended', 'no_show']) actions.push({ label: t(`clinic_status_${status}`), onClick: () => api.clinicAppointmentStatus(item.id, { status, editVersion: item.edit_version }).then(load).catch(e => setError(e.message)) }); openDialog({ title: `${item.first_names} ${item.last_names}`, message: item.starts_at, actions }); } const periodTitle = (() => { const d = new Date(`${day}T12:00:00`); if (view === 'month' || view === 'list') return d.toLocaleDateString(lang, { month: 'long', year: 'numeric' }); if (view === 'day') return d.toLocaleDateString(lang, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); const a = new Date(`${period.from.slice(0, 10)}T12:00:00`), b = new Date(`${move(period.to.slice(0, 10), -1)}T12:00:00`); return `${a.toLocaleDateString(lang, { day: 'numeric', month: 'short' })} – ${b.toLocaleDateString(lang, { day: 'numeric', month: 'short', year: 'numeric' })}`; })(); const doctorColor = id => doctors.find(d => d.id === id)?.color || 'rgb(var(--accent))'; const doctorShort = id => (doctors.find(d => d.id === id)?.name || '').split(' ').slice(0, 2).join(' '); const weekLabels = [t('calendar_mon'), t('calendar_tue'), t('calendar_wed'), t('calendar_thu'), t('calendar_fri'), t('calendar_sat'), t('calendar_sun')]; // Anterior/siguiente: un dia, una semana o un mes segun la vista. const step = n => { if (view === 'week') return move(day, 7 * n); if (view !== 'month') return move(day, n); const [y, m] = day.split('-').map(Number), d = new Date(Date.UTC(y, m - 1 + n, 1)); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-01`; }; const visible = appointments.filter(a => selectedDoctors.some(d => d.id === a.doctor_user_id) && statusFilter.includes(a.status)); const visibleBlocks = blocks.filter(b => selectedDoctors.some(d => d.id === b.doctor_user_id)); const renderEvent = (a, byDoctor = false) => ; const renderBlock = b => ; const detail = popup?.item ? popup.kind === 'appointment' ? [[t('clinic_doctor'), popup.item.doctor_name], [t('clinic_patient'), `${popup.item.first_names} ${popup.item.last_names}`], [t('clinic_type'), popup.item.type_name], [t('calendar_start'), popup.item.starts_at], [t('calendar_end'), popup.item.ends_at], [t('clinic_status'), t(`clinic_status_${popup.item.status}`)], [t('clinic_channel'), t(`clinic_channel_${popup.item.channel}`)], [t('clinic_notes'), popup.item.notes]] : popup.kind === 'patient' ? [[t('clinic_first_names'), popup.item.first_names], [t('clinic_last_names'), popup.item.last_names], [t('clinic_identification'), popup.item.identification], [t('clinic_phone'), popup.item.phone], [t('clinic_email'), popup.item.email], [t('clinic_birth_date'), popup.item.birth_date], [t('clinic_notes'), popup.item.notes]] : [[t('clinic_doctor'), doctors.find(d => d.id === popup.item.doctor_user_id)?.name], [t('clinic_kind'), t(`clinic_kind_${popup.item.kind}`)], [t('clinic_title'), popup.item.title], [t('calendar_start'), popup.item.occurrence_start], [t('calendar_end'), popup.item.occurrence_end]] : []; if (error && !me) return {error}; // Horas visibles: desde la primera hasta la ultima de atencion de los medicos mostrados, ampliado // con cualquier cita o bloqueo fuera de ese rango; sin horarios cargados, 07:00-19:00. const hourRange = (() => { let lo = 24, hi = 0; for (const d of selectedDoctors) for (const seg of d.schedules || []) { lo = Math.min(lo, Number(String(seg.starts_time).slice(0, 2))); hi = Math.max(hi, Math.ceil(Number(String(seg.ends_time).slice(0, 2)) + Number(String(seg.ends_time).slice(3, 5)) / 60)); } for (const x of [...visible.map(a => [a.starts_at, a.ends_at]), ...visibleBlocks.map(b => [b.occurrence_start, b.occurrence_end])]) { lo = Math.min(lo, Number(String(x[0]).slice(11, 13))); hi = Math.max(hi, Math.min(24, Math.ceil(Number(String(x[1]).slice(11, 13)) + Number(String(x[1]).slice(14, 16)) / 60))); } if (lo >= hi) [lo, hi] = [7, 19]; return Array.from({ length: hi - lo }, (_, i) => lo + i); })(); return

{t('nav_clinic')}

{isReception && tab === 'agenda' && }{isReception && tab === 'patients' && }{tab === 'blocks' && }
{error && {error}}{reprogram.length > 0 && {t('clinic_affected')}: {reprogram.map(a => `#${a.id} ${a.patient_name} · ${a.starts_at}`).join('; ')}} ({ key, label: t(label) }))} active={tab} onChange={key => { const current = popup ? draft : tab === 'config' ? [staffDraft, typeDraft, scheduleDraft].find(x => x.isDirty) : null; void guardedClose(current, () => setTab(key)); }} /> {tab === 'agenda' && <>
({ value: x, label: t(`clinic_view_${x}`) }))} /> ({ value: d.id, label: d.name }))} values={doctorFilter} onChange={setDoctorFilter} /> ({ value: x, label: t(`clinic_status_${x}`) }))} values={statusFilter} onChange={setStatusFilter} />
{me && doctors.length === 0 && {isAdmin ? t('clinic_no_doctors_admin') : t('clinic_no_doctors')}} {(view === 'week' || view === 'month') &&
{selectedDoctors.map(d => {d.name})}{t('clinic_blocks')}
} {view === 'day' ?
{selectedDoctors.map(d =>

{d.name}

{hourRange.map(h => { const hour = String(h).padStart(2, '0'), start = at(day, `${hour}:00`), end = plus(start, 60), weekday = (new Date(`${day}T12:00:00Z`).getUTCDay() + 6) % 7, working = d.schedules?.some(s => s.weekday === weekday && `${day} ${s.starts_time}` <= start && `${day} ${s.ends_time}` >= end); const blocked = visibleBlocks.some(b => b.doctor_user_id === d.id && b.occurrence_start < end && b.occurrence_end > start); return
working && !blocked && isReception && open('appointment', null, { doctorId: d.id, slot: start })}>{hour}:00{visible.filter(a => a.doctor_user_id === d.id && a.starts_at >= start && a.starts_at < end).map(renderEvent)}{visibleBlocks.filter(b => b.doctor_user_id === d.id && b.occurrence_start >= start && b.occurrence_start < end).map(renderBlock)}
; })}
)}
: view === 'week' ?
{Array.from({ length: 7 }, (_, i) => move(period.from.slice(0, 10), i)).map(d =>

{weekLabels[(new Date(`${d}T12:00:00Z`).getUTCDay() + 6) % 7]} {Number(d.slice(8, 10))}

{hourRange.map(h => { const start = at(d, `${String(h).padStart(2, '0')}:00`), end = plus(start, 60), weekday = (new Date(`${d}T12:00:00Z`).getUTCDay() + 6) % 7, // Con varios medicos: la hora es "de atencion" si alguno atiende; el sombreado de bloqueo // solo aplica cuando se mira un medico (con varios, cada bloqueo se ve como entrada). working = selectedDoctors.some(doc => doc.schedules?.some(seg => seg.weekday === weekday && `${d} ${seg.starts_time}` <= start && `${d} ${seg.ends_time}` >= end)), blocked = selectedDoctors.length === 1 && visibleBlocks.some(b => b.occurrence_start < end && b.occurrence_end > start); return
working && !blocked && isReception && open('appointment', null, { doctorId: selectedDoctors.length === 1 ? selectedDoctors[0].id : undefined, slot: start })}>{String(h).padStart(2, '0')}:00{visible.filter(a => a.starts_at >= start && a.starts_at < end).map(a => renderEvent(a, true))}{visibleBlocks.filter(b => b.occurrence_start >= start && b.occurrence_start < end).map(renderBlock)}
; })}
)}
: view === 'month' ? { setDay(d); setView('day'); }} renderDay={d => { const items = [...visibleBlocks.filter(b => b.occurrence_start.slice(0, 10) <= d && b.occurrence_end.slice(0, 10) >= d).map(b => ({ start: b.occurrence_start, node:
{doctorShort(b.doctor_user_id)} · {b.title}
})), ...visible.filter(a => a.starts_at.slice(0, 10) === d).map(a => ({ start: a.starts_at, node:
{a.starts_at.slice(11, 16)} {a.first_names} {a.last_names.slice(0, 1)}.
}))].sort((x, y) => x.start.localeCompare(y.start)); return <>{items.slice(0, 4).map(x => x.node)}{items.length > 4 &&
+{items.length - 4} {t('clinic_more')}
}; }} /> :
{[...visible.map(a => ({ start: a.starts_at, node: renderEvent(a) })), ...visibleBlocks.map(b => ({ start: b.occurrence_start, node: renderBlock(b) }))].sort((a, b) => a.start.localeCompare(b.start)).map((x, i) =>
{x.start.slice(0, 10)}{x.node}
)}
}} {tab === 'patients' && isReception && <> { setSearch(e.target.value); setPage(1); }} />
{patients.map(p => )}
{page} / {Math.max(1, Math.ceil(patientTotal / 20))}
} {tab === 'blocks' &&
{visibleBlocks.map(b =>
{renderBlock(b)}
)}
} {tab === 'config' && isAdmin &&

{t('clinic_staff')}

{ setStaffUser(x); const s = staffRows.find(r => r.user_id === Number(x)); setStaffInitial(s ? { scope: s.scope, isDoctor: !!s.is_doctor, specialty: s.specialty || '', color: s.color || '#0078d4', isActive: !!s.is_active, editVersion: s.edit_version } : { scope: 'own', isDoctor: true, specialty: '', color: '#0078d4', isActive: true }); }} options={[{ value: '', label: t('common_select') }, ...allUsers.filter(u => u.is_active).map(u => ({ value: u.id, label: u.name }))]} /> setStaffUser(e.target.value)} /> setStaffForm({ ...staffForm, scope: x })} options={['all', 'own'].map(x => ({ value: x, label: t(`clinic_scope_${x}`) }))} /> setStaffForm({ ...staffForm, specialty: e.target.value })} /> setStaffForm({ ...staffForm, color: e.target.value })} />

{t('clinic_types')}

{ const row = allTypes.find(r => r.id === Number(x)); setTypeInitial(row ? { id: row.id, name: row.name, durationMinutes: row.duration_minutes, color: row.color, isActive: !!row.is_active, editVersion: row.edit_version } : { name: '', durationMinutes: 30, color: '#0078d4', isActive: true }); }} options={[{ value: '', label: t('clinic_new_type') }, ...allTypes.map(x => ({ value: x.id, label: x.name }))]} /> setTypeForm({ ...typeForm, name: e.target.value })} /> setTypeForm({ ...typeForm, durationMinutes: Number(e.target.value) })} /> setTypeForm({ ...typeForm, color: e.target.value })} />

{t('clinic_schedules')}

{ setScheduleDoctor(x); setScheduleInitial({ rows: x ? await api.clinicSchedules(x) : [] }); }} options={[{ value: '', label: t('common_select') }, ...doctors.map(d => ({ value: d.id, label: d.name }))]} />{scheduleRows.map((s, i) =>
setScheduleRows(scheduleRows.map((r, j) => j === i ? { ...r, weekday: Number(x) } : r))} options={['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].map((x, n) => ({ value: n, label: t(`calendar_${x}`) }))} /> setScheduleRows(scheduleRows.map((r, j) => j === i ? { ...r, startsTime: e.target.value } : r))} /> setScheduleRows(scheduleRows.map((r, j) => j === i ? { ...r, endsTime: e.target.value } : r))} />
)}
} {popup &&
{conflict && {conflict}}{affected.length > 0 && {t('clinic_affected')}: {affected.map(a => `#${a.id} ${a.patient_name}`).join(', ')} }{editing && } {!editing ? <>
{detail.filter(([, val]) => val !== null && val !== undefined && val !== '').map(([label, val]) =>

{label}: {String(val)}

)}
{cancelMode && setCancelReason(e.target.value)} />}{cancelMode && }{popup.kind === 'appointment' &&
{isReception && <>}{['attended', 'no_show'].map(x => )}
}{popup.kind === 'patient' && isReception && <>

{t('clinic_history')}

{patientHistory.map(a =>

{a.starts_at} · {a.doctor_name} · {a.type_name} · {t(`clinic_status_${a.status}`)}

)}}{popup.kind === 'block' && <>} : <> {popup.kind === 'appointment' && <> { setSearch(e.target.value); api.clinicPatients(e.target.value, 1).then(x => setPatients(x.items)).catch(() => {}); }} /> draft.setField('patientId', x)} options={[{ value: '', label: t('common_select') }, ...patients.map(p => ({ value: p.id, label: `${p.first_names} ${p.last_names} · ${p.identification || p.phone}` }))]} /> draft.setField('doctorId', x)} options={doctors.map(d => ({ value: d.id, label: d.name }))} /> { const ty = types.find(z => z.id === Number(x)); draft.setValues(old => ({ ...old, typeId: x, endsAt: plus(old.startsAt, ty?.duration_minutes || 30) })); }} options={types.map(z => ({ value: z.id, label: z.name }))} /> draft.setValues(old => ({ ...old, startsAt: x, endsAt: plus(x, types.find(z => z.id === Number(old.typeId))?.duration_minutes || 30) }))} label={t('calendar_start')} /> draft.setField('endsAt', x)} label={t('calendar_end')} /> draft.setField('channel', x)} options={['phone', 'whatsapp', 'in_person', 'other'].map(x => ({ value: x, label: t(`clinic_channel_${x}`) }))} />