diff --git a/js/api.js b/js/api.js
new file mode 100644
index 0000000..7cc8de2
--- /dev/null
+++ b/js/api.js
@@ -0,0 +1,175 @@
+const API_CONFIG = {
+ baseUrl: 'http://exam-api-courses.std-900.ist.mospolytech.ru/api',
+ apiKey: '358a63a5-52ae-4ab0-800b-90f75ce5a5c2'
+};
+
+// Хранилище контактных данных в локальном хранилище
+const ContactDataStorage = {
+ // Сохранить данные контакта для заявки
+ save(orderId, data) {
+ const storageKey = `order_contact_${orderId}`;
+ localStorage.setItem(storageKey, JSON.stringify(data));
+ },
+
+ // Получить данные контакта по ID заявки
+ get(orderId) {
+ const storageKey = `order_contact_${orderId}`;
+ const data = localStorage.getItem(storageKey);
+ return data ? JSON.parse(data) : null;
+ },
+
+ // Удалить данные контакта из хранилища
+ remove(orderId) {
+ const storageKey = `order_contact_${orderId}`;
+ localStorage.removeItem(storageKey);
+ }
+};
+
+// Добавить API-ключ к URL запроса
+function addApiKey(url) {
+ const separator = url.includes('?') ? '&' : '?';
+ return `${url}${separator}api_key=${API_CONFIG.apiKey}`;
+}
+
+// Отправить запрос к API с обработкой ошибок
+async function apiRequest(endpoint, method = 'GET', data = null) {
+ const url = addApiKey(`${API_CONFIG.baseUrl}${endpoint}`);
+
+ const options = {
+ method,
+ headers: {'Content-Type': 'application/json'}
+ };
+
+ if (data && (method === 'POST' || method === 'PUT')) {
+ options.body = JSON.stringify(data);
+ }
+
+ try {
+ const response = await fetch(url, options);
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error(result.error || 'Ошибка сервера');
+ }
+
+ return result;
+ } catch (error) {
+ console.error('API Error:', error);
+ throw error;
+ }
+}
+
+// Загрузить список курсов и отобразить
+async function loadCourses() {
+ try {
+ AppState.courses = await apiRequest('/courses');
+ AppState.filteredCourses = [...AppState.courses];
+ renderCourses();
+ } catch (error) {
+ showNotification('Ошибка загрузки курсов: ' +
+ error.message, 'error');
+ }
+}
+
+// Загрузить список репетиторов и отобразить
+async function loadTutors() {
+ try {
+ AppState.tutors = await apiRequest('/tutors');
+ AppState.filteredTutors = [...AppState.tutors];
+ populateLanguageFilter();
+ renderTutors();
+ } catch (error) {
+ showNotification('Ошибка загрузки репетиторов: ' +
+ error.message, 'error');
+ }
+}
+
+// Загрузить список заявок и обновить интерфейс
+async function loadOrders() {
+ try {
+ AppState.orders = await apiRequest('/orders');
+ renderOrders();
+ updateUserInfo();
+ } catch (error) {
+ showNotification('Ошибка загрузки заявок: ' +
+ error.message, 'error');
+ }
+}
+
+// Создать новую заявку и сохранить контакты
+async function createOrder(orderData, contactData) {
+ try {
+ const result = await apiRequest('/orders', 'POST', orderData);
+ ContactDataStorage.save(result.id, contactData);
+ showNotification('Заявка успешно создана!', 'success');
+ return result;
+ } catch (error) {
+ showNotification('Ошибка создания заявки: ' +
+ error.message, 'error');
+ throw error;
+ }
+}
+
+// Обновить существующую заявку и контакты
+async function updateOrder(orderId, orderData, contactData) {
+ try {
+ const result = await apiRequest(`/orders/${orderId}`,
+ 'PUT', orderData);
+ if (contactData) {
+ ContactDataStorage.save(orderId, contactData);
+ }
+ showNotification('Заявка успешно обновлена!', 'success');
+ return result;
+ } catch (error) {
+ showNotification('Ошибка обновления заявки: ' +
+ error.message, 'error');
+ throw error;
+ }
+}
+
+// Удалить заявку и её контактные данные
+async function deleteOrder(orderId) {
+ try {
+ const result = await apiRequest(`/orders/${orderId}`, 'DELETE');
+ ContactDataStorage.remove(orderId);
+ showNotification('Заявка успешно удалена!', 'success');
+ return result;
+ } catch (error) {
+ showNotification('Ошибка удаления заявки: ' +
+ error.message, 'error');
+ throw error;
+ }
+}
+
+// Получить заявку по ID
+async function getOrder(orderId) {
+ try {
+ return await apiRequest(`/orders/${orderId}`);
+ } catch (error) {
+ showNotification('Ошибка получения заявки: ' +
+ error.message, 'error');
+ throw error;
+ }
+}
+
+// Получить курс по ID
+async function getCourse(courseId) {
+ try {
+ return await apiRequest(`/courses/${courseId}`);
+ } catch (error) {
+ showNotification('Ошибка получения курса: ' +
+ error.message, 'error');
+ throw error;
+ }
+}
+
+// Получить репетитора по ID
+async function getTutor(tutorId) {
+ try {
+ return await apiRequest(`/tutors/${tutorId}`);
+ } catch (error) {
+ showNotification('Ошибка получения репетитора: ' +
+ error.message, 'error');
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/js/utils.js b/js/utils.js
new file mode 100644
index 0000000..fdf6704
--- /dev/null
+++ b/js/utils.js
@@ -0,0 +1,258 @@
+// Показать уведомление с автоматическим удалением
+function showNotification(message, type = 'info') {
+ const notificationArea = document.getElementById('notificationArea');
+ if (!notificationArea) return;
+
+ const alertTypes = {
+ 'success': 'alert-success',
+ 'error': 'alert-danger',
+ 'warning': 'alert-warning',
+ 'info': 'alert-info'
+ };
+
+ const icons = {
+ 'success': 'bi-check-circle-fill',
+ 'error': 'bi-exclamation-circle-fill',
+ 'warning': 'bi-exclamation-triangle-fill',
+ 'info': 'bi-info-circle-fill'
+ };
+
+ const alertClass = alertTypes[type] || 'alert-info';
+ const iconClass = icons[type] || 'bi-info-circle-fill';
+
+ const notification = document.createElement('div');
+ notification.className = `alert ${alertClass} alert-dismissible
+ fade show notification`;
+ notification.innerHTML = `
+
+ ${message}
+
+ `;
+
+ notificationArea.appendChild(notification);
+
+ // Удалить уведомление через 5 секунд
+ setTimeout(() => {
+ notification.remove();
+ }, 5000);
+}
+
+// Форматировать цену в рублях
+function formatPrice(price) {
+ return new Intl.NumberFormat('ru-RU').format(price) + ' ₽';
+}
+
+// Форматировать дату в формате ДД.ММ.ГГГГ
+function formatDate(dateString) {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+ });
+}
+
+// Форматировать дату и время
+function formatDateTime(dateString) {
+ const date = new Date(dateString);
+ return date.toLocaleString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+}
+
+// Обрезать время до формата ЧЧ:ММ
+function formatTime(timeString) {
+ return timeString.substring(0, 5);
+}
+
+// Форматировать дату и время без секунд
+function formatDateTimeWithoutSeconds(dateString, timeString) {
+ const date = new Date(dateString);
+ const formattedDate = date.toLocaleDateString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+ });
+ const formattedTime = formatTime(timeString);
+ return `${formattedDate} ${formattedTime}`;
+}
+
+// Склонение числительных по русским правилам
+function pluralize(number, one, few, many) {
+ const mod10 = number % 10;
+ const mod100 = number % 100;
+
+ if (mod10 === 1 && mod100 !== 11) {
+ return `${number} ${one}`;
+ } else if (mod10 >= 2 && mod10 <= 4 &&
+ (mod100 < 10 || mod100 >= 20)) {
+ return `${number} ${few}`;
+ } else {
+ return `${number} ${many}`;
+ }
+}
+
+// Получить CSS класс для бейджа уровня
+function getLevelBadgeClass(level) {
+ const levelMap = {
+ 'Beginner': 'beginner',
+ 'Intermediate': 'intermediate',
+ 'Advanced': 'advanced'
+ };
+ return levelMap[level] || 'beginner';
+}
+
+// Перевести уровень на русский язык
+function getLevelText(level) {
+ const levelMap = {
+ 'Beginner': 'Начальный',
+ 'Intermediate': 'Средний',
+ 'Advanced': 'Продвинутый'
+ };
+ return levelMap[level] || level;
+}
+
+// Проверить является ли день выходным
+function isWeekend(date) {
+ const day = date.getDay();
+ return day === 0 || day === 6;
+}
+
+// Проверить является ли дата праздничным днём
+function isHoliday(date) {
+ // Праздничные дни в России на 2025-2026 годы
+ const holidays = [
+ // 2025
+ '2025-01-01', '2025-01-02', '2025-01-03', '2025-01-04',
+ '2025-01-05', '2025-01-06', '2025-01-07', '2025-01-08',
+ '2025-02-23', '2025-03-08', '2025-05-01', '2025-05-09',
+ '2025-06-12', '2025-11-04',
+ // 2026
+ '2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04',
+ '2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08',
+ '2026-02-23', '2026-03-08', '2026-05-01', '2026-05-09',
+ '2026-06-12', '2026-11-04'
+ ];
+
+ const dateStr = date.toISOString().split('T')[0];
+ return holidays.includes(dateStr);
+}
+
+// Получить множитель цены для выходных
+function getWeekendMultiplier(dateString) {
+ const date = new Date(dateString);
+ return (isWeekend(date) || isHoliday(date)) ? 1.5 : 1;
+}
+
+// Рассчитать надбавку за время занятия
+function getTimeSurcharge(timeString) {
+ const hour = parseInt(timeString.split(':')[0]);
+ let surcharge = 0;
+
+ // Утренняя надбавка
+ if (hour >= 9 && hour < 12) {
+ surcharge += 400;
+ }
+
+ // Вечерняя надбавка
+ if (hour >= 18 && hour < 20) {
+ surcharge += 1000;
+ }
+
+ return surcharge;
+}
+
+// Рассчитать дату окончания курса
+function calculateEndDate(startDateStr, weeks) {
+ const date = new Date(startDateStr);
+ date.setDate(date.getDate() + (weeks * 7));
+ return date;
+}
+
+// Отформатировать дату окончания курса
+function formatEndDate(startDateStr, weeks) {
+ const endDate = calculateEndDate(startDateStr, weeks);
+ return formatDate(endDate.toISOString().split('T')[0]);
+}
+
+// Проверить ранняя ли это регистрация
+function isEarlyRegistration(startDate) {
+ const today = new Date();
+ const start = new Date(startDate);
+ const diffTime = start - today;
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ return diffDays >= 30;
+}
+
+// Задержка выполнения функции для оптимизации
+function debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+}
+
+function generatePagination(currentPage, totalPages, containerId) {
+ const container = document.getElementById(containerId);
+ if (!container) return;
+
+ container.innerHTML = '';
+
+ if (totalPages <= 1) return;
+
+ const ul = document.createElement('ul');
+ ul.className = 'pagination justify-content-center';
+
+ const createPageItem = (page, text, disabled = false,
+ active = false) => {
+ const li = document.createElement('li');
+ li.className = `page-item ${disabled ? 'disabled' : ''}
+ ${active ? 'active' : ''}`;
+
+ const a = document.createElement('a');
+ a.className = 'page-link';
+ a.href = '#';
+ a.textContent = text || page;
+
+ if (!disabled) {
+ a.addEventListener('click', (e) => {
+ e.preventDefault();
+ return page;
+ });
+ }
+
+ li.appendChild(a);
+ return li;
+ };
+
+ ul.appendChild(createPageItem(currentPage - 1, 'Назад',
+ currentPage === 1));
+
+ for (let i = 1; i <= totalPages; i++) {
+ if (i === 1 || i === totalPages ||
+ (i >= currentPage - 1 && i <= currentPage + 1)) {
+ ul.appendChild(createPageItem(i, i, false,
+ i === currentPage));
+ } else if (i === currentPage - 2 || i === currentPage + 2) {
+ const li = document.createElement('li');
+ li.className = 'page-item disabled';
+ li.innerHTML = '...';
+ ul.appendChild(li);
+ }
+ }
+
+ ul.appendChild(createPageItem(currentPage + 1, 'Вперед',
+ currentPage === totalPages));
+
+ container.appendChild(ul);
+}
\ No newline at end of file