|
|
|
@@ -135,8 +135,8 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
[conflict, setConflict] = useState(''),
|
|
|
|
|
[affected, setAffected] = useState([]),
|
|
|
|
|
[slots, setSlots] = useState([]),
|
|
|
|
|
[cancelReason, setCancelReason] = useState(''),
|
|
|
|
|
[cancelMode, setCancelMode] = useState(false),
|
|
|
|
|
[actionReason, setActionReason] = useState(''),
|
|
|
|
|
[actionMode, setActionMode] = useState(null),
|
|
|
|
|
[patientHistory, setPatientHistory] = useState([]),
|
|
|
|
|
[quickPatientId, setQuickPatientId] = useState(null),
|
|
|
|
|
[reprogram, setReprogram] = useState([]);
|
|
|
|
@@ -296,7 +296,8 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
setEditing(!item);
|
|
|
|
|
setConflict('');
|
|
|
|
|
setAffected([]);
|
|
|
|
|
setCancelMode(false);
|
|
|
|
|
setActionMode(null);
|
|
|
|
|
setActionReason('');
|
|
|
|
|
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(() => {});
|
|
|
|
|
}
|
|
|
|
@@ -362,25 +363,27 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
setConflict(e.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function cancelAppointment() {
|
|
|
|
|
if (!cancelReason.trim()) return;
|
|
|
|
|
// Cancelar o eliminar (borrado logico): ambos exigen motivo y quedan registrados.
|
|
|
|
|
function reasonAction() {
|
|
|
|
|
if (!actionReason.trim()) return;
|
|
|
|
|
const remove = actionMode === 'delete';
|
|
|
|
|
openDialog({
|
|
|
|
|
tone: 'warning',
|
|
|
|
|
title: t('clinic_cancel'),
|
|
|
|
|
message: t('clinic_cancel_confirm'),
|
|
|
|
|
title: t(remove ? 'clinic_delete' : 'clinic_cancel'),
|
|
|
|
|
message: t(remove ? 'clinic_delete_confirm' : 'clinic_cancel_confirm'),
|
|
|
|
|
actions: [{
|
|
|
|
|
label: t('common_cancel')
|
|
|
|
|
}, {
|
|
|
|
|
label: t('clinic_cancel'),
|
|
|
|
|
label: t(remove ? 'clinic_delete' : 'clinic_cancel'),
|
|
|
|
|
variant: 'danger',
|
|
|
|
|
onClick: async () => {
|
|
|
|
|
try {
|
|
|
|
|
await api.clinicCancelAppointment(popup.item.id, {
|
|
|
|
|
reason: cancelReason,
|
|
|
|
|
await (remove ? api.clinicDeleteAppointment : api.clinicCancelAppointment)(popup.item.id, {
|
|
|
|
|
reason: actionReason,
|
|
|
|
|
editVersion: popup.item.edit_version
|
|
|
|
|
});
|
|
|
|
|
setPopup(null);
|
|
|
|
|
setCancelMode(false);
|
|
|
|
|
setActionMode(null);
|
|
|
|
|
await load();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setConflict(e.message);
|
|
|
|
@@ -393,6 +396,16 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
const actions = [{
|
|
|
|
|
label: t('common_close')
|
|
|
|
|
}];
|
|
|
|
|
if (item.deleted_at) return open('appointment', item);
|
|
|
|
|
if (isReception && item.status === 'cancelled') actions.push({
|
|
|
|
|
label: t('clinic_delete'),
|
|
|
|
|
variant: 'danger',
|
|
|
|
|
onClick: () => {
|
|
|
|
|
open('appointment', item);
|
|
|
|
|
setActionMode('delete');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (item.status === 'cancelled') return openDialog({ title: `${item.first_names} ${item.last_names}`, message: item.starts_at, actions });
|
|
|
|
|
if (isReception) {
|
|
|
|
|
actions.push({
|
|
|
|
|
label: t('clinic_confirm'),
|
|
|
|
@@ -413,7 +426,15 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
variant: 'danger',
|
|
|
|
|
onClick: () => {
|
|
|
|
|
open('appointment', item);
|
|
|
|
|
setCancelMode(true);
|
|
|
|
|
setActionMode('cancel');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
actions.push({
|
|
|
|
|
label: t('clinic_delete'),
|
|
|
|
|
variant: 'danger',
|
|
|
|
|
onClick: () => {
|
|
|
|
|
open('appointment', item);
|
|
|
|
|
setActionMode('delete');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -447,16 +468,39 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
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 statusOf = a => a.deleted_at ? 'deleted' : a.status;
|
|
|
|
|
const visible = appointments.filter(a => selectedDoctors.some(d => d.id === a.doctor_user_id) && statusFilter.includes(statusOf(a)));
|
|
|
|
|
// Quien agendo: fijo desde la creacion; si fue el agente de WhatsApp dice "Agente IA".
|
|
|
|
|
const author = a => a.created_by_kind === 'ai_agent' ? t('clinic_ai_agent') : a.created_by_name || '—';
|
|
|
|
|
// Clic en un dia/hora libre: cita nueva a la primera hora de atencion de ese dia (medico preelegido
|
|
|
|
|
// si se mira uno solo). Sin permiso de agendar, el clic solo abre ese dia.
|
|
|
|
|
function newAt(d, slot) {
|
|
|
|
|
if (!isReception || !doctors.length) {
|
|
|
|
|
setDay(d);
|
|
|
|
|
setView('day');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const weekday = (new Date(`${d}T12:00:00Z`).getUTCDay() + 6) % 7;
|
|
|
|
|
const first = selectedDoctors.flatMap(doc => (doc.schedules || []).filter(seg => seg.weekday === weekday).map(seg => String(seg.starts_time).slice(0, 5))).sort()[0];
|
|
|
|
|
open('appointment', null, {
|
|
|
|
|
doctorId: selectedDoctors.length === 1 ? selectedDoctors[0].id : undefined,
|
|
|
|
|
slot: slot || at(d, first || '09:00')
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const openItem = (e, kind, item) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
open(kind, item);
|
|
|
|
|
};
|
|
|
|
|
const visibleBlocks = blocks.filter(b => selectedDoctors.some(d => d.id === b.doctor_user_id));
|
|
|
|
|
const renderEvent = (a, byDoctor = false) => <button key={`a-${a.id}`} className="clinic-event appointment" style={{
|
|
|
|
|
const renderEvent = (a, byDoctor = false) => <button key={`a-${a.id}`} className={`clinic-event appointment status-${statusOf(a)}`} style={{
|
|
|
|
|
borderLeftColor: byDoctor ? doctorColor(a.doctor_user_id) : a.type_color
|
|
|
|
|
}} onClick={() => open('appointment', a)} onContextMenu={e => {
|
|
|
|
|
}} onClick={e => openItem(e, 'appointment', a)} onContextMenu={e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
appointmentMenu(a);
|
|
|
|
|
}} title={`${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names} · ${a.doctor_name} · ${a.type_name} · ${t(`clinic_status_${a.status}`)}`}>{byDoctor ? `${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names.slice(0, 1)}. · ${a.type_name}` : `${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names} · ${a.doctor_name} · ${a.type_name} · ${t(`clinic_status_${a.status}`)}`}</button>;
|
|
|
|
|
const renderBlock = b => <button key={`b-${b.id}-${b.occurrence_start}`} className="clinic-event block" onClick={() => open('block', b)} title={b.title}>{b.occurrence_start.slice(11, 16)} {doctors.find(d => d.id === b.doctor_user_id)?.name} · {b.title}</button>;
|
|
|
|
|
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]] : [];
|
|
|
|
|
}} title={`${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names} · ${a.doctor_name} · ${a.type_name} · ${t(`clinic_status_${statusOf(a)}`)}`}>{byDoctor ? `${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names.slice(0, 1)}. · ${a.type_name}` : `${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names} · ${a.doctor_name} · ${a.type_name} · ${t(`clinic_status_${statusOf(a)}`)}`}</button>;
|
|
|
|
|
const renderBlock = b => <button key={`b-${b.id}-${b.occurrence_start}`} className="clinic-event block" onClick={e => openItem(e, 'block', b)} title={b.title}>{b.occurrence_start.slice(11, 16)} {doctors.find(d => d.id === b.doctor_user_id)?.name} · {b.title}</button>;
|
|
|
|
|
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_created_by'), `${author(popup.item)} · ${String(popup.item.created_at || '').slice(0, 16)}`], [t('clinic_notes'), popup.item.notes], [t('clinic_cancelled_by'), popup.item.cancelled_at && `${popup.item.cancelled_by_name || '—'} · ${popup.item.cancelled_at.slice(0, 16)} · ${popup.item.cancelled_reason || ''}`], [t('clinic_deleted_by'), popup.item.deleted_at && `${popup.item.deleted_by_name || '—'} · ${popup.item.deleted_at.slice(0, 16)} · ${popup.item.deleted_reason || ''}`]] : 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], [t('clinic_created_by'), popup.item.created_by_name]] : [];
|
|
|
|
|
if (error && !me) return <InfoBar tone="error">{error}</InfoBar>;
|
|
|
|
|
// 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.
|
|
|
|
@@ -489,7 +533,7 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
}))} /><CheckListDropdown label={t('clinic_doctors')} options={doctors.map(d => ({
|
|
|
|
|
value: d.id,
|
|
|
|
|
label: d.name
|
|
|
|
|
}))} values={doctorFilter} onChange={setDoctorFilter} /><CheckListDropdown label={t('clinic_status')} options={['scheduled', 'confirmed', 'attended', 'no_show'].map(x => ({
|
|
|
|
|
}))} values={doctorFilter} onChange={setDoctorFilter} /><CheckListDropdown label={t('clinic_status')} options={['scheduled', 'confirmed', 'attended', 'no_show', 'cancelled', 'deleted'].map(x => ({
|
|
|
|
|
value: x,
|
|
|
|
|
label: t(`clinic_status_${x}`)
|
|
|
|
|
}))} values={statusFilter} onChange={setStatusFilter} /><DatePicker value={day} onChange={setDay} label={t('calendar_date')} /></div></div>
|
|
|
|
@@ -507,7 +551,7 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
return <div key={hour} className={`clinic-hour ${working ? '' : 'closed'}${blocked ? ' blocked' : ''}`} onClick={() => working && !blocked && isReception && open('appointment', null, {
|
|
|
|
|
doctorId: d.id,
|
|
|
|
|
slot: start
|
|
|
|
|
})}><small>{hour}:00</small>{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)}</div>;
|
|
|
|
|
})} title={working && !blocked && isReception ? t('clinic_new_appointment') : undefined}><small>{hour}:00</small>{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)}</div>;
|
|
|
|
|
})}</div>)}</div> : view === 'week' ? <div className="clinic-week-grid">{Array.from({
|
|
|
|
|
length: 7
|
|
|
|
|
}, (_, i) => move(period.from.slice(0, 10), i)).map(d => <div className="clinic-doctor" key={d}><h3>{weekLabels[(new Date(`${d}T12:00:00Z`).getUTCDay() + 6) % 7]} {Number(d.slice(8, 10))}</h3>{hourRange.map(h => {
|
|
|
|
@@ -518,14 +562,11 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
// 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 <div className={`clinic-hour ${working ? '' : 'closed'}${blocked ? ' blocked' : ''}`} key={h} onClick={() => working && !blocked && isReception && open('appointment', null, {
|
|
|
|
|
doctorId: selectedDoctors.length === 1 ? selectedDoctors[0].id : undefined,
|
|
|
|
|
slot: start
|
|
|
|
|
})}><small>{String(h).padStart(2, '0')}:00</small>{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)}</div>;
|
|
|
|
|
})}</div>)}</div> : view === 'month' ? <MonthGrid day={day} weekLabels={weekLabels} todayKey={today()} onDayClick={d => { 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: <div key={`b-${b.id}-${b.occurrence_start}`} className="clinic-chip block" style={{ borderLeftColor: doctorColor(b.doctor_user_id) }} title={b.title}>{doctorShort(b.doctor_user_id)} · {b.title}</div> })),
|
|
|
|
|
...visible.filter(a => a.starts_at.slice(0, 10) === d).map(a => ({ start: a.starts_at, node: <div key={`a-${a.id}`} className="clinic-chip" style={{ borderLeftColor: doctorColor(a.doctor_user_id) }} title={`${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names} · ${a.doctor_name}`}>{a.starts_at.slice(11, 16)} {a.first_names} {a.last_names.slice(0, 1)}.</div> }))].sort((x, y) => x.start.localeCompare(y.start));
|
|
|
|
|
return <>{items.slice(0, 4).map(x => x.node)}{items.length > 4 && <div className="text-muted" style={{ fontSize: '.66rem' }}>+{items.length - 4} {t('clinic_more')}</div>}</>;
|
|
|
|
|
return <div className={`clinic-hour ${working ? '' : 'closed'}${blocked ? ' blocked' : ''}`} key={h} onClick={() => working && !blocked && newAt(d, start)} title={working && !blocked && isReception ? t('clinic_new_appointment') : undefined}><small>{String(h).padStart(2, '0')}:00</small>{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)}</div>;
|
|
|
|
|
})}</div>)}</div> : view === 'month' ? <MonthGrid day={day} weekLabels={weekLabels} todayKey={today()} onDayClick={d => newAt(d)} 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: <button type="button" key={`b-${b.id}-${b.occurrence_start}`} className="clinic-chip block" style={{ borderLeftColor: doctorColor(b.doctor_user_id) }} title={b.title} onClick={e => openItem(e, 'block', b)}>{doctorShort(b.doctor_user_id)} · {b.title}</button> })),
|
|
|
|
|
...visible.filter(a => a.starts_at.slice(0, 10) === d).map(a => ({ start: a.starts_at, node: <button type="button" key={`a-${a.id}`} className={`clinic-chip status-${statusOf(a)}`} style={{ borderLeftColor: doctorColor(a.doctor_user_id) }} title={`${a.starts_at.slice(11, 16)} ${a.first_names} ${a.last_names} · ${a.doctor_name} · ${t(`clinic_status_${statusOf(a)}`)}`} onClick={e => openItem(e, 'appointment', a)} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); appointmentMenu(a); }}>{a.starts_at.slice(11, 16)} {a.first_names} {a.last_names.slice(0, 1)}.</button> }))].sort((x, y) => x.start.localeCompare(y.start));
|
|
|
|
|
return <>{items.slice(0, 4).map(x => x.node)}{items.length > 4 && <button type="button" className="clinic-more" onClick={e => { e.stopPropagation(); setDay(d); setView('day'); }}>+{items.length - 4} {t('clinic_more')}</button>}</>;
|
|
|
|
|
}} /> : <div className="clinic-list">{[...visible.map(a => ({
|
|
|
|
|
start: a.starts_at,
|
|
|
|
|
node: renderEvent(a)
|
|
|
|
@@ -707,7 +748,7 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
setError(e.message);
|
|
|
|
|
}
|
|
|
|
|
}}>{t('common_save')}</Button></div>}
|
|
|
|
|
<DetailPopup open={!!popup} title={t(`clinic_${popup?.kind || 'appointment'}`)} onClose={close} width={760} height={680}>{popup && <div className="clinic-form">{conflict && <InfoBar tone="error">{conflict}</InfoBar>}{affected.length > 0 && <InfoBar tone="warning">{t('clinic_affected')}: {affected.map(a => `#${a.id} ${a.patient_name}`).join(', ')} <Button onClick={() => void save(true)}>{t('clinic_confirm_affected')}</Button></InfoBar>}{editing && <DraftNotice draft={draft} />} {!editing ? <><div className="clinic-detail">{detail.filter(([, val]) => val !== null && val !== undefined && val !== '').map(([label, val]) => <p key={label}><strong>{label}:</strong> {String(val)}</p>)}</div>{cancelMode && <Field label={t('clinic_cancel_reason')}><input className="input" value={cancelReason} onChange={e => setCancelReason(e.target.value)} /></Field>}{cancelMode && <Button variant="danger" disabled={!cancelReason.trim()} onClick={cancelAppointment}>{t('clinic_cancel')}</Button>}{popup.kind === 'appointment' && <div className="toolbar-left">{isReception && <><Button onClick={() => setEditing(true)}>{t('common_edit')}</Button><Button onClick={() => setEditing(true)}>{t('clinic_reprogram')}</Button><Button disabled={popup.item.status === 'confirmed'} onClick={() => void actionStatus('confirmed')}>{t('clinic_confirm')}</Button><Button onClick={() => setCancelMode(true)}>{t('clinic_cancel')}</Button></>}{['attended', 'no_show'].map(x => <Button key={x} disabled={popup.item.status === x} onClick={() => void actionStatus(x)}>{t(`clinic_status_${x}`)}</Button>)}</div>}{popup.kind === 'patient' && isReception && <><Button onClick={() => setEditing(true)}>{t('common_edit')}</Button><h3>{t('clinic_history')}</h3>{patientHistory.map(a => <p key={a.id}>{a.starts_at} · {a.doctor_name} · {a.type_name} · {t(`clinic_status_${a.status}`)}</p>)}</>}{popup.kind === 'block' && <><Button onClick={() => setEditing(true)}>{t('common_edit')}</Button><Button variant="danger" onClick={() => openDialog({
|
|
|
|
|
<DetailPopup open={!!popup} title={t(`clinic_${popup?.kind || 'appointment'}`)} onClose={close} width={760} height={680}>{popup && <div className="clinic-form">{conflict && <InfoBar tone="error">{conflict}</InfoBar>}{affected.length > 0 && <InfoBar tone="warning">{t('clinic_affected')}: {affected.map(a => `#${a.id} ${a.patient_name}`).join(', ')} <Button onClick={() => void save(true)}>{t('clinic_confirm_affected')}</Button></InfoBar>}{editing && <DraftNotice draft={draft} />} {!editing ? <><div className="clinic-detail">{detail.filter(([, val]) => val !== null && val !== undefined && val !== '').map(([label, val]) => <p key={label}><strong>{label}:</strong> {String(val)}</p>)}</div>{popup.kind === 'appointment' && popup.item.deleted_at && <InfoBar tone="warning">{t('clinic_deleted_notice')}</InfoBar>}{actionMode && <><Field label={t(actionMode === 'delete' ? 'clinic_delete_reason' : 'clinic_cancel_reason')}><input className="input" value={actionReason} onChange={e => setActionReason(e.target.value)} autoFocus /></Field><div className="toolbar-left"><Button variant="danger" disabled={!actionReason.trim()} onClick={reasonAction}>{t(actionMode === 'delete' ? 'clinic_delete' : 'clinic_cancel')}</Button><Button onClick={() => { setActionMode(null); setActionReason(''); }}>{t('common_cancel')}</Button></div></>}{popup.kind === 'appointment' && !popup.item.deleted_at && !actionMode && <div className="toolbar-left">{isReception && popup.item.status !== 'cancelled' && <><Button variant="accent" onClick={() => setEditing(true)}>{t('common_edit')}</Button><Button disabled={popup.item.status === 'confirmed'} onClick={() => void actionStatus('confirmed')}>{t('clinic_confirm')}</Button><Button onClick={() => setActionMode('cancel')}>{t('clinic_cancel')}</Button></>}{popup.item.status !== 'cancelled' && ['attended', 'no_show'].map(x => <Button key={x} disabled={popup.item.status === x} onClick={() => void actionStatus(x)}>{t(`clinic_status_${x}`)}</Button>)}{isReception && <Button variant="danger" onClick={() => setActionMode('delete')}>{t('clinic_delete')}</Button>}</div>}{popup.kind === 'patient' && isReception && <><Button onClick={() => setEditing(true)}>{t('common_edit')}</Button><h3>{t('clinic_history')}</h3>{patientHistory.map(a => <p key={a.id} className={a.deleted_at ? 'clinic-deleted' : undefined}>{a.starts_at} · {a.doctor_name} · {a.type_name} · {t(`clinic_status_${statusOf(a)}`)} · {t('clinic_created_by')}: {author(a)}{a.deleted_at ? ` · ${t('clinic_deleted_by')}: ${a.deleted_by_name || '—'} (${a.deleted_reason || ''})` : ''}</p>)}</>}{popup.kind === 'block' && <><Button onClick={() => setEditing(true)}>{t('common_edit')}</Button><Button variant="danger" onClick={() => openDialog({
|
|
|
|
|
tone: 'warning',
|
|
|
|
|
title: t('clinic_delete_block'),
|
|
|
|
|
message: t('clinic_delete_block_confirm'),
|
|
|
|
@@ -730,13 +771,13 @@ export default function AgendaMedicaPage() {
|
|
|
|
|
{popup.kind === 'appointment' && <><Field label={t('clinic_search_patient')}><input className="input" value={search} onChange={e => {
|
|
|
|
|
setSearch(e.target.value);
|
|
|
|
|
api.clinicPatients(e.target.value, 1).then(x => setPatients(x.items)).catch(() => {});
|
|
|
|
|
}} /></Field><ComboBox value={v.patientId} onChange={x => draft.setField('patientId', x)} options={[{
|
|
|
|
|
}} /></Field><Field label={t('clinic_patient')}><ComboBox value={v.patientId} onChange={x => 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}`
|
|
|
|
|
}))]} /><Button onClick={() => open('patient', null, {
|
|
|
|
|
}))]} /></Field><Button onClick={() => open('patient', null, {
|
|
|
|
|
returnTo: popup
|
|
|
|
|
})}>{t('clinic_new_patient')}</Button><Field label={t('clinic_doctor')}><ComboBox value={v.doctorId} onChange={x => draft.setField('doctorId', x)} options={doctors.map(d => ({
|
|
|
|
|
value: d.id,
|
|
|
|
|