final
This commit is contained in:
+847
-43
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user