const tg = window.Telegram?.WebApp;
if (tg) {
tg.ready();
tg.expand();
}
const state = {
project: 'salon_123',
apiBase: 'https://YOUR-DOMAIN.COM/api',
selectedServiceId: null,
selectedSlotValue: null,
user: null,
phone: null,
services: [],
slots: [],
isLoadingServices: false,
isLoadingSlots: false,
isBooking: false,
};
const els = {
projectTitle: document.getElementById('projectTitle'),
services: document.getElementById('services'),
slots: document.getElementById('slots'),
bookBtn: document.getElementById('bookBtn'),
};
function parseStartParams() {
const params = new URLSearchParams(window.location.search);
const queryProject =
params.get('project') ||
params.get('startapp') ||
params.get('start') ||
'';
const tgProject =
tg?.initDataUnsafe?.start_param ||
'';
state.project = (tgProject || queryProject || 'salon_123').trim();
}
function detectUser() {
const tgUser = tg?.initDataUnsafe?.user || null;
state.user = tgUser;
}
function renderProjectTitle() {
if (els.projectTitle) {
els.projectTitle.textContent = `Проект: ${state.project}`;
}
}
function setBookButtonState() {
if (!els.bookBtn) return;
const canBook =
!!state.selectedServiceId &&
!!state.selectedSlotValue &&
!state.isBooking;
els.bookBtn.disabled = !canBook;
els.bookBtn.textContent = state.isBooking ? 'Записываем...' : 'Записаться';
}
function showAlert(message) {
if (tg?.showAlert) {
tg.showAlert(message);
} else {
alert(message);
}
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function renderServices() {
if (!els.services) return;
if (state.isLoadingServices) {
els.services.innerHTML = '
Загрузка услуг...
';
return;
}
if (!state.services.length) {
els.services.innerHTML = '
Услуги пока не найдены
';
return;
}
els.services.innerHTML = state.services
.map((service) => {
const selectedClass = state.selectedServiceId === service.id ? 'is-selected' : '';
const price = service.price ? `${service.price} ₽` : 'Цена уточняется';
const duration = service.duration ? `${service.duration} мин` : null;
return `
`;
})
.join('');
els.services.querySelectorAll('[data-service-id]').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-service-id');
state.selectedServiceId = Number.isNaN(Number(id)) ? id : Number(id);
state.selectedSlotValue = null;
renderServices();
renderSlots();
setBookButtonState();
loadSlots();
});
});
}
function renderSlots() {
if (!els.slots) return;
if (!state.selectedServiceId) {
els.slots.innerHTML = '
Сначала выберите услугу
';
return;
}
if (state.isLoadingSlots) {
els.slots.innerHTML = '
Загрузка свободного времени...
';
return;
}
if (!state.slots.length) {
els.slots.innerHTML = '
Свободных слотов пока нет
';
return;
}
els.slots.innerHTML = `
${state.slots
.map((slot) => {
const value = slot.value || slot.time;
const selectedClass = state.selectedSlotValue === value ? 'is-selected' : '';
return `
`;
})
.join('')}
`;
els.slots.querySelectorAll('[data-slot-value]').forEach((btn) => {
btn.addEventListener('click', () => {
state.selectedSlotValue = btn.getAttribute('data-slot-value');
renderSlots();
setBookButtonState();
});
});
}
async function apiGet(path) {
const url = `${state.apiBase}${path}`;
const res = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) {
throw new Error(`GET ${path} failed with ${res.status}`);
}
return res.json();
}
async function apiPost(path, body) {
const url = `${state.apiBase}${path}`;
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`POST ${path} failed with ${res.status}`);
}
return res.json();
}
function getMockServices() {
return [
{ id: 101, name: 'Маникюр', price: 1500, duration: 60 },
{ id: 102, name: 'Педикюр', price: 2200, duration: 90 },
{ id: 103, name: 'Окрашивание', price: 3500, duration: 120 },
];
}
function getMockSlots() {
return [
{ value: '2026-04-20T12:00:00', label: '20 апреля, 12:00' },
{ value: '2026-04-20T14:00:00', label: '20 апреля, 14:00' },
{ value: '2026-04-20T16:30:00', label: '20 апреля, 16:30' },
{ value: '2026-04-21T11:00:00', label: '21 апреля, 11:00' },
];
}
async function loadServices() {
state.isLoadingServices = true;
renderServices();
try {
const data = await apiGet(`/services?project=${encodeURIComponent(state.project)}`);
state.services = Array.isArray(data) ? data : data.items || [];
} catch (error) {
console.warn('Services API unavailable, using mock data.', error);
state.services = getMockServices();
} finally {
state.isLoadingServices = false;
renderServices();
setBookButtonState();
}
}
async function loadSlots() {
if (!state.selectedServiceId) return;
state.isLoadingSlots = true;
renderSlots();
try {
const data = await apiGet(
`/slots?project=${encodeURIComponent(state.project)}&service_id=${encodeURIComponent(state.selectedServiceId)}`
);
state.slots = Array.isArray(data) ? data : data.items || [];
} catch (error) {
console.warn('Slots API unavailable, using mock data.', error);
state.slots = getMockSlots();
} finally {
state.isLoadingSlots = false;
renderSlots();
setBookButtonState();
}
}
async function handleBooking() {
if (!state.selectedServiceId || !state.selectedSlotValue || state.isBooking) {
return;
}
state.isBooking = true;
setBookButtonState();
const payload = {
project: state.project,
service_id: state.selectedServiceId,
datetime: state.selectedSlotValue,
phone: state.phone,
user_id: state.user?.id || null,
first_name: state.user?.first_name || null,
last_name: state.user?.last_name || null,
username: state.user?.username || null,
platform: tg ? 'telegram' : 'web',
};
try {
const result = await apiPost('/book', payload);
if (result?.status === 'success' || result?.success === true) {
showAlert('Вы успешно записаны');
if (tg?.close) {
// Можно оставить открытым, но пока пусть просто покажет успешную запись
}
} else {
throw new Error('Unexpected booking response');
}
} catch (error) {
console.error('Booking failed', error);
showAlert('Не удалось записать. Проверь API или webhook.');
} finally {
state.isBooking = false;
setBookButtonState();
}
}
function bindEvents() {
if (els.bookBtn) {
els.bookBtn.addEventListener('click', handleBooking);
}
}
function init() {
parseStartParams();
detectUser();
renderProjectTitle();
renderServices();
renderSlots();
setBookButtonState();
bindEvents();
loadServices();
}
init();