import { useEffect, useState } from 'react'; import { odooApi, type BookingConfig } from '../services/odoo'; // Utilidades y hook compartidos del portal público de reservas en línea. // El portal es anónimo: solo usa los endpoints /public/booking/*, que // responden 404 cuando el módulo está desactivado. /** * Config del portal público. Devuelve `config=null` cuando está desactivado * (404) o el backend no responde: en ese caso el portal se muestra como "no * disponible" y no se ofrece ningún enlace. */ export const useBookingConfig = () => { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let alive = true; odooApi.getBookingConfig() .then((res) => { if (alive && res && res.activo) setConfig(res); }) .catch(() => { if (alive) setConfig(null); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, []); return { config, loading, active: !!config }; }; const DIAS = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado']; const DIAS_ABREV = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']; const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']; /** 'YYYY-MM-DD' local de un Date (sin cruzar a UTC). */ export const fechaISO = (d: Date): string => { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; }; /** Próximos `max` días desde hoy (inclusive), para el selector de fecha. */ export const proximosDias = (max: number, tope = 30): Date[] => { const n = Math.max(1, Math.min(max || 1, tope)); const hoy = new Date(); hoy.setHours(0, 0, 0, 0); return Array.from({ length: n }, (_, i) => new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() + i)); }; export const etiquetaDiaCorto = (d: Date): { abrev: string; dia: string } => ({ abrev: DIAS_ABREV[d.getDay()], dia: String(d.getDate()), }); export const fechaLarga = (d: Date): string => `${DIAS[d.getDay()]} ${d.getDate()} de ${MESES[d.getMonth()]}`; export const fechaLargaISO = (iso: string): string => { const [y, m, day] = iso.split('-').map(Number); return fechaLarga(new Date(y, (m || 1) - 1, day || 1)); }; /** Evento .ics (hora local flotante) para "agregar al calendario". */ export const construirIcs = (opts: { folio: string; fecha: string; // YYYY-MM-DD hora: string; // HH:MM duracionMin: number; titulo: string; descripcion?: string; lugar?: string; }): string => { const [y, m, d] = opts.fecha.split('-').map(Number); const [hh, mm] = opts.hora.split(':').map(Number); const fmt = (dt: Date) => `${dt.getFullYear()}${String(dt.getMonth() + 1).padStart(2, '0')}${String(dt.getDate()).padStart(2, '0')}` + `T${String(dt.getHours()).padStart(2, '0')}${String(dt.getMinutes()).padStart(2, '0')}00`; const inicio = new Date(y, (m || 1) - 1, d || 1, hh || 0, mm || 0); const fin = new Date(inicio.getTime() + Math.max(opts.duracionMin || 30, 5) * 60000); const esc = (s: string) => String(s || '').replace(/([,;\\])/g, '\\$1').replace(/\n/g, '\\n'); return [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//SKEEN//Reservas en linea//ES', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'BEGIN:VEVENT', `UID:${esc(opts.folio)}@skeen`, `DTSTAMP:${fmt(new Date())}`, `DTSTART:${fmt(inicio)}`, `DTEND:${fmt(fin)}`, `SUMMARY:${esc(opts.titulo)}`, opts.lugar ? `LOCATION:${esc(opts.lugar)}` : '', opts.descripcion ? `DESCRIPTION:${esc(opts.descripcion)}` : '', 'END:VEVENT', 'END:VCALENDAR', ].filter(Boolean).join('\r\n'); }; export const descargarIcs = (nombreArchivo: string, contenido: string) => { const blob = new Blob([contenido], { type: 'text/calendar;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = nombreArchivo.endsWith('.ics') ? nombreArchivo : `${nombreArchivo}.ics`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); }; /** URL base pública de la clínica (para el enlace /reservar compartible). */ export const urlBasePublica = (urlPublica?: string): string => { const base = (urlPublica || '').trim(); if (base) return base.replace(/\/+$/, ''); if (typeof window !== 'undefined') return window.location.origin; return ''; }; export const enlaceReserva = (urlPublica?: string): string => `${urlBasePublica(urlPublica)}/reservar`; /** Formatea el teléfono a solo dígitos (con + inicial si venía). */ export const normalizarTelefono = (valor: string): string => { const raw = (valor || '').trim(); const digitos = raw.replace(/\D/g, ''); return raw.startsWith('+') && digitos ? `+${digitos}` : digitos; }; export const telefonoValido = (valor: string): boolean => { const digitos = normalizarTelefono(valor).replace(/\D/g, ''); return digitos.length >= 10 && digitos.length <= 15; };