Files
medods_vk/static/js/birthdate.js
T
2025-12-23 01:38:19 +03:00

554 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Глобальные переменные
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;
}
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 month = birthdate.getMonth() + 1;
const day = birthdate.getDate();
const fullDate = birthdate.toLocaleDateString('ru-RU');
// Форматируем месяц и день (двузначные)
const monthNames = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
const monthStr = monthNames[month - 1];
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">${dayStr} ${monthStr}</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} ${dataStatus}</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 = '';
}
});
}