GOLIATH Logo

З чого почнемо?

📅 Обрати дату
💇 Обрати майстра

Оберіть дату

Оберіть послугу

Оберіть майстра

Оберіть час

Деталі запису

майстер Майстер:
дата Дата та час:
послуга Послуга:
ціна Всього:

Заповніть ваші дані

+380

Ваш запис створено

майстер Майстер:
послуги Послуги:
дата Дата та час:
ціна Загальна вартість:
адреса Адреса: Рівне, вул. В'ячеслава Чорновола 17
телефон Телефон: +380980001163

Слідкуйте за нами:

Оберіть майстра

Оберіть послугу

Оберіть дату

Деталі запису

майстер Майстер:
дата Дата та час:
послуга Послуга:
ціна Всього:

Заповніть ваші дані

+380

Ваш запис створено

майстер Майстер:
послуги Послуги:
дата Дата та час:
ціна Загальна вартість:
адреса Адреса: Рівне, вул. В'ячеслава Чорновола 17
телефон Телефон: +380980001163

Слідкуйте за нами:

// config const COMPANY_ID = 'b5db5c5f-bf00-11ec-95e3-e7f0639bde2f'; const BRANCH_ID = 'b5dbaa94-bf00-11ec-95e3-e7f0639bde2f'; const ADDON_CATEGORY_ID = 'a3914a16-c168-11ec-95e3-e7f0639bde2f'; let selectedResourceId = '', selectedAddonId = '', selectedDate = '', selectedTime = null; let selectedServiceIds = []; let availableResources = []; let selectedDate_DateFirst = ''; let selectedService_DateFirst = ''; let selectedMaster_DateFirst = ''; let selectedServiceId_DateFirst = ''; let selectedPrice_DateFirst = 0; let selectedDuration_DateFirst = 0; let cachedMastersWithService = null; let cachedSelectedDate_DateFirst = null; let cachedSelectedService_DateFirst = null; let allServices = [], serviceDurations = {}, addonServices = {}, categories = [], availableDates = [], selectedResource = null; let currentMonth = new Date().getMonth(), currentYear = new Date().getFullYear(); const ENABLE_RESOURCE_FILTER = true; const BLOCKED_RESOURCE_IDS = [ '778685b1-b8fb-11ed-bf71-fd595ad03df8', '8b83cb05-c020-11ed-8ec1-c976bd51801a', 'ad7bb5f4-af9f-11ed-bbf9-1d4fc2a19592', 'a2ec72a9-e021-11ec-b900-4de806f031a9', '4f1a03b2-6488-11f0-a815-9352f320a6c1' ]; // Під вибір дати першим const EXCLUDED_SERVICE_NAMES = ["Завивка волосся"]; let COMMON_SERVICE_ID = "a3914a17-c168-11ec-95e3-e7f0639bde2f" let DATE_FIRST_TIME_DURATION = 900 // utils functions function sendToDataLayer(eventName, params) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: eventName, ...params }); } function parseUtmParams() { const params = new URLSearchParams(window.location.search); return { utm_source: params.get("utm_source") || '', utm_medium: params.get("utm_medium") || '', utm_campaign: params.get("utm_campaign") || '', utm_term: params.get("utm_term") || '', utm_content: params.get("utm_content") || '' }; } function returnToStart() { document.querySelectorAll('.step').forEach(s => s.classList.remove('active')); document.getElementById('mode-step').classList.add('active'); } // barber first function startByMaster() { document.querySelectorAll('.step').forEach(s => s.classList.remove('active')); document.getElementById('barber-step-1').classList.add('active'); loadResources(); } function goToStepBarber(n) { if (n === 2 && !selectedResourceId) return; if (n === 3 && selectedServiceIds.length === 0) return; if (n === 4 && (!selectedDate || !selectedTime)) return; document.querySelectorAll('.step').forEach(s => s.classList.remove('active')); document.getElementById(`barber-step-${n}`).classList.add('active'); if (n === 4) updateSummary(); } function loadResources() { fetch(`https://api.wlaunch.net/v1/public/company/${COMPANY_ID}/branch/${BRANCH_ID}/resource?active=true&public=true`, { headers: { /*Authorization: `Bearer ${API_TOKEN}` */} }) .then(res => res.json()) .then(data => { const container = document.getElementById('masters-list'); container.innerHTML = ''; let resources = data.content; if (ENABLE_RESOURCE_FILTER) { resources = resources.filter(r => !BLOCKED_RESOURCE_IDS.includes(r.id)); } resources.sort((a, b) => a.ordinal - b.ordinal).forEach(r => { const div = document.createElement('div'); div.className = 'master-card'; const rating = r.rating ? Math.round(r.rating / 20) : 0; const stars = '⭐'.repeat(rating || 0); const photo = document.createElement('img'); photo.src = r.logo || ''; photo.alt = r.name; photo.style.width = '100%'; photo.style.borderRadius = '8px'; div.innerHTML = `
${r.name}
${stars || '—'}
`; div.prepend(photo); const ratingEl = div.querySelector('.clickable-rating'); ratingEl.style.cursor = 'pointer'; ratingEl.onclick = (e) => { e.stopPropagation(); let modal = document.getElementById('reviews-modal'); if (modal) modal.remove(); modal = document.createElement('div'); modal.id = 'reviews-modal'; modal.style.cssText = `position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:9999;`; const inner = document.createElement('div'); inner.style.cssText = `background:#111;color:#fff;padding:20px;border-radius:10px;width:90%;max-width:400px;max-height:80vh;overflow-y:auto;position:relative;`; const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = `position:absolute;top:10px;right:10px;background:none;color:#fff;border:none;font-size:20px;cursor:pointer;`; closeBtn.onclick = () => modal.remove(); inner.appendChild(closeBtn); const content = document.createElement('div'); content.innerHTML = '⏳ Завантаження відгуків...'; inner.appendChild(content); modal.appendChild(inner); document.body.appendChild(modal); fetch(`https://api.wlaunch.net/v1/company/${COMPANY_ID}/branch/${BRANCH_ID}/resource/${r.id}/rating?sort=created%2Cdesc&page=0&size=5&populate=true`, { headers: { /*Authorization: `Bearer ${API_TOKEN}` */} }) .then(res => res.json()) .then(data => { if (!data?.content?.length) { content.innerHTML = '💬 Відгуків ще немає'; return; } content.innerHTML = data.content.map(rev => { const name = rev.client?.first_name || 'Анонім'; const rating = rev.rating || '—'; const date = new Date(rev.created).toLocaleDateString('uk-UA', { day: 'numeric', month: 'long', year: 'numeric' }); const msg = rev.message?.trim() || '(без коментаря)'; return `
⭐ ${rating} — ${name}
${date}
${msg}
`; }).join(''); }) .catch(err => { console.error('❌ Помилка завантаження відгуків:', err); content.innerHTML = '❌ Не вдалося завантажити відгуки'; }); }; div.onclick = () => { selectedResourceId = r.id; selectedResource = r; selectedDate = ''; selectedTime = null; selectedAddonId = ''; selectedServiceIds = []; serviceDurations = {}; addonServices = {}; availableDates = []; const calendar = document.getElementById('calendar'); if (calendar) calendar.innerHTML = ''; const timeSlots = document.getElementById('time-slots'); if (timeSlots) timeSlots.innerHTML = ''; const noTimeMsg = document.getElementById('no-time-msg'); if (noTimeMsg) noTimeMsg.style.display = 'none'; document.querySelectorAll('.master-card').forEach(el => el.classList.remove('selected')); div.classList.add('selected'); goToStepBarber(2); sendToDataLayer('resource_select', { master_name: r.name, master_id: r.id }); loadServicesForResource(); }; container.appendChild(div); }); }); } function loadServicesForResource() { const todayISO = new Date().toISOString(); fetch(`https://api.wlaunch.net/v1/public/company/${COMPANY_ID}/branch/${BRANCH_ID}/service?resource=${selectedResourceId}&active=true&withPriceOn=${todayISO}`, { headers: { /*Authorization: `Bearer ${API_TOKEN}` */} }) .then(res => res.json()) .then(data => { allServices = data.content.sort((a, b) => a.ordinal - b.ordinal); categories = allServices.filter(s => s.type === 'GROUP' && s.service_id !== ADDON_CATEGORY_ID); const categoryTabs = document.getElementById('category-tabs'); const servicesList = document.getElementById('services-list'); categoryTabs.innerHTML = ''; servicesList.innerHTML = ''; categories.forEach((c, index) => { const tab = document.createElement('div'); tab.className = 'tab'; tab.textContent = c.name; tab.onclick = () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); renderServices(c.service_id); }; categoryTabs.appendChild(tab); if (index === 0) { tab.classList.add('active'); renderServices(c.service_id); } }); renderAddons(); }); } function renderAddons() { const addonSelect = document.getElementById('addon-select'); addonSelect.innerHTML = ``; const addons = allServices.filter(s => s.type === 'SERVICE' && s.parent_id === ADDON_CATEGORY_ID); addons.forEach(s => { const option = document.createElement('option'); option.value = s.service_id; option.textContent = `${s.name} (${Math.round(s.duration / 60)} хв)`; addonServices[s.service_id] = s.duration; addonSelect.appendChild(option); }); addonSelect.onchange = () => { selectedAddonId = addonSelect.value || ''; }; } function renderServices(categoryId) { const servicesList = document.getElementById('services-list'); servicesList.innerHTML = ''; const filtered = allServices.filter(s => s.type === 'SERVICE' && s.parent_id === categoryId); filtered.forEach((s) => { const div = document.createElement('div'); div.className = 'service-card'; if (selectedServiceIds.includes(s.service_id)) { div.classList.add('selected'); } const serviceInfo = document.createElement('div'); serviceInfo.className = 'service-info'; const title = document.createElement('div'); title.className = 'title'; title.textContent = s.name; const details = document.createElement('div'); details.className = 'details'; const timeIcon = ``; const priceIcon = ``; const price = s.prices?.[0]?.amount ? `${s.prices[0].amount / 100} грн` : 'Немає грн'; const duration = `${Math.round(s.duration / 60)} хв`; details.innerHTML = ` ${timeIcon} ${duration} ${priceIcon} ${price} `; serviceInfo.appendChild(title); serviceInfo.appendChild(details); div.appendChild(serviceInfo); const addBtn = document.createElement('div'); addBtn.className = 'service-add-btn'; addBtn.innerHTML = selectedServiceIds.includes(s.service_id) ? '✔' : '+'; div.appendChild(addBtn); addBtn.onclick = (e) => { e.stopPropagation(); const index = selectedServiceIds.indexOf(s.service_id); if (index === -1) { selectedServiceIds.push(s.service_id); sendToDataLayer('service_select', { service_name: s.name, service_id: s.service_id }); } else { selectedServiceIds.splice(index, 1); } serviceDurations[s.service_id] = s.duration; selectedDate = ''; selectedTime = null; document.getElementById('calendar').innerHTML = ''; document.getElementById('time-slots').innerHTML = ''; const noTime = document.getElementById('no-time-msg'); if (noTime) noTime.style.display = 'none'; if (selectedServiceIds.length > 0) { loadAvailableDates(); } renderServices(categoryId); document.getElementById('next-from-service').disabled = selectedServiceIds.length === 0; }; servicesList.appendChild(div); }); } function loadAvailableDates() { showLoader(); const start = new Date().toISOString().split('T')[0]; const end = new Date(Date.now() + 30 * 86400000).toISOString().split('T')[0]; const totalDuration = selectedServiceIds.reduce((sum, id) => sum + (serviceDurations[id] || 0), 0) + (selectedAddonId ? addonServices[selectedAddonId] : 0); const serviceIdParam = selectedServiceIds.length > 0 ? `&serviceId=${selectedServiceIds[0]}` : ''; fetch(`https://api.wlaunch.net/v1/company/${COMPANY_ID}/branch/${BRANCH_ID}/slot/resource?start=${start}&end=${end}&source=WIDGET&id=${selectedResourceId}${serviceIdParam}&serviceDuration=${totalDuration}&slotSize=900`, { headers: { /*Authorization: `Bearer ${API_TOKEN}` */} }) .then(res => res.json()) .then(data => { availableDates = data.slots?.[0]?.date_slots?.filter(d => d.slots?.length > 0)?.map(d => d.date) || []; renderCalendar(currentYear, currentMonth); }) .catch(error => { console.error('Помилка завантаження дат:', error); }) .finally(() => { hideLoader(); // Сховати лоадер після завершення }); } function renderCalendar(year, month) { const calendarEl = document.getElementById('calendar'); calendarEl.innerHTML = ''; const monthNames = ['Січень','Лютий','Березень','Квітень','Травень','Червень','Липень','Серпень','Вересень','Жовтень','Листопад','Грудень']; const dayNames = ['ПН','ВТ','СР','ЧТ','ПТ','СБ','НД']; const firstDay = new Date(year, month).getDay() || 7; const daysInMonth = new Date(year, month + 1, 0).getDate(); const header = document.createElement('div'); header.className = 'calendar-header'; header.innerHTML = `

