This commit is contained in:
2025-12-23 01:12:10 +03:00
parent 69706d0cb7
commit 6ec4bd00e2
22 changed files with 1923 additions and 175 deletions
+87
View File
@@ -0,0 +1,87 @@
function deleteAuthToken() {
document.cookie = "auth_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT;";
window.location.href = "/login";
}
// Вспомогательные функции для уведомлений
function showAlert(type, message) {
const alertContainer = document.getElementById('alertContainer');
// Очищаем старые алерты
alertContainer.innerHTML = '';
// Создаем новый алерт
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
// Иконка для типа алерта
const icon = getAlertIcon(type);
alert.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi ${icon} me-2 fs-5"></i>
<div class="flex-grow-1">${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertContainer.appendChild(alert);
// Автоматическое удаление через 5 секунд
setTimeout(() => {
if (alert.parentNode) {
alert.classList.remove('show');
setTimeout(() => alert.remove(), 150);
}
}, 5000);
}
function getAlertIcon(type) {
const icons = {
'success': 'bi-check-circle-fill',
'warning': 'bi-exclamation-triangle-fill',
'danger': 'bi-x-circle-fill',
'info': 'bi-info-circle-fill'
};
return icons[type] || 'bi-info-circle-fill';
}
document.getElementById("changePasswordForm").addEventListener("submit", async function (e) {
e.preventDefault(); // ❌ перезагрузка страницы
const passwordInput = document.getElementById("newPassword");
const newPassword = passwordInput.value.trim();
if (!newPassword) {
showAlert("warning", "Введите новый пароль");
return;
}
try {
const response = await fetch("/login", {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ password: newPassword })
});
const data = await response.json();
if (data.status === "ok") {
showAlert("success", "Пароль успешно изменён");
// очистить поле
passwordInput.value = "";
// закрыть dropdown
const btn = document.getElementById("changePasswordBtn");
const dropdown = bootstrap.Dropdown.getOrCreateInstance(btn);
dropdown.hide();
} else {
showAlert("danger", data.errorMessage || "Ошибка изменения пароля");
}
} catch (err) {
showAlert("danger", "Ошибка соединения с сервером");
}
});
+554
View File
@@ -0,0 +1,554 @@
// Глобальные переменные
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?action=update', {
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 = '';
}
});
}
-11
View File
@@ -127,14 +127,3 @@ async function refreshLogs() {
console.error('Ошибка при обновлении логов:', error);
}
}
function getAlertIcon(type) {
const icons = {
'success': 'bi-check-circle-fill',
'warning': 'bi-exclamation-triangle-fill',
'danger': 'bi-x-circle-fill',
'info': 'bi-info-circle-fill'
};
return icons[type] || 'bi-info-circle-fill';
}
-38
View File
@@ -618,44 +618,6 @@ function downloadResponse() {
URL.revokeObjectURL(url);
}
// Вспомогательные функции
function showAlert(type, message) {
const alertContainer = document.getElementById('alertContainer');
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
alert.style.minWidth = '300px';
alert.style.maxWidth = '400px';
alert.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi ${getAlertIcon(type)} me-2"></i>
<div class="flex-grow-1">${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertContainer.appendChild(alert);
// Автоматическое удаление через 4 секунды
setTimeout(() => {
if (alert.parentNode) {
alert.classList.remove('show');
setTimeout(() => alert.remove(), 150);
}
}, 4000);
}
function getAlertIcon(type) {
const icons = {
'success': 'bi-check-circle-fill',
'warning': 'bi-exclamation-triangle-fill',
'danger': 'bi-x-circle-fill',
'info': 'bi-info-circle-fill'
};
return icons[type] || 'bi-info-circle-fill';
}
function showLoader(show) {
const executeButton = document.getElementById('executeButton');
if (show) {
-43
View File
@@ -305,46 +305,3 @@ async function updateSchedulerStatus() {
console.error('Ошибка обновления статуса:', error);
}
}
// Вспомогательные функции для уведомлений
function showAlert(type, message) {
const alertContainer = document.getElementById('alertContainer');
// Очищаем старые алерты
alertContainer.innerHTML = '';
// Создаем новый алерт
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
// Иконка для типа алерта
const icon = getAlertIcon(type);
alert.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi ${icon} me-2 fs-5"></i>
<div class="flex-grow-1">${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertContainer.appendChild(alert);
// Автоматическое удаление через 5 секунд
setTimeout(() => {
if (alert.parentNode) {
alert.classList.remove('show');
setTimeout(() => alert.remove(), 150);
}
}, 5000);
}
function getAlertIcon(type) {
const icons = {
'success': 'bi-check-circle-fill',
'warning': 'bi-exclamation-triangle-fill',
'danger': 'bi-x-circle-fill',
'info': 'bi-info-circle-fill'
};
return icons[type] || 'bi-info-circle-fill';
}
-40
View File
@@ -101,46 +101,6 @@ async function saveVkSettings() {
}
}
// Вспомогательные функции для уведомлений
function showAlert(type, message) {
const alertContainer = document.getElementById('alertContainer');
// Создаем алерт
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
// Иконка для типа алерта
const icon = getAlertIcon(type);
alert.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi ${icon} me-2 fs-5"></i>
<div class="flex-grow-1">${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertContainer.appendChild(alert);
// Автоматическое удаление через 5 секунд
setTimeout(() => {
if (alert.parentNode) {
alert.classList.remove('show');
setTimeout(() => alert.remove(), 150);
}
}, 5000);
}
function getAlertIcon(type) {
const icons = {
'success': 'bi-check-circle-fill',
'warning': 'bi-exclamation-triangle-fill',
'danger': 'bi-x-circle-fill',
'info': 'bi-info-circle-fill'
};
return icons[type] || 'bi-info-circle-fill';
}
// Дополнительная проверка при вводе данных
document.getElementById('group_id').addEventListener('input', function (e) {
this.value = this.value.replace(/[^\d]/g, '');