// Глобальные переменные
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 = '';
}
});
}