${monthNames[month]} ${year}

`; calendarEl.appendChild(header); const grid = document.createElement('div'); grid.className = 'calendar-grid'; dayNames.forEach(day => { const el = document.createElement('div'); el.className = 'calendar-day'; el.textContent = day; grid.appendChild(el); }); for (let i = 1; i < firstDay; i++) { const empty = document.createElement('div'); empty.className = 'calendar-cell disabled'; grid.appendChild(empty); } for (let d = 1; d <= daysInMonth; d++) { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; const cell = document.createElement('div'); cell.className = 'calendar-cell'; cell.textContent = d; if (availableDates.includes(dateStr)) { cell.classList.add('available'); cell.onclick = () => { selectedDate = dateStr; document.querySelectorAll('.calendar-cell').forEach(c => c.classList.remove('selected')); cell.classList.add('selected'); const timeText = document.getElementById('select-time-text'); if (timeText) timeText.style.display = 'block'; loadAvailableTimeSlots(); }; } else { cell.classList.add('disabled'); } grid.appendChild(cell); } calendarEl.appendChild(grid); } function changeMonth(delta) { currentMonth += delta; if (currentMonth > 11) { currentMonth = 0; currentYear++; } else if (currentMonth < 0) { currentMonth = 11; currentYear--; } renderCalendar(currentYear, currentMonth); } function loadAvailableTimeSlots() { const totalDuration = selectedServiceIds.reduce((sum, id) => sum + (serviceDurations[id] || 0), 0) + (selectedAddonId ? addonServices[selectedAddonId] : 0); const serviceIdParam = selectedServiceIds.length > 0 ? `&serviceId=${selectedServiceIds[0]}` : ''; fetch(`https://api.wlaunch.net/v1/company/${COMPANY_ID}/branch/${BRANCH_ID}/slot/resource?start=${selectedDate}&end=${selectedDate}&source=WIDGET&id=${selectedResourceId}${serviceIdParam}&serviceDuration=${totalDuration}&slotSize=900`, { headers: { /*Authorization: `Bearer ${API_TOKEN}` */} }) .then(res => res.json()) .then(data => { const slots = data.slots?.[0]?.date_slots?.find(d => d.date === selectedDate)?.slots || []; const container = document.getElementById('time-slots'); const noTime = document.getElementById('no-time-msg'); container.innerHTML = ''; noTime.style.display = slots.length ? 'none' : 'block'; selectedTime = null; document.getElementById('next-from-date').disabled = true; document.getElementById('book-button').disabled = true; slots.forEach(slot => { const t = slot.time.start_time; const h = String(Math.floor(t / 3600)).padStart(2, '0'); const m = String((t % 3600) / 60).padStart(2, '0'); const btn = document.createElement('div'); btn.className = 'slot-button'; btn.textContent = `${h}:${m}`; btn.onclick = () => { selectedTime = t; sendToDataLayer('date_select', { date: selectedDate, time: `${h}:${m}` }); document.querySelectorAll('.slot-button').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); document.getElementById('next-from-date').disabled = false; document.getElementById('book-button').disabled = false; }; container.appendChild(btn); }); }); } function updateSummary() { if (!selectedResource || selectedServiceIds.length === 0 || !selectedDate || !selectedTime) return; const start = new Date(`${selectedDate}T00:00:00`); start.setSeconds(selectedTime); const totalDuration = selectedServiceIds.reduce((sum, id) => sum + (serviceDurations[id] || 0), 0) + (selectedAddonId ? addonServices[selectedAddonId] : 0); const end = new Date(start.getTime() + totalDuration * 1000); const timeText = `${String(start.getHours()).padStart(2, '0')}:${String(start.getMinutes()).padStart(2, '0')} — ${String(end.getHours()).padStart(2, '0')}:${String(end.getMinutes()).padStart(2, '0')}`; const dateText = start.toLocaleDateString('uk-UA', { day: 'numeric', month: 'long' }); document.getElementById('summary-master').textContent = selectedResource.name; document.getElementById('summary-datetime').textContent = `${dateText}, ${timeText}`; document.getElementById('summary-service').textContent = selectedServiceIds.map(id => { const s = allServices.find(s => s.service_id === id); return `${s?.name || 'Послуга'} (${Math.round((s?.duration || 0) / 60)} хв)`; }).join(', '); document.getElementById('summary-addon-row').style.display = selectedAddonId ? 'flex' : 'none'; if (selectedAddonId) { const addon = allServices.find(s => s.service_id === selectedAddonId); document.getElementById('summary-addon').textContent = `${addon?.name || ''} (${Math.round(addon?.duration / 60)} хв)`; } const totalPrice = selectedServiceIds.reduce((sum, id) => { const s = allServices.find(s => s.service_id === id); return sum + (s?.prices?.[0]?.amount || 0); }, 0); document.getElementById('summary-total').textContent = `${totalPrice / 100} ₴`; } function submitBooking() { const name = document.getElementById('client-name').value; const phone = '+380' + document.getElementById('client-phone-inner').value.trim(); const startDate = new Date(`${selectedDate}T00:00:00`); startDate.setSeconds(selectedTime); const startISO = startDate.toISOString(); const serviceSettings = selectedServiceIds.map((id, i) => ({ service: id, resources: [selectedResourceId], ordinal: i + 1 })); if (selectedAddonId) { serviceSettings.push({ service: selectedAddonId, resources: [selectedResourceId], ordinal: serviceSettings.length + 1 }); } const bookingData = { appointment: { company_id: COMPANY_ID, branch_id: BRANCH_ID, source: "online", client: { first_name: name, phone: phone }, start_time: startISO, service_resource_settings: serviceSettings, utm: parseUtmParams() // ← тільки тут! } }; fetch(`https://api.wlaunch.net/v1/client/appointment`, { method: 'POST', headers: { // Authorization: `Bearer ${API_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify(bookingData) }) .then(res => res.json()) .then(response => { if (response?.id) { const start = new Date(`${selectedDate}T00:00:00`); start.setSeconds(selectedTime); const totalDuration = selectedServiceIds.reduce((sum, id) => sum + (serviceDurations[id] || 0), 0) + (selectedAddonId ? addonServices[selectedAddonId] : 0); const end = new Date(start.getTime() + totalDuration * 1000); const timeText = `${String(start.getHours()).padStart(2, '0')}:${String(start.getMinutes()).padStart(2, '0')} — ${String(end.getHours()).padStart(2, '0')}:${String(end.getMinutes()).padStart(2, '0')}`; const dateText = start.toLocaleDateString('uk-UA', { weekday: 'long', day: 'numeric', month: 'long' }); let servicesText = selectedServiceIds.map(id => { const s = allServices.find(s => s.service_id === id); return `${s?.name || 'Послуга'} (${Math.round((s?.duration || 0) / 60)} хв)`; }).join(', '); if (selectedAddonId) { const addon = allServices.find(s => s.service_id === selectedAddonId); servicesText += `, ${addon?.name || 'Додаткова'} (${Math.round(addon?.duration / 60)} хв)`; } const totalPrice = selectedServiceIds.reduce((sum, id) => { const s = allServices.find(s => s.service_id === id); return sum + (s?.prices?.[0]?.amount || 0); }, 0); document.getElementById('thanks-master').textContent = `${selectedResource.name} (${selectedResource.short_description || 'Без опису'})`; document.getElementById('thanks-services').textContent = servicesText; document.getElementById('thanks-datetime').textContent = `${dateText}, ${timeText}`; document.getElementById('thanks-price').textContent = `${totalPrice / 100} ₴`; document.querySelectorAll('.step').forEach(s => s.classList.remove('active')); document.getElementById('barber-step-5').classList.add('active'); sendToDataLayer('record_create', { master_name: selectedResource.name, date: selectedDate, time: timeText, services: servicesText, price: totalPrice / 100 }); } else { document.getElementById('status').textContent = '❌ Не вдалося створити запис'; } }) .catch(err => console.error('Помилка', err)); } // date first function startByDate() { document.querySelectorAll('.step').forEach(s => s.classList.remove('active')); document.getElementById('date-step-1').classList.add('active'); renderCalendar_DateFirst(currentYear, currentMonth); } function resetDateStepData(step) { if (step <= 1) { selectedDate_DateFirst = ''; selectedService_DateFirst = ''; selectedMaster_DateFirst = ''; selectedServiceId_DateFirst = ''; selectedPrice_DateFirst = 0; selectedDuration_DateFirst = 0; selectedTime = null; cachedMastersWithService = null; cachedSelectedDate_DateFirst = null; cachedSelectedService_DateFirst = null; } else if (step === 2) { selectedService_DateFirst = ''; selectedMaster_DateFirst = ''; selectedServiceId_DateFirst = ''; selectedPrice_DateFirst = 0; selectedDuration_DateFirst = 0; selectedTime = null; cachedMastersWithService = null; cachedSelectedDate_DateFirst = null; cachedSelectedService_DateFirst = null; } else if (step === 3) { selectedMaster_DateFirst = ''; selectedServiceId_DateFirst = ''; selectedPrice_DateFirst = 0; selectedDuration_DateFirst = 0; selectedTime = null; } else if (step === 4) { selectedTime = null; } } function goToDateStep(n) { if (n === 2 && !selectedDate_DateFirst) return; if (n === 3 && !selectedService_DateFirst) return; if (n === 4 && !selectedMaster_DateFirst && !selectedServiceId_DateFirst) return; if (n === 5 && !selectedTime) return; resetDateStepData(n); document.querySelectorAll('.step').forEach(s => s.classList.remove('active')); document.getElementById(`date-step-${n}`).classList.add('active'); if (n === 2) loadServicesForDate(); else if (n === 3) loadMastersForServiceAndDate(); else if (n === 4) loadAvailableTimeSlots_DateFirst(); else if (n === 5) updateSummary_DateFirst(); } function changeMonth_DateFirst(delta) { currentMonth += delta; if (currentMonth > 11) { currentMonth = 0; currentYear++; } else if (currentMonth < 0) { currentMonth = 11; currentYear--; } renderCalendar_DateFirst(currentYear, currentMonth); } async function fetchAvailableSlotsForMonth(year, month) { const startDate = new Date(year, month, 1).toISOString().split('T')[0]; const endDate = new Date(year, month + 1, 0).toISOString().split('T')[0]; const response = await fetch( `https://api.wlaunch.net/v1/company/${COMPANY_ID}/branch/${BRANCH_ID}/slot/resource?start=${startDate}&end=${endDate}&serviceId=${COMMON_SERVICE_ID}&serviceDuration=${DATE_FIRST_TIME_DURATION}&slotSize=900`, { headers: { /* Authorization: `Bearer ${API_TOKEN}` */ } } ); const data = await response.json(); return data; } async function renderCalendar_DateFirst(year, month) { const monthNames = ['Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень']; const dayNames = ['ПН', 'ВТ', 'СР', 'ЧТ', 'ПТ', 'СБ', 'НД']; const firstDay = new Date(year, month, 1).getDay() || 7; const daysInMonth = new Date(year, month + 1, 0).getDate(); const today = new Date(); today.setHours(0, 0, 0, 0); const calendarEl = document.getElementById('calendar-date-first'); calendarEl.innerHTML = ''; const header = document.createElement('div'); header.className = 'calendar-header'; header.innerHTML = `

