Основная логика приложения в app.js

This commit is contained in:
EDeev 2025-12-29 14:23:00 +03:00
parent 313c8ad373
commit 7f8478e25e

756
js/app.js Normal file
View file

@ -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 = `
<div class="course-item-header">
<h5 class="course-item-title">${course.name}</h5>
<span class="badge level-badge
${getLevelBadgeClass(course.level)}">
${getLevelText(course.level)}
</span>
</div>
<div class="course-item-body">
${course.description.substring(0, 150)}...
</div>
<div class="course-item-details">
<div>
<i class="bi bi-person"></i>
<span>${course.teacher}</span>
</div>
<div>
<i class="bi bi-clock"></i>
<span>${course.total_length} недель,
${course.week_length} ч/нед</span>
</div>
<div>
<i class="bi bi-cash"></i>
<span>${formatPrice(course.course_fee_per_hour)}/час</span>
</div>
</div>
<div class="mt-2">
<button class="btn btn-primary btn-sm select-course-btn">
Выбрать
</button>
</div>
`;
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 = '<span class="page-link">...</span>';
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 = '<option value="">Выберите дату</option>';
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 =
'<option value="">Сначала выберите дату</option>';
calculateCoursePrice();
modal.show();
}
function updateCourseTimeSlots() {
const selectedDate = document.getElementById('courseStartDate').value;
const timeSelect = document.getElementById('courseStartTime');
if (!selectedDate) {
timeSelect.disabled = true;
timeSelect.innerHTML = '<option value="">Сначала выберите дату</option>';
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 = `
<td>${tutor.name}</td>
<td><span class="badge level-badge
${getLevelBadgeClass(tutor.language_level)}">
${getLevelText(tutor.language_level)}
</span></td>
<td>${tutor.languages_offered.join(', ')}</td>
<td>${tutor.work_experience} лет</td>
<td>${formatPrice(tutor.price_per_hour)}/час</td>
<td>
<button class="btn btn-sm btn-primary select-tutor-btn">
Выбрать
</button>
</td>
`;
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 = '<span class="page-link">...</span>';
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);
}
}