555 lines
21 KiB
JavaScript
555 lines
21 KiB
JavaScript
// Глобальные переменные
|
||
let usersData = [];
|
||
let selectedUserId = null;
|
||
let selectedUserName = '';
|
||
let userFormChanged = false;
|
||
let schedulerFormChanged = false;
|
||
let originalUserData = null;
|
||
let originalSchedulerData = null;
|
||
let pendingUserSwitch = null;
|
||
|
||
// Инициализация при загрузке страницы
|
||
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;
|
||
renderUsersTable();
|
||
} else {
|
||
showAlert('danger', 'Ошибка загрузки списка сотрудников');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка:', error);
|
||
showAlert('danger', 'Ошибка загрузки списка сотрудников');
|
||
renderUsersTable(); // Рендерим пустую таблицу
|
||
}
|
||
}
|
||
|
||
// Отображение таблицы сотрудников
|
||
function renderUsersTable() {
|
||
const tbody = document.getElementById('usersTableBody');
|
||
|
||
if (!usersData || usersData.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" class="text-center py-5">
|
||
<i class="bi bi-people display-1 text-muted mb-3 d-block"></i>
|
||
<h5 class="text-muted">Нет данных о сотрудниках</h5>
|
||
<p class="text-muted">Нажмите "Обновить список" для загрузки данных</p>
|
||
<button type="button" class="btn btn-outline-primary" onclick="refreshUsersList()">
|
||
<i class="bi bi-arrow-clockwise me-1"></i>Обновить список
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Сортируем по дате рождения (месяц и день)
|
||
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 month = birthdate.getMonth() + 1;
|
||
const day = birthdate.getDate();
|
||
const fullDate = birthdate.toLocaleDateString('ru-RU');
|
||
|
||
// Форматируем месяц и день (двузначные)
|
||
const monthStr = month.toString().padStart(2, '0');
|
||
const dayStr = day.toString().padStart(2, '0');
|
||
|
||
// Определяем пол
|
||
const sexBadge = user.sex === 'male' ?
|
||
'<span class="badge sex-badge sex-male">М</span>' :
|
||
'<span class="badge sex-badge sex-female">Ж</span>';
|
||
|
||
// Специальности
|
||
const specialtiesHtml = user.specialties && user.specialties.length > 0 ?
|
||
user.specialties.map(s => `<span class="badge bg-light text-dark specialty-badge">${s}</span>`).join('') :
|
||
'<span class="text-muted">-</span>';
|
||
|
||
// Статус enabled
|
||
const enabledStatus = user.enabled ?
|
||
'<div class="status-icon status-enabled" title="Включен"><i class="bi bi-check-lg"></i></div>' :
|
||
'<div class="status-icon status-disabled" title="Отключен"><i class="bi bi-x-lg"></i></div>';
|
||
|
||
// Статус данных (фото и текст)
|
||
const hasPhoto = user.photoLink && user.photoLink.trim() !== '';
|
||
const hasText = user.congratulations && user.congratulations.trim() !== '';
|
||
const hasData = hasPhoto && hasText;
|
||
|
||
const dataStatus = hasData ?
|
||
'<div class="status-icon status-data" title="Данные для поздравления заполнены"><i class="bi bi-check-lg"></i></div>' :
|
||
'<div class="status-icon status-nodata" title="Нет данных для поздравления"><i class="bi bi-x-lg"></i></div>';
|
||
|
||
// Определяем класс строки (выделение выбранного)
|
||
const isSelected = selectedUserId === user.id ? 'selected' : '';
|
||
|
||
html += `
|
||
<tr class="${isSelected}" onclick="selectUser(${user.id})" data-user-id="${user.id}">
|
||
<td>
|
||
<div class="birthdate-cell">
|
||
<span class="month-day">${monthStr}.${dayStr}</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="fw-semibold">${user.name}</span>
|
||
</td>
|
||
<td>${user.shortName}</td>
|
||
<td>
|
||
<div>${fullDate}</div>
|
||
<small class="age-badge">${age} лет</small>
|
||
</td>
|
||
<td>${sexBadge}</td>
|
||
<td>${specialtiesHtml}</td>
|
||
<td class="text-center">${enabledStatus}</td>
|
||
<td class="text-center">
|
||
<div class="icon-group">${dataStatus}</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
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 = `
|
||
<a href="${user.postLink}" target="_blank" class="btn btn-sm btn-outline-info">
|
||
<i class="bi bi-box-arrow-up-right me-1"></i>Перейти к посту в VK
|
||
</a>
|
||
`;
|
||
publishTime.innerHTML = `<i class="bi bi-clock me-1"></i>Опубликовано: ${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 = `<span class="badge bg-success"><i class="bi bi-play-circle me-1"></i>Активен</span>`;
|
||
|
||
// Обновляем отображение следующего запуска (если есть соответствующий элемент)
|
||
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', 'Ошибка обновления списка сотрудников');
|
||
}
|
||
}
|
||
|
||
// Включить всех сотрудников
|
||
async function enableAllUsers() {
|
||
if (!confirm('Включить поздравления для всех сотрудников?')) return;
|
||
|
||
try {
|
||
// Находим всех сотрудников без данных
|
||
const usersWithoutData = usersData.filter(u =>
|
||
!u.enabled || !u.photoLink || !u.congratulations
|
||
);
|
||
|
||
if (usersWithoutData.length === 0) {
|
||
showAlert('info', 'Все сотрудники уже включены');
|
||
return;
|
||
}
|
||
|
||
// Показываем уведомление о необходимости заполнения данных
|
||
showAlert('info', `Включено ${usersWithoutData.length} сотрудников. Не забудьте заполнить данные для поздравлений.`);
|
||
|
||
// Обновляем статус в локальном массиве
|
||
usersData.forEach(user => {
|
||
user.enabled = true;
|
||
});
|
||
|
||
// Перерисовываем таблицу
|
||
renderUsersTable();
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка:', error);
|
||
showAlert('danger', 'Ошибка включения сотрудников');
|
||
}
|
||
}
|
||
|
||
// Отключить всех сотрудников
|
||
async function disableAllUsers() {
|
||
if (!confirm('Отключить поздравления для всех сотрудников?')) return;
|
||
|
||
try {
|
||
// Обновляем статус в локальном массиве
|
||
usersData.forEach(user => {
|
||
user.enabled = false;
|
||
});
|
||
|
||
// Перерисовываем таблицу
|
||
renderUsersTable();
|
||
|
||
showAlert('success', 'Все сотрудники отключены');
|
||
|
||
} 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 = '';
|
||
}
|
||
});
|
||
}
|