${monthNames[month]} ${year}

`; calendarEl.appendChild(header); const grid = document.createElement('div'); grid.className = 'calendar-grid'; dayNames.forEach(day => { const el = document.createElement('div'); el.className = 'calendar-day'; el.textContent = day; grid.appendChild(el); }); for (let i = 1; i < firstDay; i++) { const empty = document.createElement('div'); empty.className = 'calendar-cell disabled'; grid.appendChild(empty); } for (let d = 1; d <= daysInMonth; d++) { const date = new Date(year, month, d); const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; const cell = document.createElement('div'); cell.className = 'calendar-cell disabled'; cell.textContent = d; cell.dataset.date = dateStr; grid.appendChild(cell); } calendarEl.appendChild(grid); const data = await fetchAvailableSlotsForMonth(year, month); const availableDates = data.slots.flatMap(slot => slot.date_slots.filter(dateSlot => dateSlot.slots.length > 0).map(dateSlot => dateSlot.date) ); for (let d = 1; d <= daysInMonth; d++) { const date = new Date(year, month, d); const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; const cell = grid.querySelector(`.calendar-cell[data-date="${dateStr}"]`); if (cell && date >= today && availableDates.includes(dateStr)) { cell.classList.remove('disabled'); cell.classList.add('available'); cell.onclick = () => { selectedDate_DateFirst = dateStr; document.querySelectorAll('.calendar-cell').forEach(c => c.classList.remove('selected')); cell.classList.add('selected'); document.getElementById('next-from-date-first').disabled = false; }; } } } async function loadServicesForDate() { const todayISO = new Date().toISOString(); let currentPage = 0; let totalPages = 1; allServices = []; while (currentPage < totalPages) { const response = await fetch( `https://api.wlaunch.net/v1/public/company/${COMPANY_ID}/branch/${BRANCH_ID}/service?active=true&size=100&page=${currentPage}&withPriceOn=${todayISO}`, { headers: { /* Authorization: `Bearer ${API_TOKEN}` */ } } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data && data.content) { allServices = allServices.concat(data.content); } totalPages = data.page ? data.page.total_pages : 1; currentPage++; } // Фільтруємо послуги const filteredServices = allServices .filter(s => s.type === 'SERVICE' && s.parent_id !== ADDON_CATEGORY_ID) .filter(s => { if (!s.prices || s.prices.length === 0) { return false; } const hasPrice = s.prices.some(price => price.amount > 0); if (!hasPrice) { return false; } if (EXCLUDED_SERVICE_NAMES.length > 0 && EXCLUDED_SERVICE_NAMES.includes(s.name.trim())) { return false; } return true; }); filteredServices.sort((a, b) => a.ordinal - b.ordinal); // Групуємо послуги за батьківською категорією const categories = [...new Set(filteredServices.map(s => { const parentService = allServices.find(service => service.service_id === s.parent_id); return parentService ? parentService.name.replace(/[\uD83C-\uDBFF\uDC00-\uDFFF\s]+$/g, '').trim() : null; }))].filter(Boolean); const categoryServices = {}; categories.forEach(categoryName => { categoryServices[categoryName] = filteredServices.filter(s => { const parentService = allServices.find(service => service.service_id === s.parent_id); return parentService && parentService.name.replace(/[\uD83C-\uDBFF\uDC00-\uDFFF\s]+$/g, '').trim() === categoryName; }); }); renderCategoryTabsForDate(categories, categoryServices); } function renderCategoryTabsForDate(categories, categoryServices) { const categoryTabs = document.getElementById('category-tabs-date-first'); const servicesList = document.getElementById('services-list-date-first'); categoryTabs.innerHTML = ''; servicesList.innerHTML = ''; // Створюємо вкладки для кожної категорії categories.forEach((categoryName, index) => { const tab = document.createElement('div'); tab.className = 'tab'; tab.textContent = categoryName; tab.onclick = () => { document.querySelectorAll('#category-tabs-date-first .tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); renderServicesForDateByCategory(categoryServices[categoryName]); }; categoryTabs.appendChild(tab); if (index === 0) { tab.classList.add('active'); renderServicesForDateByCategory(categoryServices[categoryName]); } }); } function renderServicesForDateByCategory(services) { const servicesList = document.getElementById('services-list-date-first'); servicesList.innerHTML = ''; const serviceGroups = {}; services.forEach(service => { if (!serviceGroups[service.name]) { serviceGroups[service.name] = []; } serviceGroups[service.name].push(service); }); Object.entries(serviceGroups).forEach(([serviceName, serviceItems]) => { const div = document.createElement('div'); div.className = 'service-card'; if (selectedService_DateFirst === serviceName) { div.classList.add('selected'); } const timeIcon = ``; const priceIcon = ``; let minPrice = Infinity, maxPrice = 0, minDuration = Infinity, maxDuration = 0; serviceItems.forEach(s => { if (s.prices && s.prices.length > 0) { s.prices.forEach(price => { const priceValue = price.amount / 100; if (priceValue < minPrice) minPrice = priceValue; if (priceValue > maxPrice) maxPrice = priceValue; }); } const duration = s.duration / 60; if (duration < minDuration) minDuration = duration; if (duration > maxDuration) maxDuration = duration; }); let priceText; if (minPrice === maxPrice) { priceText = `${minPrice} ₴`; } else { priceText = `${minPrice} - ${maxPrice} ₴`; } let durationText; if (minDuration === maxDuration) { durationText = `${Math.round(minDuration)} хв`; } else { durationText = `${Math.round(minDuration)} - ${Math.round(maxDuration)} хв`; } div.innerHTML = `
${serviceName}
${timeIcon} ${durationText} ${priceIcon} ${priceText}
`; const checkmark = document.createElement('div'); checkmark.className = 'service-checkmark'; checkmark.innerHTML = '+'; div.appendChild(checkmark); div.onclick = () => { selectedService_DateFirst = serviceName; document.querySelectorAll('.service-card').forEach(card => card.classList.remove('selected')); div.classList.add('selected'); document.querySelector('#date-step-2 .bottom-next-btn button').disabled = false; }; servicesList.appendChild(div); }); document.querySelector('#date-step-2 .bottom-next-btn button').disabled = !selectedService_DateFirst; } async function loadMastersForServiceAndDate() { showLoader(); document.getElementById(`date-step-3`).classList.remove('active'); if (cachedMastersWithService && cachedSelectedDate_DateFirst === selectedDate_DateFirst && cachedSelectedService_DateFirst === selectedService_DateFirst) { renderMastersForService(cachedMastersWithService); return; } const isoDate = new Date(selectedDate_DateFirst).toISOString(); const resourcesResponse = await fetch( `https://api.wlaunch.net/v1/public/company/${COMPANY_ID}/branch/${BRANCH_ID}/resource?active=true&public=true&page=0&size=100` ); if (!resourcesResponse.ok) { throw new Error(`HTTP error! status: ${resourcesResponse.status}`); } const resources = (await resourcesResponse.json()).content || []; const mastersWithService = []; for (const resource of resources) { const serviceResponse = await fetch( `https://api.wlaunch.net/v1/public/company/${COMPANY_ID}/branch/${BRANCH_ID}/service?resource=${resource.id}&active=true&size=100&page=0&withPriceOn=${isoDate}` ); if (!serviceResponse.ok) continue; const services = (await serviceResponse.json()).content || []; const filte

Html code will be here