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
+270
View File
@@ -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;
}
+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, '');