// Глобальные переменные let usersData = []; let selectedUserId = null; let selectedUserName = ''; let userFormChanged = false; let schedulerFormChanged = false; let originalUserData = null; let originalSchedulerData = null; let pendingUserSwitch = null; const monthNames = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']; // Инициализация при загрузке страницы document.addEventListener('DOMContentLoaded', function () { // Загружаем список сотрудников loadUsersList(); // Сохраняем оригинальные настройки планировщика saveOriginalSchedulerData(); // Устанавливаем обработчики событий setupEventListeners(); }); // Загрузка списка сотрудников async function loadUsersList() { try { const response = await fetch('/api/birthdate'); const data = await response.json(); if (data.status === 'ok') { usersData = data.users; // Определение ближайшего дня рождения findNearestBirthday(); renderUsersTable(); } else { showAlert('danger', 'Ошибка загрузки списка сотрудников'); } } catch (error) { console.error('Ошибка:', error); showAlert('danger', 'Ошибка загрузки списка сотрудников'); renderUsersTable(); // Рендерим пустую таблицу } } function findNearestBirthday() { const today = new Date(); today.setHours(0, 0, 0, 0); let nextBirthday = null; let nextBirthdayUserData = null; for (const user of usersData) { if (!user.enabled || !user.birthdate) continue; const birthdate = new Date(user.birthdate); // День рождения в текущем году let candidate = new Date( today.getFullYear(), birthdate.getMonth(), birthdate.getDate() ); // Если уже прошёл — переносим на следующий год if (candidate < today) { candidate.setFullYear(today.getFullYear() + 1); } if (!nextBirthday || candidate < nextBirthday) { nextBirthday = candidate; const age = candidate.getFullYear() - birthdate.getFullYear(); nextBirthdayUserData = { date: new Intl.DateTimeFormat('ru-RU', { month: 'long', day: 'numeric' }) .format(new Date(user.birthdate)), name: user.name, age }; } } const nextBirthdayElement = document.getElementById('nextBirthday'); if (nextBirthdayUserData) { nextBirthdayElement.textContent = `${nextBirthdayUserData.date} ${nextBirthdayUserData.name} исполнится ${nextBirthdayUserData.age} лет 🎉`; } return nextBirthdayUserData; } // Отображение таблицы сотрудников function renderUsersTable() { const tbody = document.getElementById('usersTableBody'); if (!usersData || usersData.length === 0) { tbody.innerHTML = `
Нет данных о сотрудниках

Нажмите "Обновить список" для загрузки данных

