beta 2.0
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
|
||||
/* Цвет для темы дней рождения */
|
||||
.bg-pink {
|
||||
background-color: #e83e8c !important;
|
||||
}
|
||||
|
||||
.text-pink {
|
||||
color: #e83e8c !important;
|
||||
}
|
||||
|
||||
/* Карточки */
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
/* Таблица сотрудников */
|
||||
.table-responsive {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #f8f9fa;
|
||||
z-index: 10;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: rgba(232, 62, 140, 0.05);
|
||||
}
|
||||
|
||||
.table tbody tr.selected {
|
||||
background-color: rgba(232, 62, 140, 0.1);
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Иконки статуса */
|
||||
.status-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.status-enabled {
|
||||
background-color: rgba(25, 135, 84, 0.1);
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
.status-disabled {
|
||||
background-color: rgba(108, 117, 125, 0.1);
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.status-data {
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.status-nodata {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Бейджи для специализаций */
|
||||
.specialty-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0.1rem;
|
||||
}
|
||||
|
||||
/* Поля ввода */
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #e83e8c;
|
||||
box-shadow: 0 0 0 0.25rem rgba(232, 62, 140, 0.25);
|
||||
}
|
||||
|
||||
/* Свитч */
|
||||
.form-switch .form-check-input {
|
||||
width: 3em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input:checked {
|
||||
background-color: #e83e8c;
|
||||
border-color: #e83e8c;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Дата рождения */
|
||||
.birthdate-cell {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.age-badge {
|
||||
font-size: 0.75rem;
|
||||
background-color: #e9ecef;
|
||||
color: #6c757d;
|
||||
padding: 0.2em 0.6em;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Пол */
|
||||
.sex-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.3em 0.6em;
|
||||
}
|
||||
|
||||
.sex-male {
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.sex-female {
|
||||
background-color: rgba(232, 62, 140, 0.1);
|
||||
color: #e83e8c;
|
||||
}
|
||||
|
||||
/* Уведомления */
|
||||
.alert-fixed {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.modal-content {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.specialty-badge {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Пустая таблица */
|
||||
.text-center.py-5 {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Информация о посте */
|
||||
#postInfo .alert {
|
||||
border-left: 4px solid #0dcaf0;
|
||||
}
|
||||
|
||||
/* Иконки в таблице */
|
||||
.icon-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Месяц и дата рождения */
|
||||
.month-day {
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
/* Зодиак (опционально) */
|
||||
.zodiac-badge {
|
||||
font-size: 0.7rem;
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* Скроллбар */
|
||||
.table-responsive::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
@@ -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", "Ошибка соединения с сервером");
|
||||
}
|
||||
});
|
||||
@@ -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 = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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, '');
|
||||
|
||||
Reference in New Issue
Block a user