707 lines
31 KiB
HTML
707 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MAY Studio — Peluquería en Barcelona</title>
|
|
<meta name="description" content="MAY Studio, peluquería en Horta, Barcelona. Cortes, color, peinados y tratamientos para señoras, caballeros y niños.">
|
|
<link rel="icon" href="/favicon.ico" sizes="any">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="/style.css">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- NAV -->
|
|
<nav class="nav">
|
|
<div class="container nav-inner">
|
|
<a href="#" class="logo">MAY <span>Studio</span></a>
|
|
<ul class="nav-links">
|
|
<li><a href="#profesional">La profesional</a></li>
|
|
<li><a href="#galeria">Nuestro trabajo</a></li>
|
|
<li><a href="#reservar">Reservar</a></li>
|
|
<li><a href="#contacto">Contacto</a></li>
|
|
</ul>
|
|
<a href="tel:+34933333333" class="btn btn-primary btn-nav">Llamar</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- HERO -->
|
|
<header class="hero">
|
|
<div class="hero-bg">
|
|
<img src="/assets/hero.jpg" alt="Interior del salón MAY Studio">
|
|
</div>
|
|
<div class="container hero-content">
|
|
<h1>MAY Studio<br>Tu peluquería en Horta</h1>
|
|
<p class="hero-sub">Cortes, color, peinados y tratamientos capilares para toda la familia. Un espacio pensado para ti.</p>
|
|
<div class="hero-actions">
|
|
<a href="#reservar" class="btn btn-primary">Reservar cita</a>
|
|
<a href="#galeria" class="btn btn-secondary">Ver nuestro trabajo</a>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- PROFESIONAL (Mayra) -->
|
|
<section id="profesional" class="section">
|
|
<div class="container">
|
|
<div class="profesional-grid">
|
|
<div class="profesional-img">
|
|
<img src="/assets/may.jpg" alt="Mayra, peluquera profesional en MAY Studio">
|
|
</div>
|
|
<div class="profesional-info">
|
|
<h3>Mayra</h3>
|
|
<p class="profesional-role">Peluquera & fundadora</p>
|
|
<p>Soy Mayra, fundadora de MAY Studio, un espacio dedicado al cuidado del cabello con un enfoque personalizado.</p>
|
|
<p>Estoy especializada en color, mechas y cortes, buscando siempre resultados naturales, equilibrados y elegantes.</p>
|
|
<p>Cada cliente recibe una atención individualizada, analizando su cabello y su estilo para crear un resultado que sea bonito, favorecedor y fácil de mantener día a día.</p>
|
|
<p>MAY Studio nace con la idea de ofrecer un ambiente tranquilo donde el cuidado del cabello y la atención al detalle son lo más importante.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- GALERÍA / Nuestro trabajo -->
|
|
<section id="galeria" class="section section-alt">
|
|
<div class="container">
|
|
<h2 class="section-title">Nuestro trabajo</h2>
|
|
<p class="section-subtitle">Explora algunos de los estilos y transformaciones que creamos en MAY Studio. Haz clic en cualquier imagen para ampliar.</p>
|
|
<div class="gallery-grid">
|
|
<div class="gallery-item"><img src="/assets/galeria-1.jpg" alt="Trabajo de peluquería"></div>
|
|
<div class="gallery-item"><img src="/assets/galeria-2.jpg" alt="Trabajo de peluquería"></div>
|
|
<div class="gallery-item"><img src="/assets/galeria-3.jpg" alt="Trabajo de peluquería"></div>
|
|
<div class="gallery-item"><img src="/assets/galeria-4.jpg" alt="Trabajo de peluquería"></div>
|
|
<div class="gallery-item"><img src="/assets/galeria-5.jpg" alt="Trabajo de peluquería"></div>
|
|
<div class="gallery-item"><img src="/assets/galeria-6.jpg" alt="Trabajo de peluquería"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- RESERVAR -->
|
|
<section id="reservar" class="section">
|
|
<div class="container">
|
|
<h2 class="section-title">Reservar cita</h2>
|
|
<p class="section-subtitle">Elige uno o varios servicios, día y hora para tu cita</p>
|
|
|
|
<!-- Horario integrado -->
|
|
<div style="max-width: 500px; margin: 0 auto 32px;">
|
|
<h3 style="font-family: 'DM Serif Display', serif; font-size: 1.1rem; text-align: center; margin-bottom: 12px;">Horario</h3>
|
|
<div class="schedule">
|
|
<div class="schedule-row">
|
|
<span>Lunes a Viernes</span>
|
|
<span>9:30 — 13:30 / 16:30 — 20:30</span>
|
|
</div>
|
|
<div class="schedule-row">
|
|
<span>Sábados</span>
|
|
<span>9:00 — 14:00</span>
|
|
</div>
|
|
<div class="schedule-row closed">
|
|
<span>Domingos y festivos</span>
|
|
<span>Cerrado</span>
|
|
</div>
|
|
</div>
|
|
<p class="schedule-note">Llámanos o reserva online. Te atendemos con o sin cita.</p>
|
|
</div>
|
|
|
|
<!-- Service selector -->
|
|
<div class="service-selector" id="service-selector">
|
|
<p class="services-loading">Cargando servicios…</p>
|
|
</div>
|
|
<div class="service-summary-bar" id="service-summary">
|
|
<span id="summary-names"></span>
|
|
<span class="summary-sep">·</span>
|
|
<span>Duración: <strong id="summary-dur"></strong></span>
|
|
</div>
|
|
|
|
<div class="booking-calendar">
|
|
<!-- Left: calendar -->
|
|
<div class="cal-left">
|
|
<div class="cal-header">
|
|
<button type="button" class="cal-nav" id="cal-prev">←</button>
|
|
<span class="cal-month" id="cal-month"></span>
|
|
<button type="button" class="cal-nav" id="cal-next">→</button>
|
|
</div>
|
|
<div class="cal-weekdays">
|
|
<span>Lu</span><span>Ma</span><span>Mi</span><span>Ju</span><span>Vi</span><span>Sá</span><span>Do</span>
|
|
</div>
|
|
<div class="cal-grid" id="cal-grid"></div>
|
|
</div>
|
|
<!-- Right: slots + contact form -->
|
|
<div class="cal-right">
|
|
<div id="slots-placeholder" class="slots-placeholder">Selecciona primero el/los servicio(s) que deseas</div>
|
|
<div id="slots-container" style="display:none">
|
|
<h3 class="slots-title" id="slots-title"></h3>
|
|
<div class="slots-grid" id="slots-grid"></div>
|
|
</div>
|
|
<form id="booking-form" class="booking-mini-form" style="display:none" novalidate>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="nombre">Nombre *</label>
|
|
<input type="text" id="nombre" name="nombre" autocomplete="given-name" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="apellidos">Apellidos *</label>
|
|
<input type="text" id="apellidos" name="apellidos" autocomplete="family-name" required>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="email">Email *</label>
|
|
<input type="email" id="email" name="email" autocomplete="email" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="telefono">Teléfono *</label>
|
|
<input type="tel" id="telefono" name="telefono" autocomplete="tel" required>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mensaje">Mensaje (opcional)</label>
|
|
<textarea id="mensaje" name="mensaje" rows="2" placeholder="Algo que debamos saber…"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" id="booking-submit">Confirmar reserva</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div id="booking-result" class="booking-result" style="display:none"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- CONTACTO -->
|
|
<section id="contacto" class="section section-alt">
|
|
<div class="container">
|
|
<h2 class="section-title">Encuéntranos</h2>
|
|
<div class="contact-grid">
|
|
<div class="contact-info">
|
|
<div class="contact-item">
|
|
<strong>Dirección</strong>
|
|
<p>Carrer d'Horta, 12<br>08032 Barcelona (Horta)</p>
|
|
</div>
|
|
<div class="contact-item">
|
|
<strong>Teléfono</strong>
|
|
<p><a href="tel:+34933333333">933 333 333</a></p>
|
|
</div>
|
|
<div class="contact-item">
|
|
<strong>Cómo llegar</strong>
|
|
<p>Metro L5 — Horta<br>Bus V19, 45, 102</p>
|
|
</div>
|
|
</div>
|
|
<div class="contact-map">
|
|
<iframe
|
|
src="https://www.openstreetmap.org/export/embed.html?bbox=2.1550%2C41.4380%2C2.1650%2C41.4430&layer=mapnik&marker=41.4405%2C2.1600"
|
|
width="100%" height="300" style="border:0; border-radius: 12px;"
|
|
loading="lazy" title="Mapa MAY Studio">
|
|
</iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- FOOTER -->
|
|
<footer class="footer">
|
|
<div class="container footer-inner">
|
|
<span class="logo">MAY <span>Studio</span></span>
|
|
<p>© 2026 MAY Studio. Tu peluquería en Horta.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
function esc(s) {
|
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// === EasyAppointments config ===
|
|
// IMPORTANTE: Después de configurar EasyAppointments, ve al admin (http://localhost:8888)
|
|
// y crea al menos un Provider. Anota su ID y ponlo aquí.
|
|
const PROVIDER_ID = 5;
|
|
|
|
// ---- Horario desde EasyAppointments (Settings → Business, company_working_plan) ----
|
|
// Si la petición falla se mantiene el horario estático del HTML.
|
|
async function loadSchedule() {
|
|
try {
|
|
const res = await fetch('/ea-api/v1/settings/company_working_plan');
|
|
if (!res.ok) return;
|
|
const setting = await res.json();
|
|
const plan = JSON.parse(setting.value);
|
|
|
|
const DAYS = [
|
|
['monday', 'Lunes'], ['tuesday', 'Martes'], ['wednesday', 'Miércoles'],
|
|
['thursday', 'Jueves'], ['friday', 'Viernes'], ['saturday', 'Sábado'], ['sunday', 'Domingo']
|
|
];
|
|
const fmt = t => t.replace(/^0/, '');
|
|
// "09:00-18:00 con pausa 14:30-15:00" → "9:00 — 14:30 / 15:00 — 18:00"
|
|
const dayLabel = p => {
|
|
if (!p || !p.start || !p.end) return null; // cerrado
|
|
const breaks = (p.breaks || []).slice().sort((a, b) => a.start.localeCompare(b.start));
|
|
const segs = [];
|
|
let cur = p.start;
|
|
for (const b of breaks) {
|
|
if (b.start > cur) segs.push(fmt(cur) + ' — ' + fmt(b.start));
|
|
if (b.end > cur) cur = b.end;
|
|
}
|
|
if (cur < p.end) segs.push(fmt(cur) + ' — ' + fmt(p.end));
|
|
return segs.length ? segs.join(' / ') : null;
|
|
};
|
|
|
|
// Agrupa días consecutivos con el mismo horario.
|
|
const groups = [];
|
|
for (const [key, nombre] of DAYS) {
|
|
const label = dayLabel(plan[key]);
|
|
const last = groups[groups.length - 1];
|
|
if (last && last.label === label) last.dias.push(nombre);
|
|
else groups.push({ label, dias: [nombre] });
|
|
}
|
|
const rows = groups.map(g => {
|
|
const dias = g.dias.length === 1 ? g.dias[0]
|
|
: g.dias.length === 2 ? g.dias[0] + ' y ' + g.dias[1]
|
|
: g.dias[0] + ' a ' + g.dias[g.dias.length - 1];
|
|
return g.label
|
|
? `<div class="schedule-row"><span>${esc(dias)}</span><span>${esc(g.label)}</span></div>`
|
|
: `<div class="schedule-row closed"><span>${esc(dias)}</span><span>Cerrado</span></div>`;
|
|
}).join('');
|
|
if (rows) document.querySelector('.schedule').innerHTML = rows;
|
|
} catch (e) {
|
|
console.warn('No se pudo cargar el horario desde EA; se muestra el estático.');
|
|
}
|
|
}
|
|
|
|
// ---- Service selector state ----
|
|
let selectedServices = []; // [{id, nombre, duracion}]
|
|
let totalDuration = 0;
|
|
|
|
// ---- Categories and Services from EasyAppointments ----
|
|
async function loadCategories() {
|
|
try {
|
|
let res = await fetch('/ea-api/v1/categories');
|
|
if (!res.ok) {
|
|
res = await fetch('/ea-api/v1/service_categories');
|
|
}
|
|
if (!res.ok) return [];
|
|
return await res.json();
|
|
} catch (e) {
|
|
console.warn('No se pudieron cargar las categorías de EA');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function loadServices() {
|
|
const selectorContainer = document.getElementById('service-selector');
|
|
try {
|
|
const [servicesRes, categories] = await Promise.all([
|
|
fetch('/ea-api/v1/services'),
|
|
loadCategories()
|
|
]);
|
|
if (!servicesRes.ok) throw new Error('HTTP ' + servicesRes.status);
|
|
const services = await servicesRes.json();
|
|
|
|
// Filter services to those assigned to this provider
|
|
let displayServices = services;
|
|
try {
|
|
const provRes = await fetch(`/ea-api/v1/providers/${PROVIDER_ID}`);
|
|
if (provRes.ok) {
|
|
const prov = await provRes.json();
|
|
const allowed = (prov.services || prov.service_ids || []).map(id => parseInt(id));
|
|
if (allowed.length > 0) {
|
|
displayServices = services.filter(s => allowed.includes(parseInt(s.id)));
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
|
|
const catMap = {};
|
|
(categories || []).forEach(cat => {
|
|
catMap[cat.id] = cat;
|
|
});
|
|
|
|
const grouped = {};
|
|
displayServices.forEach(s => {
|
|
const catId = s.categoryId ?? s.serviceCategoryId ?? 'uncat';
|
|
if (!grouped[catId]) {
|
|
const catInfo = catMap[catId] || {};
|
|
grouped[catId] = {
|
|
nombre: catInfo.name || catInfo.title || 'Otros servicios',
|
|
servicios: []
|
|
};
|
|
}
|
|
grouped[catId].servicios.push({
|
|
id: s.id,
|
|
nombre: s.name,
|
|
precio: (parseFloat(s.price) || 0) + " €",
|
|
duracion: s.duration || 30
|
|
});
|
|
});
|
|
|
|
let categorias = Object.values(grouped);
|
|
// Sort categories alphabetically for consistent order
|
|
categorias.sort((a, b) => a.nombre.localeCompare(b.nombre, 'es'));
|
|
|
|
// Sort services inside each category
|
|
categorias.forEach(cat => {
|
|
cat.servicios.sort((a, b) => a.nombre.localeCompare(b.nombre, 'es'));
|
|
});
|
|
|
|
if (categorias.length === 0 || displayServices.length === 0) {
|
|
selectorContainer.innerHTML = '<p class="services-loading">No hay servicios asignados para este proveedor o categorías.</p>';
|
|
return;
|
|
}
|
|
|
|
renderServiceSelector(categorias);
|
|
} catch (e) {
|
|
console.error(e);
|
|
if (selectorContainer) selectorContainer.innerHTML = '<p class="services-loading">No se pudieron cargar los servicios. Inténtalo más tarde o llámanos.</p>';
|
|
}
|
|
}
|
|
|
|
function renderServiceSelector(categorias) {
|
|
const container = document.getElementById('service-selector');
|
|
container.innerHTML = categorias.map(cat => `
|
|
<div class="svc-cat-card" data-cat="${esc(cat.nombre)}">
|
|
<h4 class="svc-cat-title">${esc(cat.nombre)}</h4>
|
|
<div class="svc-options">
|
|
${cat.servicios.map(s => `
|
|
<label class="svc-option">
|
|
<input type="checkbox"
|
|
value="${esc(s.nombre)}"
|
|
data-cat="${esc(cat.nombre)}"
|
|
data-duracion="${s.duracion || 30}"
|
|
data-id="${s.id || ''}">
|
|
<span class="svc-name">${esc(s.nombre)}</span>
|
|
<span class="svc-meta">${s.duracion || 30} min · ${esc(s.precio)}</span>
|
|
</label>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Listener se añade una vez desde loadServices
|
|
if (!container.dataset.listenerAttached) {
|
|
container.addEventListener('change', handleServiceChange);
|
|
container.dataset.listenerAttached = 'true';
|
|
}
|
|
}
|
|
|
|
function handleServiceChange(e) {
|
|
if (e.target.type !== 'checkbox') return;
|
|
|
|
// Rebuild selected services from all checked boxes (incluye id de EA).
|
|
const checked = document.querySelectorAll('#service-selector input[type=checkbox]:checked');
|
|
selectedServices = [];
|
|
totalDuration = 0;
|
|
checked.forEach(cb => {
|
|
selectedServices.push({
|
|
id: parseInt(cb.dataset.id) || null,
|
|
nombre: cb.value,
|
|
duracion: parseInt(cb.dataset.duracion) || 30
|
|
});
|
|
totalDuration += parseInt(cb.dataset.duracion) || 30;
|
|
});
|
|
|
|
// No lock to category anymore - services from different categories can be selected
|
|
updateServiceSummary();
|
|
|
|
if (selectedDate) {
|
|
selectedHora = null;
|
|
document.getElementById('booking-form').style.display = 'none';
|
|
const parts = selectedDate.split('-');
|
|
const dow = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])).getDay();
|
|
renderSlots(selectedDate, dow);
|
|
} else {
|
|
const ph = document.getElementById('slots-placeholder');
|
|
ph.textContent = selectedServices.length === 0
|
|
? 'Selecciona primero el/los servicio(s) que deseas'
|
|
: 'Selecciona un día en el calendario';
|
|
}
|
|
}
|
|
|
|
function updateServiceSummary() {
|
|
const bar = document.getElementById('service-summary');
|
|
if (selectedServices.length === 0) { bar.style.display = 'none'; return; }
|
|
bar.style.display = 'flex';
|
|
document.getElementById('summary-names').textContent = selectedServices.map(s => s.nombre).join(' + ');
|
|
const h = Math.floor(totalDuration / 60);
|
|
const m = totalDuration % 60;
|
|
document.getElementById('summary-dur').textContent =
|
|
h > 0 ? (m > 0 ? h + ' h ' + m + ' min' : h + ' h') : m + ' min';
|
|
}
|
|
|
|
// ---- Calendar state (usando availabilities de EasyAppointments) ----
|
|
let calYear, calMonth;
|
|
let selectedDate = null;
|
|
let selectedHora = null;
|
|
let availableSlotsForDate = []; // array de "HH:mm" devueltos por EA
|
|
|
|
const MONTH_NAMES = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
|
|
|
|
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
|
function fmtDate(y, m, d) { return y + '-' + pad(m + 1) + '-' + pad(d); }
|
|
|
|
function timeToMinutes(hora) {
|
|
const [h, m] = hora.split(':').map(Number);
|
|
return h * 60 + m;
|
|
}
|
|
|
|
function minutesToTime(mins) {
|
|
const h = Math.floor(mins / 60);
|
|
const m = mins % 60;
|
|
return pad(h) + ':' + pad(m);
|
|
}
|
|
|
|
// Comprueba si desde 'startHora' hay un bloque consecutivo libre de 'neededMinutes'
|
|
// usando la lista de starts devueltos por EA.
|
|
function canFitFullBlock(startHora, neededMinutes, availableList) {
|
|
if (!startHora || neededMinutes <= 0 || !availableList || availableList.length === 0) return false;
|
|
const startM = timeToMinutes(startHora);
|
|
const step = 30;
|
|
const numSteps = Math.max(1, Math.ceil(neededMinutes / step));
|
|
for (let i = 0; i < numSteps; i++) {
|
|
const checkM = startM + i * step;
|
|
const checkStr = minutesToTime(checkM);
|
|
if (!availableList.includes(checkStr)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Obtiene el serviceId principal para consultar disponibilidad (el de mayor duración o el primero)
|
|
function getPrimaryServiceId() {
|
|
if (!selectedServices.length) return null;
|
|
// Prioriza el de mayor duración
|
|
const sorted = [...selectedServices].sort((a, b) => (b.duracion || 0) - (a.duracion || 0));
|
|
return sorted[0].id;
|
|
}
|
|
|
|
function renderCalendar() {
|
|
document.getElementById('cal-month').textContent = MONTH_NAMES[calMonth] + ' ' + calYear;
|
|
|
|
const grid = document.getElementById('cal-grid');
|
|
grid.innerHTML = '';
|
|
|
|
const firstDay = new Date(calYear, calMonth, 1);
|
|
let startDow = firstDay.getDay();
|
|
startDow = startDow === 0 ? 6 : startDow - 1;
|
|
|
|
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
|
|
const today = new Date();
|
|
today.setHours(0,0,0,0);
|
|
|
|
for (let i = 0; i < startDow; i++) {
|
|
const empty = document.createElement('span');
|
|
empty.className = 'cal-day cal-empty';
|
|
grid.appendChild(empty);
|
|
}
|
|
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
const date = new Date(calYear, calMonth, d);
|
|
const dateStr = fmtDate(calYear, calMonth, d);
|
|
const dow = date.getDay();
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.textContent = d;
|
|
btn.className = 'cal-day';
|
|
if (date < today || dow === 0) { btn.disabled = true; btn.classList.add('cal-disabled'); }
|
|
if (dateStr === selectedDate) btn.classList.add('cal-selected');
|
|
btn.addEventListener('click', () => selectDay(dateStr, dow));
|
|
grid.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function selectDay(dateStr, dow) {
|
|
selectedDate = dateStr;
|
|
selectedHora = null;
|
|
document.getElementById('booking-form').style.display = 'none';
|
|
renderCalendar();
|
|
renderSlots(dateStr, dow);
|
|
}
|
|
|
|
// Versión simplificada usando EasyAppointments
|
|
async function renderSlots(dateStr, dow) {
|
|
const container = document.getElementById('slots-container');
|
|
const placeholder = document.getElementById('slots-placeholder');
|
|
const grid = document.getElementById('slots-grid');
|
|
const title = document.getElementById('slots-title');
|
|
|
|
if (selectedServices.length === 0) {
|
|
placeholder.textContent = 'Selecciona primero el servicio que deseas';
|
|
placeholder.style.display = '';
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const primaryServiceId = getPrimaryServiceId();
|
|
if (!primaryServiceId) {
|
|
placeholder.textContent = 'Error seleccionando servicio principal';
|
|
placeholder.style.display = '';
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
placeholder.style.display = 'none';
|
|
container.style.display = '';
|
|
|
|
const [y, m, d] = dateStr.split('-');
|
|
const dayNames = ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'];
|
|
const dateObj = new Date(parseInt(y), parseInt(m) - 1, parseInt(d));
|
|
title.textContent = dayNames[dateObj.getDay()] + ' ' + parseInt(d) + ' de ' + MONTH_NAMES[parseInt(m) - 1];
|
|
|
|
try {
|
|
const url = `/ea-api/v1/availabilities?providerId=${PROVIDER_ID}&serviceId=${primaryServiceId}&date=${dateStr}`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(await res.text());
|
|
availableSlotsForDate = await res.json();
|
|
} catch (e) {
|
|
console.error('Error al obtener availabilities de EA:', e);
|
|
availableSlotsForDate = [];
|
|
}
|
|
|
|
if (!availableSlotsForDate || availableSlotsForDate.length === 0) {
|
|
placeholder.textContent = 'No hay disponibilidad este día para el servicio seleccionado';
|
|
placeholder.style.display = '';
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Filtrado clave para multiservicio:
|
|
// Usamos la lista de EA (basada en el servicio más largo) pero solo mostramos
|
|
// aquellos starts desde los que cabe un bloque completo de 'totalDuration' minutos.
|
|
const filteredSlots = availableSlotsForDate.filter(hora =>
|
|
canFitFullBlock(hora, totalDuration, availableSlotsForDate)
|
|
);
|
|
|
|
if (filteredSlots.length === 0) {
|
|
placeholder.textContent = 'No hay bloques libres de ' + totalDuration + ' minutos';
|
|
placeholder.style.display = '';
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = filteredSlots.map(hora => {
|
|
const isSel = hora === selectedHora;
|
|
const cls = 'slot-btn' + (isSel ? ' slot-selected' : '');
|
|
return `<button type="button" class="${cls}" data-hora="${esc(hora)}">${esc(hora)}</button>`;
|
|
}).join('');
|
|
|
|
grid.querySelectorAll('.slot-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
selectedHora = btn.dataset.hora;
|
|
renderSlots(dateStr, dow);
|
|
document.getElementById('booking-form').style.display = '';
|
|
});
|
|
});
|
|
}
|
|
|
|
function initCalendar() {
|
|
const now = new Date();
|
|
calYear = now.getFullYear();
|
|
calMonth = now.getMonth();
|
|
|
|
document.getElementById('cal-prev').addEventListener('click', () => {
|
|
const now = new Date();
|
|
if (calYear === now.getFullYear() && calMonth === now.getMonth()) return;
|
|
calMonth--;
|
|
if (calMonth < 0) { calMonth = 11; calYear--; }
|
|
renderCalendar();
|
|
hideSlots();
|
|
});
|
|
|
|
document.getElementById('cal-next').addEventListener('click', () => {
|
|
calMonth++;
|
|
if (calMonth > 11) { calMonth = 0; calYear++; }
|
|
renderCalendar();
|
|
hideSlots();
|
|
});
|
|
|
|
renderCalendar();
|
|
}
|
|
|
|
function hideSlots() {
|
|
selectedDate = null;
|
|
selectedHora = null;
|
|
document.getElementById('slots-container').style.display = 'none';
|
|
document.getElementById('slots-placeholder').style.display = '';
|
|
document.getElementById('slots-placeholder').textContent = selectedServices.length === 0
|
|
? 'Selecciona primero el servicio que deseas'
|
|
: 'Selecciona un día en el calendario';
|
|
document.getElementById('booking-form').style.display = 'none';
|
|
}
|
|
|
|
function initBookingForm() {
|
|
document.getElementById('booking-form').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const submit = document.getElementById('booking-submit');
|
|
const result = document.getElementById('booking-result');
|
|
|
|
if (selectedServices.length === 0) { alert('Selecciona al menos un servicio'); return; }
|
|
if (!selectedDate || !selectedHora) { alert('Selecciona día y hora'); return; }
|
|
|
|
const nombre = form.nombre.value.trim();
|
|
const apellidos = form.apellidos.value.trim();
|
|
// EasyAppointments exige nombre y apellido al crear el cliente.
|
|
if (!nombre || !apellidos) { alert('Escribe tu nombre y tus apellidos'); return; }
|
|
|
|
const email = form.email.value.trim();
|
|
if (!email) { alert('El email es obligatorio'); return; }
|
|
|
|
submit.disabled = true;
|
|
submit.textContent = 'Enviando…';
|
|
result.style.display = 'none';
|
|
|
|
try {
|
|
// El servidor (server-side) busca/crea el cliente y crea la cita
|
|
// contra EasyAppointments. El navegador solo envía la selección.
|
|
const payload = {
|
|
nombre: nombre,
|
|
apellidos: apellidos,
|
|
email: email,
|
|
telefono: form.telefono.value.trim(),
|
|
mensaje: form.mensaje.value.trim(),
|
|
providerId: PROVIDER_ID,
|
|
serviceIds: selectedServices.map(s => s.id).filter(Boolean),
|
|
date: selectedDate,
|
|
time: selectedHora
|
|
};
|
|
|
|
const res = await fetch('/api/book', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (res.ok) {
|
|
form.reset();
|
|
selectedHora = null;
|
|
// Refrescar slots
|
|
const savedDate = selectedDate;
|
|
const savedDow = new Date(parseInt(savedDate.split('-')[0]), parseInt(savedDate.split('-')[1])-1, parseInt(savedDate.split('-')[2])).getDay();
|
|
renderSlots(savedDate, savedDow);
|
|
form.style.display = 'none';
|
|
result.className = 'booking-result booking-success';
|
|
result.textContent = '¡Reserva confirmada! Te esperamos.';
|
|
} else {
|
|
const err = await res.json().catch(() => ({}));
|
|
result.className = 'booking-result booking-error';
|
|
result.textContent = err.error || 'No se pudo crear la reserva. Inténtalo de nuevo o llámanos.';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
result.className = 'booking-result booking-error';
|
|
result.textContent = 'No se pudo completar la reserva. Inténtalo de nuevo o llámanos.';
|
|
}
|
|
|
|
result.style.display = 'block';
|
|
submit.disabled = false;
|
|
submit.textContent = 'Confirmar reserva';
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadSchedule();
|
|
loadServices();
|
|
initCalendar();
|
|
initBookingForm();
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|