`; return; } document.getElementById('usersCount').textContent = usersData.length; // Сортируем по дате рождения (месяц и день) usersData.sort((a, b) => { const dateA = new Date(a.birthdate); const dateB = new Date(b.birthdate); // Извлекаем месяц и день const monthA = dateA.getMonth(); const dayA = dateA.getDate(); const monthB = dateB.getMonth(); const dayB = dateB.getDate(); // Сначала сравниваем месяцы, потом дни return monthA !== monthB ? monthA - monthB : dayA - dayB; }); let html = ''; usersData.forEach(user => { const birthdate = new Date(user.birthdate); const now = new Date(); const age = now.getFullYear() - birthdate.getFullYear(); const fullDate = birthdate.toLocaleDateString('ru-RU'); // Форматируем месяц и день (двузначные) const dateText = new Intl.DateTimeFormat('ru-RU', { month: 'long', day: 'numeric' }) .format(new Date(user.birthdate)); // Определяем пол const sexBadge = user.sex === 'male' ? 'М' : 'Ж'; // Специальности const specialtiesHtml = user.specialties && user.specialties.length > 0 ? user.specialties.map(s => `${s}`).join('') : '-'; // Статус enabled const enabledStatus = user.enabled ? '
' : '
'; // Статус данных (фото и текст) const hasPhoto = user.photoLink && user.photoLink.trim() !== ''; const hasText = user.congratulations && user.congratulations.trim() !== ''; const hasData = hasPhoto && hasText; const dataStatus = hasData ? '
' : '
'; // Определяем класс строки (выделение выбранного) const isSelected = selectedUserId === user.id ? 'selected' : ''; html += `
${dateText}
${user.name} ${user.shortName}
${fullDate}
${age} лет ${sexBadge} ${specialtiesHtml} ${enabledStatus} ${user.enabled ? dataStatus : ''} `; }); tbody.innerHTML = html; } // Выбор сотрудника function selectUser(userId) { // Если есть несохраненные изменения if (userFormChanged && selectedUserId !== null) { pendingUserSwitch = userId; document.getElementById('modalUserName').textContent = selectedUserName; const modal = new bootstrap.Modal(document.getElementById('confirmModal')); modal.show(); return; } // Выполняем переключение performUserSwitch(userId); } // Выполнение переключения сотрудника function performUserSwitch(userId) { const user = usersData.find(u => u.id === userId); if (!user) return; // Обновляем выделение в таблице document.querySelectorAll('#usersTableBody tr').forEach(row => { row.classList.remove('selected'); }); document.querySelector(`tr[data-user-id="${userId}"]`).classList.add('selected'); // Сохраняем данные selectedUserId = userId; selectedUserName = user.name; // Обновляем заголовок document.getElementById('selectedUserName').textContent = user.name; // Заполняем форму document.getElementById('userId').value = user.id; document.getElementById('user_enabled').checked = user.enabled; document.getElementById('photo_link').value = user.photoLink || ''; document.getElementById('congratulations').value = user.congratulations || ''; // Показываем информацию о посте, если есть const postInfo = document.getElementById('postInfo'); const postLinkContainer = document.getElementById('postLinkContainer'); const publishTime = document.getElementById('publishTime'); if (user.postLink) { postInfo.style.display = 'block'; postLinkContainer.innerHTML = ` Перейти к посту в VK `; publishTime.innerHTML = `Опубликовано: ${user.publishAt || 'неизвестно'}`; } else { postInfo.style.display = 'none'; } // Сохраняем оригинальные данные для сравнения saveOriginalUserData(); // Сбрасываем флаг изменений userFormChanged = false; updateSaveUserButton(); } // Отметка изменений в форме сотрудника function markUserChanged() { userFormChanged = true; updateSaveUserButton(); } // Отметка изменений в форме планировщика function markSchedulerChanged() { schedulerFormChanged = true; updateSaveSchedulerButton(); } // Обновление кнопки сохранения сотрудника function updateSaveUserButton() { const button = document.getElementById('saveUserButton'); button.disabled = !userFormChanged; } // Обновление кнопки сохранения планировщика function updateSaveSchedulerButton() { const button = document.getElementById('saveSchedulerButton'); button.disabled = !schedulerFormChanged; } // Сохранение оригинальных данных сотрудника function saveOriginalUserData() { if (!selectedUserId) return; originalUserData = { enabled: document.getElementById('user_enabled').checked, photoLink: document.getElementById('photo_link').value, congratulations: document.getElementById('congratulations').value }; } // Сохранение оригинальных данных планировщика function saveOriginalSchedulerData() { originalSchedulerData = { hour: parseInt(document.getElementById('scheduler_hour').value) || 9, minute: parseInt(document.getElementById('scheduler_minute').value) || 0, enabled: document.getElementById('scheduler_enabled').checked }; } // Сброс формы сотрудника function resetUserForm() { if (!selectedUserId) return; if (userFormChanged && confirm('Отменить изменения?')) { const user = usersData.find(u => u.id === selectedUserId); if (user) { document.getElementById('user_enabled').checked = user.enabled; document.getElementById('photo_link').value = user.photoLink || ''; document.getElementById('congratulations').value = user.congratulations || ''; userFormChanged = false; updateSaveUserButton(); saveOriginalUserData(); } } } // Сброс формы планировщика function resetSchedulerForm() { if (schedulerFormChanged && confirm('Отменить изменения в настройках планировщика?')) { if (originalSchedulerData) { document.getElementById('scheduler_hour').value = originalSchedulerData.hour; document.getElementById('scheduler_minute').value = originalSchedulerData.minute; document.getElementById('scheduler_enabled').checked = originalSchedulerData.enabled; schedulerFormChanged = false; updateSaveSchedulerButton(); } } } // Сохранение данных сотрудника async function saveUserData() { function normalizeVkPhotoLink(link) { if (!link) return link; try { const url = new URL(link); const z = url.searchParams.get('z'); if (z && z.includes('%2F')) { url.searchParams.set('z', z.split('%2F')[0]); } return url.toString(); } catch (e) { // если это невалидный URL — возвращаем как есть return link; } } if (!selectedUserId) { showAlert('warning', 'Выберите сотрудника для сохранения'); return; } const userData = { enabled: document.getElementById('user_enabled').checked, photoLink: normalizeVkPhotoLink( document.getElementById('photo_link').value.trim() ), congratulations: document.getElementById('congratulations').value.trim() }; // Проверка длины поздравления if (userData.congratulations.length > 2000) { showAlert('warning', 'Длина поздравления не должна превышать 2000 символов, сейчас - ' + userData.congratulations.length); return; } // Проверка: если фото или текст заполнены, оба должны быть заполнены if ((userData.photoLink && !userData.congratulations) || (!userData.photoLink && userData.congratulations)) { showAlert('warning', 'Для поздравления должны быть заполнены и фото, и текст'); return; } const postData = { userUpdate: { userId: selectedUserId, userData: userData } }; try { const response = await fetch('/api/birthdate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postData) }); const data = await response.json(); if (response.ok && data.status === 'ok') { showAlert('success', 'Данные сотрудника сохранены!'); // Обновляем данные в локальном массиве const userIndex = usersData.findIndex(u => u.id === selectedUserId); if (userIndex !== -1) { usersData[userIndex].enabled = userData.enabled; usersData[userIndex].photoLink = userData.photoLink; usersData[userIndex].congratulations = userData.congratulations; // Перерисовываем таблицу renderUsersTable(); } // Сбрасываем флаг изменений userFormChanged = false; updateSaveUserButton(); saveOriginalUserData(); } else { showAlert('danger', 'Ошибка сохранения данных сотрудника'); } } catch (error) { console.error('Ошибка:', error); showAlert('danger', 'Ошибка сохранения данных сотрудника'); } } // Сохранение настроек планировщика async function saveSchedulerSettings() { const scheduleSettings = { hour: parseInt(document.getElementById('scheduler_hour').value), minute: parseInt(document.getElementById('scheduler_minute').value), enabled: document.getElementById('scheduler_enabled').checked }; // Валидация if (scheduleSettings.hour < 0 || scheduleSettings.hour > 23) { showAlert('warning', 'Час должен быть от 0 до 23'); return; } if (scheduleSettings.minute < 0 || scheduleSettings.minute > 59) { showAlert('warning', 'Минута должна быть от 0 до 59'); return; } const postData = { scheduleSettings: scheduleSettings }; try { const response = await fetch('/api/birthdate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postData) }); const data = await response.json(); if (response.ok && data.status === 'ok') { showAlert('success', 'Настройки планировщика сохранены!'); // Обновляем информацию о следующем запуске if (data.next_run_time) { const schedulerStatus = document.getElementById('schedulerStatus'); schedulerStatus.innerHTML = `Активен`; // Обновляем отображение следующего запуска (если есть соответствующий элемент) const nextRunAlert = document.querySelector('.alert-success'); if (nextRunAlert) { nextRunAlert.querySelector('p').textContent = data.next_run_time; } } // Сбрасываем флаг изменений schedulerFormChanged = false; updateSaveSchedulerButton(); saveOriginalSchedulerData(); } else { showAlert('danger', 'Ошибка сохранения настроек планировщика'); } } catch (error) { console.error('Ошибка:', error); showAlert('danger', 'Ошибка сохранения настроек планировщика'); } } // Обновление списка сотрудников async function refreshUsersList() { try { const response = await fetch('/api/birthdate', { method: 'PATCH' }); const data = await response.json(); if (response.ok && data.status === 'ok') { showAlert('success', 'Список сотрудников обновлен!'); // Перезагружаем список await loadUsersList(); } else { showAlert('danger', 'Ошибка обновления списка сотрудников'); } } catch (error) { console.error('Ошибка:', error); showAlert('danger', 'Ошибка обновления списка сотрудников'); } } // Обработчики событий для модального окна function cancelSwitchUser() { pendingUserSwitch = null; } function discardUserChanges() { userFormChanged = false; updateSaveUserButton(); if (pendingUserSwitch) { performUserSwitch(pendingUserSwitch); pendingUserSwitch = null; } } function saveAndSwitchUser() { if (pendingUserSwitch) { saveUserData().then(() => { if (!userFormChanged) { performUserSwitch(pendingUserSwitch); pendingUserSwitch = null; } }); } } // Настройка обработчиков событий function setupEventListeners() { // Валидация полей времени document.getElementById('scheduler_hour').addEventListener('input', function () { let value = parseInt(this.value); if (value < 0) this.value = 0; if (value > 23) this.value = 23; }); document.getElementById('scheduler_minute').addEventListener('input', function () { let value = parseInt(this.value); if (value < 0) this.value = 0; if (value > 59) this.value = 59; }); // Предотвращение закрытия страницы при несохраненных изменениях window.addEventListener('beforeunload', function (e) { if (userFormChanged || schedulerFormChanged) { e.preventDefault(); e.returnValue = ''; } }); }