From 7f8478e25e65be5e40041691776cedba9d96ef74 Mon Sep 17 00:00:00 2001 From: EDeev Date: Mon, 29 Dec 2025 14:23:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20app?= =?UTF-8?q?.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/app.js | 756 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 756 insertions(+) create mode 100644 js/app.js diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..ff3cc5d --- /dev/null +++ b/js/app.js @@ -0,0 +1,756 @@ +const AppState = { + courses: [], + tutors: [], + orders: [], + filteredCourses: [], + filteredTutors: [], + selectedCourse: null, + selectedTutor: null, + currentCoursePage: 1, + coursesPerPage: 3, + currentTutorPage: 1, + tutorsPerPage: 3 +}; + +document.addEventListener('DOMContentLoaded', () => { + initApp(); +}); + +// Инициализировать главную страницу приложения +async function initApp() { + await loadCourses(); + await loadTutors(); + setupEventListeners(); + initMap(); +} + +// Настроить все обработчики событий +function setupEventListeners() { + const courseSearch = document.getElementById('courseSearch'); + const levelFilter = document.getElementById('levelFilter'); + const sortCourses = document.getElementById('sortCourses'); + + if (courseSearch) { + courseSearch.addEventListener('input', + debounce(filterAndRenderCourses, 300)); + } + + if (levelFilter) { + levelFilter.addEventListener('change', filterAndRenderCourses); + } + + if (sortCourses) { + sortCourses.addEventListener('change', filterAndRenderCourses); + } + + const tutorLevelFilter = + document.getElementById('tutorLevelFilter'); + const minExperience = document.getElementById('minExperience'); + const languageFilter = document.getElementById('languageFilter'); + const sortTutors = document.getElementById('sortTutors'); + + if (tutorLevelFilter) { + tutorLevelFilter.addEventListener('change', + filterAndRenderTutors); + } + + if (minExperience) { + minExperience.addEventListener('input', + debounce(filterAndRenderTutors, 300)); + } + + if (languageFilter) { + languageFilter.addEventListener('change', + filterAndRenderTutors); + } + + if (sortTutors) { + sortTutors.addEventListener('change', filterAndRenderTutors); + } + + const submitCourseOrder = + document.getElementById('submitCourseOrder'); + if (submitCourseOrder) { + submitCourseOrder.addEventListener('click', submitCourseOrderForm); + } + + const submitTutorOrder = + document.getElementById('submitTutorOrder'); + if (submitTutorOrder) { + submitTutorOrder.addEventListener('click', submitTutorOrderForm); + } + + const courseStartDate = document.getElementById('courseStartDate'); + if (courseStartDate) { + courseStartDate.addEventListener('change', + updateCourseTimeSlots); + } + + const courseStartTime = document.getElementById('courseStartTime'); + if (courseStartTime) { + courseStartTime.addEventListener('change', calculateCoursePrice); + } + + const studentsNumber = document.getElementById('studentsNumber'); + if (studentsNumber) { + studentsNumber.addEventListener('input', calculateCoursePrice); + } + + const checkboxes = ['supplementary', 'personalized', 'excursions', + 'assessment', 'interactive']; + checkboxes.forEach(id => { + const checkbox = document.getElementById(id); + if (checkbox) { + checkbox.addEventListener('change', calculateCoursePrice); + } + }); + +} + +// Отфильтровать и отобразить курсы +function filterAndRenderCourses() { + const searchTerm = document.getElementById('courseSearch').value + .toLowerCase(); + const levelFilter = document.getElementById('levelFilter').value; + const sortOption = document.getElementById('sortCourses').value; + + AppState.filteredCourses = AppState.courses.filter(course => { + const matchesSearch = course.name.toLowerCase() + .includes(searchTerm) || + course.description.toLowerCase().includes(searchTerm); + const matchesLevel = !levelFilter || + course.level === levelFilter; + return matchesSearch && matchesLevel; + }); + + if (sortOption) { + const [field, order] = sortOption.split('_'); + AppState.filteredCourses.sort((a, b) => { + let aVal, bVal; + + if (field === 'duration') { + aVal = a.total_length; + bVal = b.total_length; + } else if (field === 'price') { + aVal = a.course_fee_per_hour; + bVal = b.course_fee_per_hour; + } + + return order === 'asc' ? aVal - bVal : bVal - aVal; + }); + } + + AppState.currentCoursePage = 1; + renderCourses(); +} + +function renderCourses() { + const container = document.getElementById('coursesList'); + if (!container) return; + + container.innerHTML = ''; + + const startIdx = (AppState.currentCoursePage - 1) * + AppState.coursesPerPage; + const endIdx = startIdx + AppState.coursesPerPage; + const coursesToShow = AppState.filteredCourses + .slice(startIdx, endIdx); + + coursesToShow.forEach(course => { + const item = document.createElement('div'); + item.className = 'course-item'; + item.dataset.courseId = course.id; + + item.innerHTML = ` +
+
${course.name}
+ + ${getLevelText(course.level)} + +
+
+ ${course.description.substring(0, 150)}... +
+
+
+ + ${course.teacher} +
+
+ + ${course.total_length} недель, + ${course.week_length} ч/нед +
+
+ + ${formatPrice(course.course_fee_per_hour)}/час +
+
+
+ +
+ `; + + item.addEventListener('click', (e) => { + if (!e.target.closest('.select-course-btn')) { + selectCourse(course); + } + }); + + const selectBtn = item.querySelector('.select-course-btn'); + selectBtn.addEventListener('click', (e) => { + e.stopPropagation(); + selectCourse(course); + }); + + container.appendChild(item); + }); + + renderCoursePagination(); +} + +function renderCoursePagination() { + const container = document.getElementById('coursesPagination'); + if (!container) return; + + container.innerHTML = ''; + + const totalPages = Math.ceil(AppState.filteredCourses.length / + AppState.coursesPerPage); + + if (totalPages <= 1) return; + + const createPageItem = (page, text, disabled = false) => { + const li = document.createElement('li'); + li.className = `page-item ${disabled ? 'disabled' : ''} + ${page === AppState.currentCoursePage ? + 'active' : ''}`; + + const a = document.createElement('a'); + a.className = 'page-link'; + a.href = '#courses'; + a.textContent = text || page; + + if (!disabled) { + a.addEventListener('click', (e) => { + e.preventDefault(); + AppState.currentCoursePage = page; + renderCourses(); + }); + } + + li.appendChild(a); + return li; + }; + + container.appendChild( + createPageItem(AppState.currentCoursePage - 1, 'Назад', + AppState.currentCoursePage === 1) + ); + + for (let i = 1; i <= totalPages; i++) { + if (i === 1 || i === totalPages || + (i >= AppState.currentCoursePage - 1 && + i <= AppState.currentCoursePage + 1)) { + container.appendChild(createPageItem(i, i, false)); + } else if (i === AppState.currentCoursePage - 2 || + i === AppState.currentCoursePage + 2) { + const li = document.createElement('li'); + li.className = 'page-item disabled'; + li.innerHTML = '...'; + container.appendChild(li); + } + } + + container.appendChild( + createPageItem(AppState.currentCoursePage + 1, 'Вперед', + AppState.currentCoursePage === totalPages) + ); +} + +// Выбрать курс и открыть форму заказа +function selectCourse(course) { + document.querySelectorAll('.course-item').forEach(item => { + item.classList.remove('selected'); + }); + + const selectedItem = document.querySelector( + `.course-item[data-course-id="${course.id}"]` + ); + if (selectedItem) { + selectedItem.classList.add('selected'); + } + + AppState.selectedCourse = course; + openCourseModal(course); +} + +// Открыть модальное окно заказа курса +function openCourseModal(course) { + const modal = new bootstrap.Modal( + document.getElementById('courseModal') + ); + + document.getElementById('courseId').value = course.id; + document.getElementById('courseName').value = course.name; + document.getElementById('courseTeacher').value = course.teacher; + document.getElementById('orderMode').value = 'create'; + + // Получить уникальные даты (без времени) + const uniqueDates = [...new Set(course.start_dates.map(dt => dt.split('T')[0]))]; + + const startDateSelect = document.getElementById('courseStartDate'); + startDateSelect.innerHTML = ''; + + uniqueDates.forEach(dateStr => { + const option = document.createElement('option'); + option.value = dateStr; + option.textContent = formatDate(dateStr); + startDateSelect.appendChild(option); + }); + + const durationText = `${course.total_length} недель, + ${course.week_length} ч/нед`; + document.getElementById('courseDuration').value = durationText; + + document.getElementById('studentsNumber').value = 1; + + ['earlyRegistration', 'groupEnrollment', + 'intensiveCourse'].forEach(id => { + document.getElementById(id).checked = false; + }); + + ['supplementary', 'personalized', 'excursions', + 'assessment', 'interactive'].forEach(id => { + document.getElementById(id).checked = false; + }); + + ['studentName', 'studentPhone', 'studentEmail', + 'studentMessage'].forEach(id => { + document.getElementById(id).value = ''; + }); + + document.getElementById('courseStartTime').disabled = true; + document.getElementById('courseStartTime').innerHTML = + ''; + + calculateCoursePrice(); + + modal.show(); +} + +function updateCourseTimeSlots() { + const selectedDate = document.getElementById('courseStartDate').value; + const timeSelect = document.getElementById('courseStartTime'); + + if (!selectedDate) { + timeSelect.disabled = true; + timeSelect.innerHTML = ''; + document.getElementById('courseEndDate').value = ''; + return; + } + + timeSelect.disabled = false; + timeSelect.innerHTML = ''; + + const course = AppState.selectedCourse; + + // Отфильтровать времена для выбранной даты + const timesForDate = course.start_dates + .filter(dt => dt.split('T')[0] === selectedDate) + .map(dt => dt.split('T')[1].substring(0, 5)); + + // Добавить опции времени с надбавками + timesForDate.forEach(timeStr => { + const option = document.createElement('option'); + option.value = timeStr; + + // Рассчитать время окончания занятия + const startDateTime = new Date(`${selectedDate}T${timeStr}`); + const endDateTime = new Date(startDateTime); + endDateTime.setHours(endDateTime.getHours() + course.week_length); + const endTimeStr = endDateTime.toTimeString().substring(0, 5); + + // Получить надбавку за время + const surcharge = getTimeSurcharge(timeStr); + let surchargeText = ''; + if (surcharge > 0) { + surchargeText = ` (+${surcharge}₽)`; + } + + option.textContent = `${timeStr} - ${endTimeStr}${surchargeText}`; + timeSelect.appendChild(option); + }); + + // Обновить дату окончания курса + const endDate = formatEndDate(selectedDate, course.total_length); + document.getElementById('courseEndDate').value = endDate; + + calculateCoursePrice(); +} + +// Рассчитать итоговую цену курса с надбавками +function calculateCoursePrice() { + const course = AppState.selectedCourse; + if (!course) return; + + const startDate = document.getElementById('courseStartDate').value; + const startTime = document.getElementById('courseStartTime').value; + const persons = parseInt( + document.getElementById('studentsNumber').value + ) || 1; + + if (!startDate || !startTime) { + document.getElementById('totalPrice').textContent = '0'; + return; + } + + const courseFeePerHour = course.course_fee_per_hour; + const totalHours = course.total_length * course.week_length; + const weekendMultiplier = getWeekendMultiplier(startDate); + const timeSurcharge = getTimeSurcharge(startTime); + + let basePrice = (courseFeePerHour * totalHours * + weekendMultiplier + timeSurcharge) * persons; + + const earlyReg = isEarlyRegistration(startDate); + const groupEnroll = persons >= 5; + const intensive = course.week_length > 20; + + document.getElementById('earlyRegistration').checked = earlyReg; + document.getElementById('groupEnrollment').checked = groupEnroll; + document.getElementById('intensiveCourse').checked = intensive; + + if (earlyReg) basePrice *= 0.9; + if (groupEnroll) basePrice *= 0.85; + if (intensive) basePrice *= 1.2; + + if (document.getElementById('supplementary').checked) { + basePrice += 2000 * persons; + } + + if (document.getElementById('personalized').checked) { + basePrice += 1500 * course.total_length; + } + + if (document.getElementById('excursions').checked) { + basePrice *= 1.25; + } + + if (document.getElementById('assessment').checked) { + basePrice += 300; + } + + if (document.getElementById('interactive').checked) { + basePrice *= 1.5; + } + + document.getElementById('totalPrice').textContent = + Math.round(basePrice); +} + +// Отправить форму заказа курса на сервер +async function submitCourseOrderForm() { + const form = document.getElementById('courseOrderForm'); + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + const courseId = parseInt(document.getElementById('courseId').value); + const startDate = document.getElementById('courseStartDate').value + .split('T')[0]; + const startTime = document.getElementById('courseStartTime').value; + const persons = parseInt( + document.getElementById('studentsNumber').value + ); + const price = parseInt( + document.getElementById('totalPrice').textContent + ); + + const orderData = { + course_id: courseId, + tutor_id: 0, + date_start: startDate, + time_start: startTime, + duration: AppState.selectedCourse.total_length * + AppState.selectedCourse.week_length, + persons: persons, + price: price, + early_registration: + document.getElementById('earlyRegistration').checked, + group_enrollment: + document.getElementById('groupEnrollment').checked, + intensive_course: + document.getElementById('intensiveCourse').checked, + supplementary: document.getElementById('supplementary').checked, + personalized: document.getElementById('personalized').checked, + excursions: document.getElementById('excursions').checked, + assessment: document.getElementById('assessment').checked, + interactive: document.getElementById('interactive').checked + }; + + const contactData = { + name: document.getElementById('studentName').value, + phone: document.getElementById('studentPhone').value, + email: document.getElementById('studentEmail').value, + message: document.getElementById('studentMessage').value + }; + + try { + await createOrder(orderData, contactData); + const modal = bootstrap.Modal.getInstance( + document.getElementById('courseModal') + ); + modal.hide(); + form.reset(); + } catch (error) { + console.error('Error creating order:', error); + } +} + +function populateLanguageFilter() { + const languageFilter = document.getElementById('languageFilter'); + if (!languageFilter) return; + + const languages = new Set(); + AppState.tutors.forEach(tutor => { + tutor.languages_offered.forEach(lang => languages.add(lang)); + }); + + Array.from(languages).sort().forEach(lang => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = lang; + languageFilter.appendChild(option); + }); +} + +// Отфильтровать и отобразить репетиторов +function filterAndRenderTutors() { + const levelFilter = + document.getElementById('tutorLevelFilter').value; + const minExp = parseInt( + document.getElementById('minExperience').value + ) || 0; + const langFilter = document.getElementById('languageFilter').value; + const sortOption = document.getElementById('sortTutors').value; + + AppState.filteredTutors = AppState.tutors.filter(tutor => { + const matchesLevel = !levelFilter || + tutor.language_level === levelFilter; + const matchesExp = tutor.work_experience >= minExp; + const matchesLang = !langFilter || + tutor.languages_offered.includes(langFilter); + return matchesLevel && matchesExp && matchesLang; + }); + + if (sortOption) { + const [field, order] = sortOption.split('_'); + AppState.filteredTutors.sort((a, b) => { + const aVal = a.price_per_hour; + const bVal = b.price_per_hour; + return order === 'asc' ? aVal - bVal : bVal - aVal; + }); + } + + AppState.currentTutorPage = 1; + renderTutors(); +} + +function renderTutors() { + const tbody = document.getElementById('tutorsList'); + if (!tbody) return; + + tbody.innerHTML = ''; + + const startIdx = (AppState.currentTutorPage - 1) * + AppState.tutorsPerPage; + const endIdx = startIdx + AppState.tutorsPerPage; + const tutorsToShow = AppState.filteredTutors + .slice(startIdx, endIdx); + + tutorsToShow.forEach(tutor => { + const tr = document.createElement('tr'); + tr.dataset.tutorId = tutor.id; + tr.innerHTML = ` + ${tutor.name} + + ${getLevelText(tutor.language_level)} + + ${tutor.languages_offered.join(', ')} + ${tutor.work_experience} лет + ${formatPrice(tutor.price_per_hour)}/час + + + + `; + + tr.addEventListener('click', (e) => { + if (!e.target.closest('.select-tutor-btn')) { + selectTutor(tutor); + } + }); + + const selectBtn = tr.querySelector('.select-tutor-btn'); + selectBtn.addEventListener('click', (e) => { + e.stopPropagation(); + selectTutor(tutor); + }); + + tbody.appendChild(tr); + }); + + renderTutorPagination(); +} + +function renderTutorPagination() { + const container = document.getElementById('tutorsPagination'); + if (!container) return; + + container.innerHTML = ''; + + const totalPages = Math.ceil(AppState.filteredTutors.length / + AppState.tutorsPerPage); + + if (totalPages <= 1) return; + + const createPageItem = (page, text, disabled = false) => { + const li = document.createElement('li'); + li.className = `page-item ${disabled ? 'disabled' : ''} + ${page === AppState.currentTutorPage ? + 'active' : ''}`; + + const a = document.createElement('a'); + a.className = 'page-link'; + a.href = '#tutors'; + a.textContent = text || page; + + if (!disabled) { + a.addEventListener('click', (e) => { + e.preventDefault(); + AppState.currentTutorPage = page; + renderTutors(); + }); + } + + li.appendChild(a); + return li; + }; + + container.appendChild( + createPageItem(AppState.currentTutorPage - 1, 'Назад', + AppState.currentTutorPage === 1) + ); + + for (let i = 1; i <= totalPages; i++) { + if (i === 1 || i === totalPages || + (i >= AppState.currentTutorPage - 1 && + i <= AppState.currentTutorPage + 1)) { + container.appendChild(createPageItem(i, i, false)); + } else if (i === AppState.currentTutorPage - 2 || + i === AppState.currentTutorPage + 2) { + const li = document.createElement('li'); + li.className = 'page-item disabled'; + li.innerHTML = '...'; + container.appendChild(li); + } + } + + container.appendChild( + createPageItem(AppState.currentTutorPage + 1, 'Вперед', + AppState.currentTutorPage === totalPages) + ); +} + +// Выбрать репетитора и открыть форму заказа +function selectTutor(tutor) { + document.querySelectorAll('#tutorsList tr').forEach(row => { + row.classList.remove('selected'); + }); + + const selectedRow = document.querySelector( + `[data-tutor-id="${tutor.id}"]` + ); + if (selectedRow) { + selectedRow.classList.add('selected'); + } + + AppState.selectedTutor = tutor; + openTutorModal(tutor); +} + +// Открыть модальное окно заказа репетитора +function openTutorModal(tutor) { + const modal = new bootstrap.Modal( + document.getElementById('tutorModal') + ); + + document.getElementById('tutorId').value = tutor.id; + document.getElementById('tutorName').value = tutor.name; + + ['tutorStudentName', 'tutorStudentPhone', + 'tutorStudentEmail', 'tutorStudentMessage'].forEach(id => { + document.getElementById(id).value = ''; + }); + + modal.show(); +} + +// Отправить форму заказа репетитора на сервер +async function submitTutorOrderForm() { + const form = document.getElementById('tutorOrderForm'); + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + const tutorId = parseInt(document.getElementById('tutorId').value); + const tutor = AppState.selectedTutor; + + // Используем текущую дату как плейсхолдер + const today = new Date(); + const dateStr = today.toISOString().split('T')[0]; + + const orderData = { + tutor_id: tutorId, + course_id: 0, + date_start: dateStr, + time_start: '10:00', + duration: 1, + persons: 1, + price: tutor ? tutor.price_per_hour : 0, + early_registration: false, + group_enrollment: false, + intensive_course: false, + supplementary: false, + personalized: false, + excursions: false, + assessment: false, + interactive: false + }; + + const contactData = { + name: document.getElementById('tutorStudentName').value, + phone: document.getElementById('tutorStudentPhone').value, + email: document.getElementById('tutorStudentEmail').value, + message: document.getElementById('tutorStudentMessage').value + }; + + try { + await createOrder(orderData, contactData); + const modal = bootstrap.Modal.getInstance( + document.getElementById('tutorModal') + ); + modal.hide(); + form.reset(); + } catch (error) { + console.error('Error creating order:', error); + } +} \ No newline at end of file