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
+14 -3
View File
@@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta
from fastapi import APIRouter, Depends, Request
from fastapi.responses import RedirectResponse
from db.handlers.access import AccessLevelHandler
from db.handlers.categories import CategoryHandler
from utils import render, requestDict, logger
from .user import router as user
@@ -181,14 +182,24 @@ async def post_requests(
"startDate": startDate.strftime("%Y-%m-%d"),
"endDate": endDate.strftime("%Y-%m-%d"),
}
# logger.info(resultData.get("data"))
case "users":
users = await UserHandler.getAll()
if users:
if isinstance(users, list):
if len(users) == 0:
resultData["status"] = "ok"
resultData["data"] = {
"users": [],
"accessLevels": [],
}
for user in users:
user.pop("hashed_password")
accessLevels = await AccessLevelHandler.getAll()
resultData["status"] = "ok"
resultData["data"] = users
resultData["data"] = {
"users": users,
"accessLevels": accessLevels,
}
# logger.info(resultData)
case _:
pass
return resultData
Binary file not shown.
Binary file not shown.
+56 -3
View File
@@ -12,9 +12,62 @@ async def get_user():
return
@router.post("/")
async def create_user():
return
@router.post("/", summary="Правка данных пользователя")
async def manage_user(request_data: dict = Depends(requestDict)):
response = {"status": "error"}
userData = request_data.get("body").get("userData", {})
action = request_data.get("body").get("action")
userId = request_data.get("body").get("userId")
match action:
case "create":
result = await UserHandler.add(userData, userId)
if result:
response["status"] = "ok"
case "update":
result = await UserHandler.edit(userData, user_id=userId)
if "error" not in result:
response["status"] = "ok"
else:
response["message"] = result["error"]
case "delete":
result = await UserHandler.delete(userData["id"], userId)
if "error" not in result:
response["status"] = "ok"
else:
response["message"] = result["error"]
case _:
logger.error(f"Неверное действие: {action}")
return response
@router.post("/level", summary="Правка уровня доступа")
async def manage_access_level(request_data: dict = Depends(requestDict)):
logger.info(request_data.get("body"))
action = request_data.get("body").get("action")
userId = request_data.get("body").get("userId")
levelData = request_data.get("body").get("changedLevelData")
match action:
case "create":
result = await AccessLevelHandler.add(levelData, userId)
if "error" not in result:
return {"status": "ok"}
else:
return {"status": "error", "message": result["error"]}
case "update":
result = await AccessLevelHandler.edit(levelData, userId)
if "error" not in result:
return {"status": "ok"}
else:
return {"status": "error", "message": result["error"]}
case "delete":
result = await AccessLevelHandler.delete(levelData["id"], userId)
if "error" not in result:
return {"status": "ok"}
else:
return {"status": "error", "message": result["error"]}
case _:
logger.error(f"Неверное действие: {action}")
return {"status": "ok"}
@router.get("/login", name="Authentication", summary="Авторизация пользователя")
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();