diff --git a/frontend/src/pages/Agenda.tsx b/frontend/src/pages/Agenda.tsx index c44f3ba..f08da21 100644 --- a/frontend/src/pages/Agenda.tsx +++ b/frontend/src/pages/Agenda.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react'; import { useEffect, useState, useCallback } from 'react'; -import { Clock, CheckCircle, XCircle, CheckCheck, UserCheck, UserX, Plus, PackageCheck, Sparkles, RotateCcw, LayoutGrid, List, Rows3, Edit2, Ban, Trash2 } from 'lucide-react'; +import { Clock, CheckCircle, XCircle, CheckCheck, UserCheck, UserX, Plus, PackageCheck, Sparkles, RotateCcw, LayoutGrid, List, Rows3, Edit2, Ban, Trash2, Phone } from 'lucide-react'; import Layout from '../components/Layout'; import { Card, @@ -59,7 +59,7 @@ const Agenda: FC = () => { const [stateFilter, setStateFilter] = useState(''); const [doctorFilter, setDoctorFilter] = useState(() => (medicoEmployeeId ? String(medicoEmployeeId) : '')); const [viewMode, setViewMode] = useState<'lista' | 'doctores' | 'horas'>('doctores'); - const [quickFilter, setQuickFilter] = useState<'' | 'libres' | 'primera' | 'checkin' | 'noshow'>(''); + const [quickFilter, setQuickFilter] = useState<'' | 'libres' | 'primera' | 'checkin' | 'noshow' | 'porconfirmar'>(''); const [detailApt, setDetailApt] = useState(null); const [editMode, setEditMode] = useState(false); const [editForm, setEditForm] = useState({ date: '', time: '', doctor_id: '', notes: '' }); @@ -136,17 +136,52 @@ const Agenda: FC = () => { if (a.state !== 'confirmed' || a.date !== todayStr || start === null) return false; return start + (a.duration ?? 30) / 60 <= nowFloat; }; + // Sin 1ra confirmación telefónica (misma regla que pending_confirm del API) + const isPendingConfirm = (a: Appointment): boolean => + !a.phone_confirmed && !['cancelled', 'no_show', 'done'].includes(a.state); const visibleAppointments = filteredAppointments.filter((apt) => { if (quickFilter === 'primera' && !apt.is_first_visit) return false; if (quickFilter === 'checkin' && !isCheckinDue(apt)) return false; if (quickFilter === 'noshow' && !isNoShowDue(apt)) return false; + if (quickFilter === 'porconfirmar' && !isPendingConfirm(apt)) return false; return true; }); const countPrimera = filteredAppointments.filter((a) => a.is_first_visit).length; const countCheckin = filteredAppointments.filter(isCheckinDue).length; const countNoShow = filteredAppointments.filter(isNoShowDue).length; + const countPorConfirmar = filteredAppointments.filter(isPendingConfirm).length; + + // Indicador de doble confirmación: sin confirmar (gris) / teléfono (azul) / en clínica (verde) + const confirmIndicator = (apt: Appointment, size = 12) => { + if (['arrived', 'in_progress', 'done'].includes(apt.state)) { + return ( + + ); + } + if (apt.phone_confirmed) { + return ( + + ); + } + return ( + + ); + }; + + const confirmTitle = (apt: Appointment): string => { + if (['arrived', 'in_progress', 'done'].includes(apt.state)) return '2da confirmación: llegó a la clínica'; + if (apt.phone_confirmed) { + const quien = apt.phone_confirmed_by ? ` por ${apt.phone_confirmed_by}` : ''; + const cuando = apt.phone_confirmed_at ? ` el ${apt.phone_confirmed_at}` : ''; + return `1ra confirmación (teléfono)${quien}${cuando}`; + } + return 'Sin confirmar por teléfono'; + }; // ---- Vista por médico (grid doctores x bloques de 15 min) ---- const GRID_START = 8; // horario clínica: 8:00 @@ -444,6 +479,21 @@ const Agenda: FC = () => { in_progress: 'En curso', done: 'Completada', cancelled: 'Cancelada', no_show: 'No show', }; + const handlePhoneConfirm = async (apt: Appointment, confirmed: boolean) => { + try { + setUpdating(apt.id); + const res = await odooApi.phoneConfirm(apt.id, confirmed); + toast.success(confirmed ? 'Cita confirmada por teléfono' : 'Confirmación telefónica retirada'); + if (res.status === 'success' && detailApt?.id === apt.id) setDetailApt(res.appointment); + await load(); + } catch (err) { + toast.error('Error al actualizar la confirmación telefónica'); + console.error(err); + } finally { + setUpdating(null); + } + }; + const handlePackageFinished = async (id: number, finished: boolean) => { try { setPkgUpdating(id); @@ -630,6 +680,7 @@ const Agenda: FC = () => { Servicio Doctor Estado + Conf. Pago Acciones @@ -862,6 +913,7 @@ const Agenda: FC = () => { { key: 'primera', label: 'Primera vez', count: countPrimera }, { key: 'checkin', label: 'Check-in', count: countCheckin }, { key: 'noshow', label: 'No show', count: countNoShow }, + { key: 'porconfirmar', label: 'Por confirmar', count: countPorConfirmar }, ] as const).map((chip) => ( + {apt.payment_state} @@ -1143,6 +1208,7 @@ const Agenda: FC = () => { { label: 'Hora', value: apt.time }, { label: 'Doctor', value: apt.doctor || '-' }, { label: 'Estado', value: {apt.state} }, + { label: 'Confirmación', value: {confirmIndicator(apt, 14)} }, { label: 'Pago', value: apt.payment_state }, ]} actions={} @@ -1525,17 +1591,52 @@ const Agenda: FC = () => {

{detailApt.notes}

)} + {/* Doble confirmación: 1ra teléfono, 2da llegada (check-in) */} +
+

Confirmaciones

+
+
+

1ra confirmación (teléfono)

+

+ {detailApt.phone_confirmed + ? `Confirmada${detailApt.phone_confirmed_by ? ` por ${detailApt.phone_confirmed_by}` : ''}${detailApt.phone_confirmed_at ? ` · ${detailApt.phone_confirmed_at}` : ''}` + : 'Sin confirmar'} +

+
+ {!['cancelled', 'no_show', 'done'].includes(detailApt.state) && ( + + )} +
+
+
+

2da confirmación (llegada)

+

+ {['arrived', 'in_progress', 'done'].includes(detailApt.state) + ? 'Paciente en clínica (check-in realizado)' + : 'Pendiente de llegada'} +

+
+ {detailApt.state === 'confirmed' && ( + + )} +
+
{detailApt.state === 'pending' && ( )} - {detailApt.state === 'confirmed' && ( - - )} {(detailApt.state === 'confirmed' || detailApt.state === 'arrived') && (