This commit is contained in:
2025-12-14 18:48:06 +03:00
parent ccec507033
commit 1a51349eed
17 changed files with 1081 additions and 134 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

+847 -43
View File
@@ -181,7 +181,7 @@ function prepareTabs() {
<div class="card-body py-2 col-12 col-md-3">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-3 rounded me-3">
<div class="bg-primary bg-opacity-10 p-3 rounded mx-3">
<i class="${tabData.icon} fs-3 text-primary"></i>
</div>
<div>
@@ -267,7 +267,7 @@ function fillTab(tabId, tabData) {
renderJurnalServicesTab(tabId, tabData);
break;
case 'users':
renderSimpleTab(tabId, tabData, 'Пользователи системы');
renderUsersTab(tabId, tabData);
break;
}
} catch (error) {
@@ -282,35 +282,6 @@ function fillTab(tabId, tabData) {
}
}
function renderSimpleTab(tabId, tabData, title) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
tabContent.innerHTML = `
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title mb-3">${title}</h5>
<div class="row">
${Object.entries(tabData).map(([key, value]) => `
<div class="col-12 col-md-6 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="card-title mb-2">${key}</h6>
<p class="card-text">
${typeof value === 'object' ? JSON.stringify(value, null, 4) : value}
</p>
</div>
</div>
</div>
`).join('')}
</div>
</div>
</div>
</div>
</div>
`;
}
async function manageCategory(categoriesList) {
// Удаляем старое модальное окно, если оно существует
@@ -1421,7 +1392,7 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
data-toolid="${tool.id}">
<div class="position-relative">
<img src="${imageUrl}"
class="card-img-top toolkit-card-img"
class="card-img-top toolkit-card-img object-fit-cover"
alt="${tool.title || 'Инструмент'}"
onerror="this.src='static/images/tools/default.png'">
<span class="position-absolute top-0 end-0 m-2 category-badge bg-primary text-white rounded-pill">
@@ -3714,7 +3685,7 @@ async function showToolkitDetailsModal(toolkitId) {
<a href="${img}" data-lightbox="toolkit-${toolkitData.id}" data-title="${toolkitData.title}">
<img src="${img}" alt="${toolkitData.title}"
class="d-block w-100 rounded mb-3"
style="max-height: 300px; object-fit: contain; cursor: zoom-in;">
style="max-height: 300px; object-fit: cover; cursor: zoom-in;">
</a>
</div>
`).join('')}
@@ -3747,7 +3718,7 @@ async function showToolkitDetailsModal(toolkitId) {
<a href="${images[0]}" data-lightbox="toolkit-${toolkitData.id}" data-title="${toolkitData.title}">
<img src="${images[0]}" alt="${toolkitData.title}"
class="img-fluid rounded mb-3"
style="max-height: 300px; object-fit: contain; cursor: zoom-in;">
style="max-height: 300px; object-fit: cover; cursor: zoom-in;">
</a>
</div>
` : '<div class="col-md-4"></div>';
@@ -4272,7 +4243,7 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
<div class="position-relative">
<img src="${mainImagePreview}"
class="img-fluid rounded mb-2"
style="max-height: 150px; object-fit: contain;">
style="max-height: 150px; object-fit: cover;">
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 mt-1 me-1"
id="removeMainImageBtn" title="Заменить">
<i class="bi bi-arrow-repeat"></i>
@@ -4342,7 +4313,6 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
const mainImageDropZone = modal.querySelector('#mainImageDropZone');
const mainImageInput = modal.querySelector('#mainImageInput');
const mainImageContent = modal.querySelector('#mainImageContent');
const removeMainImageBtn = modal.querySelector('#removeMainImageBtn');
const additionalImagesContainer = modal.querySelector('#additionalImagesContainer');
const addAdditionalImageBtn = modal.querySelector('#addAdditionalImageBtn');
const additionalImagesDropZone = modal.querySelector('#additionalImagesDropZone');
@@ -4351,7 +4321,6 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
const addSpecBtn = modal.querySelector('#addSpecBtn');
const submitBtn = modal.querySelector('#submitToolkitBtn');
const spinner = modal.querySelector('#submitToolkitSpinner');
const submitText = modal.querySelector('#submitToolkitText');
const errorDiv = modal.querySelector('#manageToolkitError');
const errorMessage = modal.querySelector('#manageToolkitErrorMessage');
@@ -4384,7 +4353,7 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
<div class="position-relative">
<img src="${mainImagePreview}"
class="img-fluid rounded mb-2"
style="max-height: 150px; object-fit: contain;">
style="max-height: 150px; object-fit: cover;">
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 mt-1 me-1"
id="removeMainImageBtn" title="Заменить">
<i class="bi bi-arrow-repeat"></i>
@@ -4475,7 +4444,7 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
<div class="col-3">
<img src="${image.preview}"
class="img-fluid rounded"
style="height: 60px; object-fit: contain; background-color: ${!image.isFile ? '#f8f9fa' : 'transparent'}">
style="height: 60px; object-fit: cover; background-color: ${!image.isFile ? '#f8f9fa' : 'transparent'}">
</div>
<div class="col-7">
<div class="small">
@@ -6127,10 +6096,12 @@ function renderJurnalServicesTab(tabId, tabData) {
// Форматируем детали в зависимости от типа действия
let detailsHtml = '';
if (actionType === 'Авторизован пользователь') {
if (actionType.includes('пользователь')) {
// Для авторизации
detailsHtml = `
<div class="fw-semibold">${actionData}</div>
<div class="fw-semibold">
${typeof actionData === 'object' ? Object.entries(actionData).map(([key, value]) => `<div>${key}: <span class="small fw-normal">${value}</span></div>`).join('') : actionData}
</div>
`;
} else if (actionType.includes('Добавлен') || actionType.includes('Обновлен') || actionType.includes('Добавлена')) {
// Для добавления/обновления сущностей
@@ -6174,12 +6145,12 @@ function renderJurnalServicesTab(tabId, tabData) {
if (actionData.image) {
detailsHtml += `<div class="mt-2, fw-bold">Изображения:</div>`;
detailsHtml += `<div class="small text-muted">Основное:<div>`;
detailsHtml += `<div class="mt-2"><img src="${actionData.image.main}" class="img-thumbnail" style="width: 64px; height: 64px;" alt="Основное изображение инструмента">`;
detailsHtml += `<div class="mt-2"><img src="${actionData.image.main}" class="img-thumbnail object-fit-cover" style="width: 64px; height: 64px;" alt="Основное изображение инструмента">`;
if (actionData.image.additional) {
detailsHtml += `<div class="small text-muted">Дополнительные:<div>`;
detailsHtml += `<div class="d-flex mt-2">`;
actionData.image.additional.forEach(img => {
detailsHtml += `<div><img src="${img}" class="img-thumbnail" style="width: 64px; height: 64px;" alt="Дополнительное изображение инструмента"></div>`;
detailsHtml += `<div><img src="${img}" class="img-thumbnail object-fit-cover" style="width: 64px; height: 64px;" alt="Дополнительное изображение инструмента"></div>`;
});
detailsHtml += `</div>`;
}
@@ -6227,6 +6198,839 @@ function renderJurnalServicesTab(tabId, tabData) {
renderServicesTable();
}
function renderUsersTab(tabId, tabData) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
const { users, accessLevels } = tabData;
users.sort((a, b) => a.username.localeCompare(b.username));
// Создаем мапу уровней доступа для быстрого доступа
const accessLevelMap = {};
const accessLevelTitles = [];
accessLevels.forEach(level => {
accessLevelMap[level.id] = level.title;
accessLevelTitles.push(level.title);
});
const userUsernames = [];
const userLogins = [];
users.forEach(user => {
userUsernames.push(user.username);
userLogins.push(user.login);
});
// Рендерим опциональный блок с кнопками
tabOptionalContent.innerHTML = `
<div class="row align-items-center mb-3">
<div class="col-12">
<div class="d-flex flex-wrap gap-2">
${accessData.users_creation ? `
<button class="btn btn-success" id="${tabId}-add-user-btn">
<i class="bi bi-person-plus me-1"></i>Добавить пользователя
</button>
` : ''}
${accessData.access_level_view ? `
<button class="btn btn-outline-secondary" id="${tabId}-access-levels-btn">
<i class="bi bi-shield-lock me-1"></i>Уровни доступа
</button>
` : ''}
</div>
</div>
</div>
`;
// Функция для показа модального окна с подтверждением
function showConfirmationModal(title, message, confirmCallback) {
const modalId = `${tabId}-confirmation-modal`;
// Удаляем старую модалку, если есть
const existingModal = document.getElementById(modalId);
if (existingModal) {
existingModal.remove();
}
const modalHTML = `
<div class="modal fade" id="${modalId}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>${message}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-danger" id="${modalId}-confirm-btn">Подтвердить</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
const modal = new bootstrap.Modal(document.getElementById(modalId));
document.getElementById(`${modalId}-confirm-btn`).addEventListener('click', () => {
confirmCallback();
modal.hide();
});
modal.show();
// Удаляем модалку после скрытия
modal._element.addEventListener('hidden.bs.modal', () => {
modal._element.remove();
});
}
// Функция для рендеринга карточек пользователей
function renderUsersCards() {
if (!users || users.length === 0) {
tabContent.innerHTML = `
<div class="alert alert-info m-4" role="alert">
<i class="bi bi-info-circle me-2"></i>
Нет пользователей в системе
</div>
`;
return;
}
tabContent.innerHTML = `
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4" id="${tabId}-users-container">
${users.map(user => {
const isActive = user.is_active;
const levelTitle = accessLevelMap[user.access_level_id] || `Уровень ${user.access_level_id}`;
const createdAt = new Date(user.created_at).toLocaleDateString('ru-RU');
return `
<div class="col">
<div class="card h-100 ${!isActive ? 'bg-light opacity-75 border-2 border-danger' : 'border'}"
data-user-id="${user.id}" id="${tabId}-user-card-${user.id}">
<div class="card-body">
<div class="d-flex align-items-start mb-3">
<img src="${user.photo || 'static/images/users/default.png'}"
alt="${user.username}"
class="rounded-circle me-3 border object-fit-cover" width="60" height="60"
onerror="this.src='static/images/users/default.png'">
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="card-title mb-1">${user.username || 'Без имени'}</h5>
<h6 class="card-subtitle text-muted mb-2">${user.login || 'Без логина'}</h6>
</div>
</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-shield me-2"></i>
<span>${levelTitle}</span>
</div>
<div class="d-flex align-items-center mb-2">
<i class="bi bi-calendar me-2"></i>
<small class="text-muted">Создан: ${createdAt}</small>
</div>
<div class="d-flex align-items-center">
<i class="bi bi-clock-history me-2"></i>
<small class="text-muted">Обновлен: ${user.updated_at ? new Date(user.updated_at).toLocaleDateString('ru-RU') : 'Нет данных'}</small>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
${accessData.users_edit ? `
<button class="btn btn-sm btn-outline-primary edit-user-btn"
data-user-id="${user.id}">
<i class="bi bi-pencil"></i> Изменить
</button>
` : ''}
${accessData.users_disabling ? `
<button class="btn btn-sm ${isActive ? 'btn-warning' : 'btn-success'} toggle-user-btn"
data-user-id="${user.id}" data-is-active="${isActive}">
<i class="bi ${isActive ? 'bi-person-slash' : 'bi-person-check'}"></i>
${isActive ? 'Заблокировать' : 'Разблокировать'}
</button>
` : ''}
</div>
</div>
</div>
</div>
`;
}).join('')}
</div>
`;
// Назначаем обработчики для кнопок в карточках
document.querySelectorAll(`.edit-user-btn`).forEach(btn => {
btn.addEventListener('click', async () => {
const userId = btn.dataset.userId;
const user = users.find(u => u.id == userId);
if (user) {
await openEditUserModal(user);
}
});
});
document.querySelectorAll(`.toggle-user-btn`).forEach(btn => {
btn.addEventListener('click', async () => {
const userId = btn.dataset.userId;
const isActive = btn.dataset.isActive === 'true';
const user = users.find(u => u.id == userId);
if (user) {
const action = isActive ? 'заблокировать' : 'разблокировать';
showConfirmationModal(
`${isActive ? 'Блокировка' : 'Разблокировка'} пользователя`,
`Вы уверены, что хотите ${action} пользователя ${user.username}?`,
() => toggleUserStatus(userId, !isActive)
);
}
});
});
}
// Функция для открытия модального окна редактирования пользователя
async function openEditUserModal(user = null) {
const isNew = !user;
const modalId = `${tabId}-edit-user-modal`;
// Удаляем старую модалку, если есть
const existingModal = document.getElementById(modalId);
if (existingModal) {
existingModal.remove();
}
const modalHTML = `
<div class="modal fade" id="${modalId}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${isNew ? 'Добавить пользователя' : 'Редактировать пользователя'}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="${modalId}-form">
<div class="text-center mb-3">
<img src="${user?.photo || 'static/images/users/default.png'}"
alt="Фото пользователя"
class="rounded-circle border object-fit-cover"
width="100" height="100"
id="${modalId}-photo-preview"
onerror="this.src='static/images/users/default.png'">
${accessData.users_edit ? `
<div class="mt-2">
<input type="file"
class="form-control form-control-sm"
id="${modalId}-photo-input"
accept="image/*"
style="display: none;">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" id="${modalId}-change-photo-btn">
<i class="bi bi-upload"></i> Изменить
</button>
${user?.photo && !user.photo.includes('default.png') ? `
<button type="button" class="btn btn-outline-danger" id="${modalId}-remove-photo-btn">
<i class="bi bi-trash"></i> Удалить
</button>
` : ''}
</div>
</div>
` : ''}
</div>
<div class="mb-3">
<label for="${modalId}-login" class="form-label">Логин *</label>
<input type="text" class="form-control" id="${modalId}-login"
value="${user?.login || ''}" required>
</div>
<div class="mb-3">
<label for="${modalId}-username" class="form-label">Имя пользователя *</label>
<input type="text" class="form-control" id="${modalId}-username"
value="${user?.username || ''}" required>
</div>
<div class="mb-3">
<label for="${modalId}-password" class="form-label">${isNew ? 'Пароль *' : 'Новый пароль'}</label>
<input type="password" class="form-control" id="${modalId}-password"
${isNew ? 'required' : ''}
placeholder="${isNew ? '' : 'Оставьте пустым, если не нужно менять'}">
${!isNew ? '<div class="form-text">Оставьте пустым, если не нужно менять пароль</div>' : ''}
</div>
<div>
<label for="${modalId}-access-level" class="form-label">Уровень доступа *</label>
<select class="form-select" id="${modalId}-access-level" required>
${accessLevels.map(level => `
<option value="${level.id}" ${user?.access_level_id == level.id ? 'selected' : ''}>
${level.title}
</option>
`).join('')}
</select>
</div>
</form>
</div>
<div class="modal-footer">
${!isNew && accessData.users_disabling ? `
<button type="button" class="btn btn-danger me-auto" id="${modalId}-delete-btn">
<i class="bi bi-trash"></i> Удалить
</button>
` : ''}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="${modalId}-save-btn">Сохранить</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
const modal = new bootstrap.Modal(document.getElementById(modalId));
let photoFile = null;
let removePhoto = false;
// Обработчики для фото
if (accessData.users_edit) {
const changePhotoBtn = document.getElementById(`${modalId}-change-photo-btn`);
const photoInput = document.getElementById(`${modalId}-photo-input`);
const photoPreview = document.getElementById(`${modalId}-photo-preview`);
changePhotoBtn.addEventListener('click', () => {
photoInput.click();
});
photoInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
if (file.type.startsWith('image/')) {
photoFile = file;
const reader = new FileReader();
reader.onload = (e) => {
photoPreview.src = e.target.result;
};
reader.readAsDataURL(file);
removePhoto = false;
} else {
showConfirmationModal('Ошибка', 'Пожалуйста, выберите файл изображения', () => { });
photoInput.value = '';
}
}
});
if (document.getElementById(`${modalId}-remove-photo-btn`)) {
document.getElementById(`${modalId}-remove-photo-btn`).addEventListener('click', () => {
photoPreview.src = 'static/images/users/default.png';
photoFile = null;
removePhoto = true;
});
}
}
// Обработчик удаления пользователя
if (!isNew && accessData.users_disabling) {
document.getElementById(`${modalId}-delete-btn`).addEventListener('click', () => {
modal.hide();
showConfirmationModal(
'Удаление пользователя',
`Вы уверены, что хотите удалить пользователя ${user.username}? Это действие нельзя отменить.`,
() => deleteUser(user.id)
);
});
}
// Обработчик сохранения
document.getElementById(`${modalId}-save-btn`).addEventListener('click', async () => {
const form = document.getElementById(`${modalId}-form`);
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formUserData = {
login: document.getElementById(`${modalId}-login`).value.trim(),
username: document.getElementById(`${modalId}-username`).value.trim(),
access_level_id: parseInt(document.getElementById(`${modalId}-access-level`).value),
};
if (isNew || ((user && formUserData.login != user.login))) {
if (userLogins.includes(formUserData.login)) {
showInfo('Пользователь с таким логином уже существует', 'warning');
return;
}
}
if (isNew || ((user && formUserData.username != user.username))) {
if (userUsernames.includes(formUserData.username)) {
showInfo('Пользователь с таким именем уже существует', 'warning');
return;
}
}
// Добавляем пароль, если он указан
const password = document.getElementById(`${modalId}-password`).value;
if (password || isNew) {
formUserData.password = password || '';
}
// Определяем действие
let action = isNew ? 'create' : 'update';
// Обработка фото
if (photoFile) {
if (photoFile.size > 5 * 1024 * 1024) {
showInfo('Фото больше 5 МБ, оно не подходит', 'warning');
return;
}
formUserData.photo = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error('Ошибка чтения файла'));
reader.readAsDataURL(photoFile);
});
} else if (removePhoto) {
formUserData.photo = '';
}
let changedUserData = {};
if (isNew) {
changedUserData = formUserData;
} else {
changedUserData = Object.keys(formUserData).reduce((acc, key) => {
if (formUserData[key] !== user[key]) {
acc[key] = formUserData[key];
}
return acc;
}, {});
}
if (Object.keys(changedUserData).length === 0) {
showInfo('Нет изменений', 'info');
return;
}
// Обновляем данные
if (!isNew) {
changedUserData.id = user.id;
}
await sendUserRequest(changedUserData, modal, action);
});
modal.show();
// Удаляем модалку после скрытия
modal._element.addEventListener('hidden.bs.modal', () => {
modal._element.remove();
});
}
// Функция для обновления данных
async function refreshThisTab() {
// Обновляем данные
const newData = await apiRequest('/', { tabId: 'users' });
if (newData && newData.status === 'ok') {
renderUsersTab(tabId, newData.data);
} else {
showInfo('Не удалось загрузить обновленные данные', 'error');
}
}
// Функция для отправки запроса на изменение пользователя
const currentUserId = userData.id;
async function sendUserRequest(userData, modal, action) {
const result = await apiRequest('/user/', { action, userData, userId: currentUserId });
const actionTextMap = {
create: 'создан',
update: 'обновлен',
remove_photo: 'обновлен',
delete: 'удален'
}
if (result && result.status === 'ok') {
if (modal) { modal.hide(); }
showInfo('Пользователь успешно ' + actionTextMap[action], 'success');
await refreshThisTab();
} else {
const errorMsg = result?.message || 'Произошла ошибка при ' + actionTextMap[action] + 'ии';
showInfo(errorMsg, 'error');
}
}
// Функция для удаления пользователя
async function deleteUser(userId) {
await sendUserRequest({ id: userId }, null, 'delete');
}
// Функция для блокировки/разблокировки пользователя
async function toggleUserStatus(userId, newStatus) {
await sendUserRequest({ id: userId, is_active: newStatus }, null, 'update');
}
// Функция для открытия модального окна управления уровнями доступа
async function openAccessLevelsModal() {
const modalId = `${tabId}-access-levels-modal`;
// Удаляем старую модалку, если есть
const existingModal = document.getElementById(modalId);
if (existingModal) {
existingModal.remove();
}
// Создаем копию уровней доступа для редактирования
const editableLevels = JSON.parse(JSON.stringify(accessLevels));
let newLevelMode = false;
let currentLevelId = editableLevels[0]?.id;
const modalHTML = `
<div class="modal fade" id="${modalId}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Управление уровнями доступа</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-4">
<div class="d-flex flex-wrap gap-2 mb-3" id="${modalId}-level-tabs">
${editableLevels.map((level, index) => `
<button class="btn ${index === 0 ? 'btn-primary' : 'btn-outline-primary'} level-tab-btn"
data-level-id="${level.id}">
${level.title}
</button>
`).join('')}
${accessData.access_level_edit ? `
<button class="btn btn-outline-success" id="${modalId}-add-level-btn">
<i class="bi bi-plus-lg"></i> Добавить
</button>
` : ''}
</div>
</div>
<div id="${modalId}-level-content">
<!-- Содержимое уровня будет загружено динамически -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
${accessData.access_level_edit ? `
<button type="button" class="btn btn-primary" id="${modalId}-save-btn" style="display: none;">
Сохранить изменения
</button>
<button type="button" class="btn btn-success" id="${modalId}-add-save-btn" style="display: none;">
Добавить уровень доступа
</button>
` : ''}
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
const modal = new bootstrap.Modal(document.getElementById(modalId));
// Функция для загрузки содержимого уровня
function loadLevelContent(levelId, isNew = false) {
let level;
if (isNew) {
level = {
id: 'new',
title: '',
description: '',
tools_creation: false,
tools_registration: false,
tools_edit: false,
tools_delete: false,
users_creation: false,
users_edit: false,
users_disabling: false,
users_view: false,
available_own_toolbox: false,
view_all_toolboxes: false,
view_requests: false,
view_services: false,
access_level_view: false,
access_level_edit: false,
manage_toolboxes: false,
debit_request_confirm: false,
refund_request_confirm: false
};
} else {
level = editableLevels.find(l => l.id == levelId);
}
if (!level) return;
currentLevelId = level.id;
newLevelMode = isNew;
const contentDiv = document.getElementById(`${modalId}-level-content`);
const isEditable = accessData.access_level_edit;
contentDiv.innerHTML = `
<div class="mb-3">
<label class="form-label required">Название уровня</label>
${isNew || isEditable ? `
<input type="text" class="form-control" id="${modalId}-level-title"
value="${level.title}" ${isEditable ? '' : 'readonly'} required>
` : `
<div class="form-control-plaintext">${level.title}</div>
`}
</div>
<div class="mb-4">
<label class="form-label required">Описание</label>
${isNew || isEditable ? `
<textarea class="form-control" id="${modalId}-level-description"
rows="2" ${isEditable ? '' : 'readonly'}>${level.description || ''}</textarea>
` : `
<div class="form-control-plaintext">${level.description || 'Нет описания'}</div>
`}
</div>
<div class="row mt-4">
<div class="col-md-6">
<h6>Основные права:</h6>
<div class="list-group list-group-flush">
${generateCheckbox('tools_creation', 'Создание инструментов', level.tools_creation)}
${generateCheckbox('tools_edit', 'Редактирование инструментов', level.tools_edit)}
${generateCheckbox('users_view', 'Просмотр пользователей', level.users_view)}
${generateCheckbox('users_edit', 'Редактирование пользователей', level.users_edit)}
${generateCheckbox('users_disabling', 'Блокировка пользователей', level.users_disabling)}
${generateCheckbox('users_creation', 'Создание пользователей', level.users_creation)}
${generateCheckbox('access_level_view', 'Просмотр уровней доступа', level.access_level_view)}
${generateCheckbox('access_level_edit', 'Редактирование уровней доступа', level.access_level_edit)}
</div>
</div>
<div class="col-md-6">
<h6>Дополнительные права:</h6>
<div class="list-group list-group-flush">
${generateCheckbox('view_requests', 'Просмотр журнала движения', level.view_requests)}
${generateCheckbox('view_services', 'Просмотр сервисного журнала', level.view_services)}
${generateCheckbox('manage_toolboxes', 'Управление складами', level.manage_toolboxes)}
${generateCheckbox('available_own_toolbox', 'Есть собственный склад', level.available_own_toolbox)}
${generateCheckbox('tools_registration', 'Оприходование инструментов', level.tools_registration)}
${generateCheckbox('tools_delete', 'Удаление инструментов', level.tools_delete)}
${generateCheckbox('debit_request_confirm', 'Решение по списанию', level.debit_request_confirm)}
${generateCheckbox('refund_request_confirm', 'Решение по возврату', level.refund_request_confirm)}
${generateCheckbox('view_all_toolboxes', 'Просмотр всех складов', level.view_all_toolboxes)}
</div>
</div>
</div>
`;
// Вспомогательная функция для генерации чекбоксов
function generateCheckbox(name, label, checked) {
return `
<div class="list-group-item">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="${modalId}-${name}" ${checked ? 'checked' : ''}
${(isNew || isEditable) ? '' : 'disabled'}>
<label class="form-check-label" for="${modalId}-${name}">
${label}
</label>
</div>
</div>
`;
}
// Показываем/скрываем кнопки сохранения
document.getElementById(`${modalId}-save-btn`).style.display = (isEditable && !isNew) ? 'block' : 'none';
document.getElementById(`${modalId}-add-save-btn`).style.display = (isEditable && isNew) ? 'block' : 'none';
}
// Обработчики для вкладок уровней
document.querySelectorAll(`.level-tab-btn`).forEach((btn, index) => {
btn.addEventListener('click', () => {
// Обновляем активную вкладку
document.querySelectorAll(`.level-tab-btn`).forEach(b => {
b.classList.remove('btn-primary');
b.classList.add('btn-outline-primary');
});
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-primary');
// Загружаем содержимое уровня
const levelId = btn.dataset.levelId;
newLevelMode = false;
loadLevelContent(levelId, false);
});
});
// Обработчик для добавления нового уровня
if (accessData.access_level_edit) {
document.getElementById(`${modalId}-add-level-btn`).addEventListener('click', () => {
// Создаем новую вкладку
const tabsContainer = document.getElementById(`${modalId}-level-tabs`);
const newTabId = 'new-' + Date.now();
// Обновляем все вкладки
document.querySelectorAll(`.level-tab-btn`).forEach(b => {
b.classList.remove('btn-primary');
b.classList.add('btn-outline-primary');
});
// Добавляем новую вкладку
const newTab = document.createElement('button');
newTab.className = 'btn btn-primary level-tab-btn';
newTab.textContent = 'Новый уровень';
newTab.dataset.levelId = newTabId;
newTab.addEventListener('click', () => {
document.querySelectorAll(`.level-tab-btn`).forEach(b => {
b.classList.remove('btn-primary');
b.classList.add('btn-outline-primary');
});
newTab.classList.remove('btn-outline-primary');
newTab.classList.add('btn-primary');
loadLevelContent(newTabId, true);
});
// Убираем кнопку добавления
document.getElementById(`${modalId}-add-level-btn`).style.display = 'none';
tabsContainer.appendChild(newTab);
loadLevelContent(newTabId, true);
});
}
async function sendLevelData(isNew) {
const level = editableLevels.find(l => l.id == currentLevelId);
if (!level && !isNew) return;
// Собираем данные из формы
const levelData = {
title: document.getElementById(`${modalId}-level-title`).value,
description: document.getElementById(`${modalId}-level-description`).value
};
if (!levelData.title.trim() || (!isNew && levelData.title !== level.title && accessLevelTitles.includes(levelData.title)) || (isNew && accessLevelTitles.includes(levelData.title))) {
showInfo('Ошибка при сохранении', 'error');
showInfo('Введите уникальное название уровня доступа', 'warning');
return;
}
if (!levelData.description.trim()) {
showInfo('Ошибка при сохранении', 'error');
showInfo('Введите описание уровня доступа', 'warning');
return;
}
if (!isNew) {
levelData.id = level.id;
}
// Проверяем обязательные поля и уникальность
if (isNew) {
if (!levelData.title.trim() || editableLevels.includes(levelData.title)) {
showInfo('Ошибка при добавлении', 'error');
showInfo('Введите уникальное название уровня доступа', 'warning');
return;
}
} else {
if (levelData.title !== level.title && (!levelData.title.trim() || editableLevels.includes(levelData.title))) {
showInfo('Ошибка при сохранении', 'error');
showInfo('Введите уникальное название уровня доступа', 'warning');
return;
}
}
// Собираем все чекбоксы
const checkboxNames = [
'tools_creation', 'tools_registration', 'tools_edit', 'tools_delete',
'users_creation', 'users_edit', 'users_disabling', 'users_view',
'available_own_toolbox', 'view_all_toolboxes', 'view_requests',
'view_services', 'access_level_view', 'access_level_edit',
'manage_toolboxes', 'debit_request_confirm', 'refund_request_confirm'
];
checkboxNames.forEach(name => {
levelData[name] = document.getElementById(`${modalId}-${name}`).checked;
});
let changedLevelData = {};
if (isNew) {
changedLevelData = levelData;
} else {
changedLevelData = Object.keys(levelData).reduce((acc, key) => {
if (levelData[key] !== level[key]) {
acc[key] = levelData[key];
}
return acc;
}, {});
}
if (Object.keys(changedLevelData).length === 0) {
showInfo('Нет изменений', 'info');
return;
}
if (!isNew) {
changedLevelData.id = level.id;
}
const result = await apiRequest('/user/level', { action: isNew ? 'create' : 'update', changedLevelData, userId: currentUserId });
if (result && result.status === 'ok') {
modal.hide();
showInfo('Данные сохранены', 'success');
// Обновляем данные
await refreshThisTab();
} else {
const errorMsg = result?.message || 'Произошла ошибка при сохранении';
showInfo(errorMsg, 'error');
}
}
// Обработчик для сохранения изменений существующего уровня
if (accessData.access_level_edit) {
document.getElementById(`${modalId}-save-btn`).addEventListener('click', async () => {
await sendLevelData(newLevelMode);
});
// Обработчик для добавления нового уровня
document.getElementById(`${modalId}-add-save-btn`).addEventListener('click', async () => {
await sendLevelData(newLevelMode);
});
}
// Загружаем первый уровень по умолчанию
if (editableLevels.length > 0) {
loadLevelContent(editableLevels[0].id);
}
modal.show();
// Удаляем модалку после скрытия
modal._element.addEventListener('hidden.bs.modal', () => {
modal._element.remove();
});
}
// Обработчики для кнопок в опциональном блоке
if (accessData.users_creation) {
document.getElementById(`${tabId}-add-user-btn`).addEventListener('click', () => {
openEditUserModal();
});
}
if (accessData.access_level_view) {
document.getElementById(`${tabId}-access-levels-btn`).addEventListener('click', () => {
openAccessLevelsModal();
});
}
// Первоначальный рендеринг карточек
renderUsersCards();
}
document.addEventListener('DOMContentLoaded', async () => {
await getCookieData();