// Puente entre este kit de UI (prototipo React sin backend) y la API real en PHP // (carpeta /reservas en la raíz del proyecto). Asume que, en producción, /reservas/ // queda servido en la raíz del mismo dominio que el sitio (ver reservas/README.md). const API_BASE = '/reservas/api'; // Todos los datos de contacto del estudio están en Colombia (ver CONTACT en data.jsx), // así que el formulario solo pide el número local y aquí se antepone el indicativo. const PHONE_COUNTRY_CODE = '+57'; // Clave PÚBLICA de reCAPTCHA v3 (protege el registro de cuentas nuevas). No es secreta — // está pensada para viajar en el frontend; la clave privada vive solo en el servidor // (RECAPTCHA_SECRET_KEY en config.php). Se obtienen juntas en // https://www.google.com/recaptcha/admin — reemplaza este valor por la clave real del sitio. const RECAPTCHA_SITE_KEY = '6Le1A38tAAAAAGx00dM4k3xwuOahayLzo9mtiYdW'; function soloDigitos(texto) { return (texto || '').replace(/\D/g, ''); } function telefonoLocalValido(texto) { return soloDigitos(texto).length === 10; } function telefonoE164(local) { return PHONE_COUNTRY_CODE + soloDigitos(local); } // Inverso de telefonoE164: para mostrar el teléfono guardado (con indicativo) en un // campo que solo pide el número local, como ya hace el registro. function telefonoSinIndicativo(telefono) { const texto = telefono || ''; return texto.startsWith(PHONE_COUNTRY_CODE) ? texto.slice(PHONE_COUNTRY_CODE.length) : soloDigitos(texto); } // Login y recuperar-contraseña piden "teléfono o correo" en un solo campo. Para que el // cliente no tenga que escribir el +57 ahí (solo lo pedimos explícito en el registro), // si lo que escribió no es un correo ni ya trae un '+', se asume número local de Colombia // y se le antepone el indicativo antes de mandarlo al backend (que sigue esperando E.164). function normalizarIdentificador(texto) { const valor = (texto || '').trim(); if (valor === '' || valor.includes('@')) return valor; if (valor.startsWith('+')) return '+' + soloDigitos(valor); const digitos = soloDigitos(valor); if (digitos.length === 10) return telefonoE164(digitos); // Alguien escribió el indicativo pero sin el '+' (ej. "57 317 812 3658") — se le antepone // el '+' en vez de tratarlo como número local, para no terminar mandando "+5757...". if (digitos.length === 12 && digitos.startsWith('57')) return '+' + digitos; return valor; } // Si /reservas/api todavía no está corriendo con PHP+MySQL (en local o en Hostinger), // el servidor responde con HTML/texto plano en vez de JSON. Sin este chequeo, resp.json() // lanza un SyntaxError críptico ("Unexpected non-whitespace character...") que terminaba // mostrándose tal cual en pantalla; aquí lo cambiamos por un mensaje que explica la causa real. async function leerJson(resp) { const texto = await resp.text(); try { return JSON.parse(texto); } catch (e) { throw new Error('No se pudo conectar con el servidor de reservas (/reservas/api). Verifica que esté desplegado con PHP y MySQL corriendo.'); } } async function fetchDisponibilidad(fecha, tipo) { // cache: 'no-store' es imprescindible: sin esto, un navegador que ya haya consultado esta // misma fecha/tipo antes (por ejemplo, cuando SÍ estaba disponible) puede seguir sirviendo // esa respuesta vieja desde su caché local aunque el admin bloquee el día después, incluso // haciendo recarga forzada (Ctrl+Shift+R) - eso solo afecta los recursos de la carga inicial, // no los fetch() que dispara React después. El servidor ya manda Cache-Control: no-store // (ver enviar_json en helpers.php), pero eso solo evita que se guarden respuestas NUEVAS; // no borra lo que un navegador ya haya guardado de antes de ese cambio. const resp = await fetch(`${API_BASE}/disponibilidad.php?fecha=${encodeURIComponent(fecha)}&tipo=${encodeURIComponent(tipo)}`, { cache: 'no-store' }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo consultar la disponibilidad.'); return datos; } async function crearReserva(payload) { const resp = await fetch(`${API_BASE}/reservar.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo completar la reserva.'); return datos; } // --- Cuentas de cliente (login/registro/tiquetera) --- // credentials: 'include' es lo que hace que la cookie de sesión de cliente // (pls_cliente_sesion, ver reservas/config.php) viaje en cada fetch; sin esto el // backend vería cada request como si no hubiera sesión, sin importar que ya se haya // iniciado sesión antes en la misma pestaña. // Pide un token de reCAPTCHA v3 para la acción indicada. grecaptcha (cargado en index.html // vía el script de Google) es invisible: no hay checkbox ni imagen que resolver, solo // evalúa comportamiento en segundo plano y devuelve un puntaje que el backend verifica // (ver verificar_recaptcha() en lib/helpers.php). function obtenerTokenRecaptcha(accion) { return new Promise((resolve, reject) => { if (typeof grecaptcha === 'undefined' || !grecaptcha.ready) { reject(new Error('No se pudo cargar la verificación anti-spam. Recarga la página e intenta de nuevo.')); return; } grecaptcha.ready(() => { grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: accion }).then(resolve).catch(() => { reject(new Error('No se pudo completar la verificación anti-spam. Recarga la página e intenta de nuevo.')); }); }); }); } async function registrarCliente({ nombre, telefono, correo, password, passwordConfirmacion, recaptchaToken }) { const resp = await fetch(`${API_BASE}/auth_registro.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre, telefono, correo, password, password_confirmacion: passwordConfirmacion, recaptcha_token: recaptchaToken }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo crear la cuenta.'); return datos.cliente; } async function iniciarSesion(identificador, password) { const resp = await fetch(`${API_BASE}/auth_login.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identificador, password }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo iniciar sesión.'); return datos.cliente; } async function cerrarSesion() { const resp = await fetch(`${API_BASE}/auth_logout.php`, { method: 'POST', credentials: 'include' }); await leerJson(resp); } // Devuelve null si no hay sesión activa (401 es el resultado normal de "no logueado", // no un error) y el payload completo de mi_cuenta.php si sí la hay. async function consultarSesion() { const resp = await fetch(`${API_BASE}/mi_cuenta.php`, { credentials: 'include', cache: 'no-store' }); if (resp.status === 401) return null; const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo consultar la cuenta.'); return datos; } async function cancelarReservaPropia(reservaId) { const resp = await fetch(`${API_BASE}/cancelar_reserva.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reserva_id: reservaId }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo cancelar la reserva.'); return datos; } async function unirseListaEspera({ fecha, hora, tipo }) { const resp = await fetch(`${API_BASE}/lista_espera.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fecha, hora, tipo }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo anotar en la lista de espera.'); return datos; } async function salirListaEspera(esperaId) { const resp = await fetch(`${API_BASE}/cancelar_espera.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ espera_id: esperaId }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo salir de la lista de espera.'); return datos; } async function solicitarRecuperarPassword(identificador) { const resp = await fetch(`${API_BASE}/recuperar_password.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identificador }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo procesar la solicitud.'); return datos; } async function restablecerPassword({ identificador, codigo, password, passwordConfirmacion }) { const resp = await fetch(`${API_BASE}/restablecer_password.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identificador, codigo, password, password_confirmacion: passwordConfirmacion }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'El código no es válido o ya expiró.'); return datos.cliente; } async function actualizarDatosCliente({ nombre, telefono, correo }) { const resp = await fetch(`${API_BASE}/actualizar_datos.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre, telefono, correo }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudieron guardar los cambios.'); return datos.cliente; } async function cambiarPasswordCliente({ passwordActual, passwordNueva, passwordConfirmacion }) { const resp = await fetch(`${API_BASE}/cambiar_password.php`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password_actual: passwordActual, password_nueva: passwordNueva, password_confirmacion: passwordConfirmacion }), }); const datos = await leerJson(resp); if (!resp.ok) throw new Error(datos.error || 'No se pudo cambiar la contraseña.'); return datos; } // Genera los próximos `dias` días (incluyendo hoy) como items {id: 'YYYY-MM-DD', label: 'LUN 12'} // listos para el componente Tabs, en vez de los siete días de la semana genéricos del mock. function proximosDias(dias = 7) { const nombres = ['DOM', 'LUN', 'MAR', 'MIÉ', 'JUE', 'VIE', 'SÁB']; const items = []; const hoy = new Date(); for (let i = 0; i < dias; i++) { const d = new Date(hoy); d.setDate(hoy.getDate() + i); // OJO: no usar d.toISOString() aquí. Convierte a UTC, así que para cualquier visitante // en Colombia (UTC-5) después de las 7pm el id quedaría un día adelantado respecto a la // fecha del calendario local (la pestaña diría "JUE 6" pero consultaría "2026-08-07"). // Con getFullYear/getMonth/getDate el id usa la fecha del calendario local del navegador, // igual que el label. const id = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; items.push({ id, label: `${nombres[d.getDay()]} ${d.getDate()}` }); } return items; } Object.assign(window, { API_BASE, PHONE_COUNTRY_CODE, RECAPTCHA_SITE_KEY, soloDigitos, telefonoLocalValido, telefonoE164, telefonoSinIndicativo, normalizarIdentificador, fetchDisponibilidad, crearReserva, proximosDias, obtenerTokenRecaptcha, registrarCliente, iniciarSesion, cerrarSesion, consultarSesion, cancelarReservaPropia, solicitarRecuperarPassword, restablecerPassword, unirseListaEspera, salirListaEspera, actualizarDatosCliente, cambiarPasswordCliente, });