+ Portal público donde tus clientes agendan solos, sin usuario ni contraseña. + Mientras esté desactivado, nada cambia en el sistema ni se muestra ningún enlace. +
+{apt.time} · {apt.patient}
+{portalIcon(apt)} {apt.time} · {apt.patient}
{apt.service}{!isMedico && apt.doctor ? ` · ${apt.doctor}` : ''}
@@ -2230,6 +2236,7 @@ const Agenda: FC = () => {{brand.pie}
diff --git a/frontend/src/pages/Reservar.tsx b/frontend/src/pages/Reservar.tsx new file mode 100644 index 0000000..e4a3615 --- /dev/null +++ b/frontend/src/pages/Reservar.tsx @@ -0,0 +1,446 @@ +import type { FC, ReactNode } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + ArrowLeft, CalendarPlus, Check, ChevronRight, Clock, Copy, Loader2, MapPin, MessageCircle, User, +} from 'lucide-react'; +import { Button, Input, TextArea } from '../components/ui'; +import { odooApi, type BookingConfig, type BookingCreateResult } from '../services/odoo'; +import { + useBookingConfig, proximosDias, fechaISO, etiquetaDiaCorto, fechaLargaISO, construirIcs, + descargarIcs, normalizarTelefono, telefonoValido, +} from '../lib/publicBooking'; + +// Portal público de reservas en línea (sin sesión). Mobile-first: la mayoría +// de las clientas reservan del celular. Respeta brand y glosario del backend. + +type ServicioPortal = BookingConfig['servicios'][number]; +type GrupoServicios = { code: string; nombre: string; servicios: ServicioPortal[] }; + +const Reservar: FC = () => { + const navigate = useNavigate(); + const { config, loading } = useBookingConfig(); + + const [paso, setPaso] = useState(0); + const [servicioId, setServicioId] = useState+ Por el momento no puedes agendar desde aquí. Comunícate directamente con la clínica. +
+Folio {resultado.folio}
+{resultado.mensaje_confirmacion}
+ )} + {resultado.requiere_confirmacion && !resultado.mensaje_confirmacion && ( ++ Tu reserva quedó registrada y la clínica la confirmará en breve. +
+ )} +Guarda este enlace para gestionar tu reserva
+{config.config.descripcion}
+ )} +{g.nombre}
++ No hay horarios disponibles ese día. Prueba con otra fecha. +
+ ) : ( +{error}
} + + {config.config.telefono_contacto && ( +
+
{error}
} +{config.brand.nombre}
+{error || 'El enlace no es válido o ya expiró.'}
+ +Folio {cita.folio}
+ ++ Tu reserva quedó cancelada. Si fue un error, puedes agendar de nuevo. +
+ )} + + {!cancelada && cita.puede_cancelar && ( ++ ¿Seguro que quieres cancelar? Puedes dejarnos un motivo (opcional). +
+ setMotivo(e.target.value)} /> + {error &&{error}
} ++ Esta reserva ya no se puede cancelar en línea + {cita.permitir_cancelar ? ` (con menos de ${cita.cancelar_hasta_horas} h de anticipación)` : ''}. + {cita.telefono_contacto ? ` Comunícate al ${cita.telefono_contacto}.` : ' Comunícate con la clínica.'} +
+ )} +