final
This commit is contained in:
+14
-3
@@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta
|
|||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
|
from db.handlers.access import AccessLevelHandler
|
||||||
from db.handlers.categories import CategoryHandler
|
from db.handlers.categories import CategoryHandler
|
||||||
from utils import render, requestDict, logger
|
from utils import render, requestDict, logger
|
||||||
from .user import router as user
|
from .user import router as user
|
||||||
@@ -181,14 +182,24 @@ async def post_requests(
|
|||||||
"startDate": startDate.strftime("%Y-%m-%d"),
|
"startDate": startDate.strftime("%Y-%m-%d"),
|
||||||
"endDate": endDate.strftime("%Y-%m-%d"),
|
"endDate": endDate.strftime("%Y-%m-%d"),
|
||||||
}
|
}
|
||||||
# logger.info(resultData.get("data"))
|
|
||||||
case "users":
|
case "users":
|
||||||
users = await UserHandler.getAll()
|
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:
|
for user in users:
|
||||||
user.pop("hashed_password")
|
user.pop("hashed_password")
|
||||||
|
accessLevels = await AccessLevelHandler.getAll()
|
||||||
resultData["status"] = "ok"
|
resultData["status"] = "ok"
|
||||||
resultData["data"] = users
|
resultData["data"] = {
|
||||||
|
"users": users,
|
||||||
|
"accessLevels": accessLevels,
|
||||||
|
}
|
||||||
|
# logger.info(resultData)
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
return resultData
|
return resultData
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+56
-3
@@ -12,9 +12,62 @@ async def get_user():
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/", summary="Правка данных пользователя")
|
||||||
async def create_user():
|
async def manage_user(request_data: dict = Depends(requestDict)):
|
||||||
return
|
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="Авторизация пользователя")
|
@router.get("/login", name="Authentication", summary="Авторизация пользователя")
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 338 KiB |
+847
-43
@@ -181,7 +181,7 @@ function prepareTabs() {
|
|||||||
|
|
||||||
<div class="card-body py-2 col-12 col-md-3">
|
<div class="card-body py-2 col-12 col-md-3">
|
||||||
<div class="d-flex align-items-center">
|
<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>
|
<i class="${tabData.icon} fs-3 text-primary"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -267,7 +267,7 @@ function fillTab(tabId, tabData) {
|
|||||||
renderJurnalServicesTab(tabId, tabData);
|
renderJurnalServicesTab(tabId, tabData);
|
||||||
break;
|
break;
|
||||||
case 'users':
|
case 'users':
|
||||||
renderSimpleTab(tabId, tabData, 'Пользователи системы');
|
renderUsersTab(tabId, tabData);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) {
|
async function manageCategory(categoriesList) {
|
||||||
// Удаляем старое модальное окно, если оно существует
|
// Удаляем старое модальное окно, если оно существует
|
||||||
@@ -1421,7 +1392,7 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
|
|||||||
data-toolid="${tool.id}">
|
data-toolid="${tool.id}">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<img src="${imageUrl}"
|
<img src="${imageUrl}"
|
||||||
class="card-img-top toolkit-card-img"
|
class="card-img-top toolkit-card-img object-fit-cover"
|
||||||
alt="${tool.title || 'Инструмент'}"
|
alt="${tool.title || 'Инструмент'}"
|
||||||
onerror="this.src='static/images/tools/default.png'">
|
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">
|
<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}">
|
<a href="${img}" data-lightbox="toolkit-${toolkitData.id}" data-title="${toolkitData.title}">
|
||||||
<img src="${img}" alt="${toolkitData.title}"
|
<img src="${img}" alt="${toolkitData.title}"
|
||||||
class="d-block w-100 rounded mb-3"
|
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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
@@ -3747,7 +3718,7 @@ async function showToolkitDetailsModal(toolkitId) {
|
|||||||
<a href="${images[0]}" data-lightbox="toolkit-${toolkitData.id}" data-title="${toolkitData.title}">
|
<a href="${images[0]}" data-lightbox="toolkit-${toolkitData.id}" data-title="${toolkitData.title}">
|
||||||
<img src="${images[0]}" alt="${toolkitData.title}"
|
<img src="${images[0]}" alt="${toolkitData.title}"
|
||||||
class="img-fluid rounded mb-3"
|
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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
` : '<div class="col-md-4"></div>';
|
` : '<div class="col-md-4"></div>';
|
||||||
@@ -4272,7 +4243,7 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
|
|||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<img src="${mainImagePreview}"
|
<img src="${mainImagePreview}"
|
||||||
class="img-fluid rounded mb-2"
|
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"
|
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 mt-1 me-1"
|
||||||
id="removeMainImageBtn" title="Заменить">
|
id="removeMainImageBtn" title="Заменить">
|
||||||
<i class="bi bi-arrow-repeat"></i>
|
<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 mainImageDropZone = modal.querySelector('#mainImageDropZone');
|
||||||
const mainImageInput = modal.querySelector('#mainImageInput');
|
const mainImageInput = modal.querySelector('#mainImageInput');
|
||||||
const mainImageContent = modal.querySelector('#mainImageContent');
|
const mainImageContent = modal.querySelector('#mainImageContent');
|
||||||
const removeMainImageBtn = modal.querySelector('#removeMainImageBtn');
|
|
||||||
const additionalImagesContainer = modal.querySelector('#additionalImagesContainer');
|
const additionalImagesContainer = modal.querySelector('#additionalImagesContainer');
|
||||||
const addAdditionalImageBtn = modal.querySelector('#addAdditionalImageBtn');
|
const addAdditionalImageBtn = modal.querySelector('#addAdditionalImageBtn');
|
||||||
const additionalImagesDropZone = modal.querySelector('#additionalImagesDropZone');
|
const additionalImagesDropZone = modal.querySelector('#additionalImagesDropZone');
|
||||||
@@ -4351,7 +4321,6 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
|
|||||||
const addSpecBtn = modal.querySelector('#addSpecBtn');
|
const addSpecBtn = modal.querySelector('#addSpecBtn');
|
||||||
const submitBtn = modal.querySelector('#submitToolkitBtn');
|
const submitBtn = modal.querySelector('#submitToolkitBtn');
|
||||||
const spinner = modal.querySelector('#submitToolkitSpinner');
|
const spinner = modal.querySelector('#submitToolkitSpinner');
|
||||||
const submitText = modal.querySelector('#submitToolkitText');
|
|
||||||
const errorDiv = modal.querySelector('#manageToolkitError');
|
const errorDiv = modal.querySelector('#manageToolkitError');
|
||||||
const errorMessage = modal.querySelector('#manageToolkitErrorMessage');
|
const errorMessage = modal.querySelector('#manageToolkitErrorMessage');
|
||||||
|
|
||||||
@@ -4384,7 +4353,7 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
|
|||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<img src="${mainImagePreview}"
|
<img src="${mainImagePreview}"
|
||||||
class="img-fluid rounded mb-2"
|
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"
|
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 mt-1 me-1"
|
||||||
id="removeMainImageBtn" title="Заменить">
|
id="removeMainImageBtn" title="Заменить">
|
||||||
<i class="bi bi-arrow-repeat"></i>
|
<i class="bi bi-arrow-repeat"></i>
|
||||||
@@ -4475,7 +4444,7 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
|
|||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<img src="${image.preview}"
|
<img src="${image.preview}"
|
||||||
class="img-fluid rounded"
|
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>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<div class="small">
|
<div class="small">
|
||||||
@@ -6127,10 +6096,12 @@ function renderJurnalServicesTab(tabId, tabData) {
|
|||||||
// Форматируем детали в зависимости от типа действия
|
// Форматируем детали в зависимости от типа действия
|
||||||
let detailsHtml = '';
|
let detailsHtml = '';
|
||||||
|
|
||||||
if (actionType === 'Авторизован пользователь') {
|
if (actionType.includes('пользователь')) {
|
||||||
// Для авторизации
|
// Для авторизации
|
||||||
detailsHtml = `
|
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('Добавлена')) {
|
} else if (actionType.includes('Добавлен') || actionType.includes('Обновлен') || actionType.includes('Добавлена')) {
|
||||||
// Для добавления/обновления сущностей
|
// Для добавления/обновления сущностей
|
||||||
@@ -6174,12 +6145,12 @@ function renderJurnalServicesTab(tabId, tabData) {
|
|||||||
if (actionData.image) {
|
if (actionData.image) {
|
||||||
detailsHtml += `<div class="mt-2, fw-bold">Изображения:</div>`;
|
detailsHtml += `<div class="mt-2, fw-bold">Изображения:</div>`;
|
||||||
detailsHtml += `<div class="small text-muted">Основное:<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) {
|
if (actionData.image.additional) {
|
||||||
detailsHtml += `<div class="small text-muted">Дополнительные:<div>`;
|
detailsHtml += `<div class="small text-muted">Дополнительные:<div>`;
|
||||||
detailsHtml += `<div class="d-flex mt-2">`;
|
detailsHtml += `<div class="d-flex mt-2">`;
|
||||||
actionData.image.additional.forEach(img => {
|
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>`;
|
detailsHtml += `</div>`;
|
||||||
}
|
}
|
||||||
@@ -6227,6 +6198,839 @@ function renderJurnalServicesTab(tabId, tabData) {
|
|||||||
renderServicesTable();
|
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 () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await getCookieData();
|
await getCookieData();
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
-13
@@ -6,21 +6,20 @@ from db.handlers.records import ServiceRecordsHandler
|
|||||||
|
|
||||||
|
|
||||||
class AccessLevelHandler:
|
class AccessLevelHandler:
|
||||||
async def add(newData):
|
async def add(newData, userId: int = None):
|
||||||
title = newData.get("title", None)
|
title = newData.get("title", None)
|
||||||
if not title:
|
if not title:
|
||||||
logger.error("Не указано название уровня доступа")
|
logger.error("Не указано название уровня доступа")
|
||||||
return {}
|
return {"error": "Не указано название уровня доступа"}
|
||||||
exists = await CRUD.read(select(AccessLevel).where(AccessLevel.title == title))
|
exists = await CRUD.read(select(AccessLevel).where(AccessLevel.title == title))
|
||||||
if exists:
|
if exists:
|
||||||
logger.error("Уровень доступа с таким названием уже существует")
|
logger.error("Уровень доступа с таким названием уже существует")
|
||||||
return {}
|
return {"error": "Уровень доступа с таким названием уже существует"}
|
||||||
try:
|
try:
|
||||||
logger.info(f"Создание уровня доступа {title}")
|
logger.info(f"Создание уровня доступа {title}")
|
||||||
user_id = newData.pop("user_id", None)
|
|
||||||
accessData = await AccessLevel(**newData).save()
|
accessData = await AccessLevel(**newData).save()
|
||||||
await ServiceRecordsHandler.add(
|
await ServiceRecordsHandler.add(
|
||||||
user_id, {"Добавлен уровень доступа": accessData.toDict()}
|
userId, {"Добавлен уровень доступа": accessData.toDict()}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка создания уровня доступа: {str(e)}")
|
logger.error(f"Ошибка создания уровня доступа: {str(e)}")
|
||||||
@@ -36,23 +35,22 @@ class AccessLevelHandler:
|
|||||||
return {}
|
return {}
|
||||||
return accessData.toDict()
|
return accessData.toDict()
|
||||||
|
|
||||||
async def edit(accessId: int, **kwargs):
|
async def edit(levelData, userId: int = None):
|
||||||
query = select(AccessLevel).where(AccessLevel.id == accessId)
|
query = select(AccessLevel).where(AccessLevel.id == levelData.pop("id"))
|
||||||
accessData = await CRUD.read(query)
|
accessData = await CRUD.read(query)
|
||||||
if not accessData:
|
if not accessData:
|
||||||
logger.error("Уровень доступа не найден")
|
logger.error("Уровень доступа не найден")
|
||||||
return {}
|
return {"error": "Уровень доступа не найден"}
|
||||||
try:
|
try:
|
||||||
user_id = kwargs.pop("user_id", None)
|
editedAccessData = await accessData.edit(**levelData)
|
||||||
editedAccessData = await accessData.edit(**kwargs)
|
|
||||||
await ServiceRecordsHandler.add(
|
await ServiceRecordsHandler.add(
|
||||||
user_id, {"Обновлен уровень доступа": editedAccessData.toDict()}
|
userId, {"Обновлен уровень доступа": editedAccessData.toDict()}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка обновления уровня доступа: {str(e)}")
|
logger.error(f"Ошибка обновления уровня доступа: {str(e)}")
|
||||||
return {}
|
return {"error": "Ошибка обновления уровня доступа"}
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}"
|
f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {levelData.keys()}"
|
||||||
)
|
)
|
||||||
return editedAccessData.toDict()
|
return editedAccessData.toDict()
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -1,6 +1,6 @@
|
|||||||
from datetime import date, datetime, time, timedelta
|
from datetime import date, datetime, time, timedelta
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from db.handlers.stock import StockHandler
|
from db.handlers.stock import StockHandler
|
||||||
from db.schemas.records import StocksRecords, ServicesRecords
|
from db.schemas.records import StocksRecords, ServicesRecords
|
||||||
@@ -175,6 +175,21 @@ class StocksRecordsHandler:
|
|||||||
logger.exception("Ошибка получения записей")
|
logger.exception("Ошибка получения записей")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def getUserRecords(user_id: int) -> int:
|
||||||
|
from db import CRUD
|
||||||
|
|
||||||
|
try:
|
||||||
|
query = select(func.count(StocksRecords.id)).where(
|
||||||
|
StocksRecords.init_user_id == user_id,
|
||||||
|
)
|
||||||
|
logger.debug(f"Получение всех записей пользователя {user_id}")
|
||||||
|
records = await CRUD.read(query)
|
||||||
|
logger.debug(f"{records} записей пользователя {user_id} успешно получены")
|
||||||
|
return records
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения записей: {str(e)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
async def get(user_id: int, manager: bool):
|
async def get(user_id: int, manager: bool):
|
||||||
from db import CRUD
|
from db import CRUD
|
||||||
|
|
||||||
|
|||||||
+67
-38
@@ -4,13 +4,20 @@ from db.handlers.access import AccessLevelHandler
|
|||||||
from db.handlers.toolbox import ToolboxHandler
|
from db.handlers.toolbox import ToolboxHandler
|
||||||
from utils import logger, pwd_hash, saveImage, safeFilename, deleteImage, pwd_verify
|
from utils import logger, pwd_hash, saveImage, safeFilename, deleteImage, pwd_verify
|
||||||
from db.schemas.user import User
|
from db.schemas.user import User
|
||||||
from db.handlers.records import ServiceRecordsHandler
|
from db.handlers.records import ServiceRecordsHandler, StocksRecordsHandler
|
||||||
|
|
||||||
|
|
||||||
def handleUserPhoto(imageData, login: str):
|
def handleUserPhoto(imageData, login: str):
|
||||||
|
import base64
|
||||||
|
|
||||||
login = safeFilename(login)
|
login = safeFilename(login)
|
||||||
fileName = f"users/{login}.png"
|
fileName = f"static/images/users/{login}.png"
|
||||||
if not saveImage(imageData, fileName):
|
if imageData.startswith("data:image"):
|
||||||
|
header, encoded = imageData.split(",", 1)
|
||||||
|
else:
|
||||||
|
encoded = imageData
|
||||||
|
file_bytes = base64.b64decode(encoded)
|
||||||
|
if not saveImage(file_bytes, fileName):
|
||||||
return None
|
return None
|
||||||
return fileName
|
return fileName
|
||||||
|
|
||||||
@@ -39,9 +46,9 @@ class UserHandler:
|
|||||||
userData["hashed_password"] = pwd_hash(userData.pop("password"))
|
userData["hashed_password"] = pwd_hash(userData.pop("password"))
|
||||||
if "photo" in userData:
|
if "photo" in userData:
|
||||||
imageData = userData.pop("photo")
|
imageData = userData.pop("photo")
|
||||||
photoFile = handleUserPhoto(imageData, login)
|
imageFileName = handleUserPhoto(imageData, login)
|
||||||
if photoFile:
|
if imageFileName:
|
||||||
userData["photo"] = photoFile
|
userData["photo"] = imageFileName
|
||||||
try:
|
try:
|
||||||
newUser = await User(**userData).save()
|
newUser = await User(**userData).save()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -67,10 +74,9 @@ class UserHandler:
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}"
|
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}"
|
||||||
)
|
)
|
||||||
await ServiceRecordsHandler.add(
|
|
||||||
user_id, {"Добавлен пользователь": newUser.toDict()}
|
|
||||||
)
|
|
||||||
newUserData = newUser.toDict()
|
newUserData = newUser.toDict()
|
||||||
|
newUserData.pop("hashed_password")
|
||||||
|
await ServiceRecordsHandler.add(user_id, {"Добавлен пользователь": newUserData})
|
||||||
newUserData["access_level_data"] = userAccessLevel
|
newUserData["access_level_data"] = userAccessLevel
|
||||||
return newUserData
|
return newUserData
|
||||||
|
|
||||||
@@ -78,37 +84,53 @@ class UserHandler:
|
|||||||
id = userData.get("id", None)
|
id = userData.get("id", None)
|
||||||
if not id:
|
if not id:
|
||||||
logger.error("Не указан id пользователя")
|
logger.error("Не указан id пользователя")
|
||||||
return {}
|
return {"error": "Не указан id пользователя"}
|
||||||
query = select(User).where(User.id == id)
|
query = select(User).where(User.id == id)
|
||||||
user = await CRUD.read(query)
|
user = await CRUD.read(query)
|
||||||
if not user:
|
if not user:
|
||||||
logger.error("Пользователь с таким id не найден")
|
logger.error("Пользователь с таким id не найден")
|
||||||
return {}
|
return {"error": "Пользователь не найден"}
|
||||||
changedUserData = userData.get("changedUserData", {})
|
if len(userData.keys()) == 0:
|
||||||
if len(changedUserData.keys()) == 0:
|
|
||||||
logger.error("Не указаны изменяемые данные")
|
logger.error("Не указаны изменяемые данные")
|
||||||
return {}
|
return {"error": "Не указаны изменяемые данные"}
|
||||||
if "password" in changedUserData:
|
if "password" in userData:
|
||||||
userData["hashed_password"] = pwd_hash(changedUserData.pop("password"))
|
userData["hashed_password"] = pwd_hash(userData.pop("password"))
|
||||||
if "photo" in changedUserData:
|
if "photo" in userData:
|
||||||
imageData = changedUserData.pop("photo")
|
imageData = userData.pop("photo")
|
||||||
photoFile = handleUserPhoto(imageData, user.login)
|
if imageData != "":
|
||||||
|
login = user.login if "login" not in userData else userData["login"]
|
||||||
|
photoFile = handleUserPhoto(imageData, login)
|
||||||
if photoFile:
|
if photoFile:
|
||||||
changedUserData["photo"] = photoFile
|
userData["photo"] = photoFile
|
||||||
|
deleteImage(user.photo)
|
||||||
|
else:
|
||||||
|
logger.error("Ошибка обновления фото пользователя")
|
||||||
|
return {"error": "Ошибка обновления фото пользователя"}
|
||||||
|
else:
|
||||||
|
userData["photo"] = "static/images/users/default.png"
|
||||||
deleteImage(user.photo)
|
deleteImage(user.photo)
|
||||||
try:
|
try:
|
||||||
editedUser = await user.edit(**changedUserData)
|
userData.pop("id")
|
||||||
|
editedUser = await user.edit(**userData)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка обновления пользователя: {str(e)}")
|
logger.error(f"Ошибка обновления пользователя: {str(e)}")
|
||||||
return {}
|
return {"error": "Ошибка обновления пользователя"}
|
||||||
if not editedUser:
|
if not editedUser:
|
||||||
logger.error("Ошибка обновления пользователя")
|
logger.error("Ошибка обновления пользователя")
|
||||||
return {}
|
return {"error": "Ошибка обновления пользователя"}
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {changedUserData.keys()}"
|
f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {userData.keys()}"
|
||||||
)
|
)
|
||||||
if not user.available_own_toolbox:
|
if user.access_level_id != editedUser.access_level_id:
|
||||||
if editedUser.available_own_toolbox:
|
userAccessLevel = await AccessLevelHandler.get(user.access_level_id)
|
||||||
|
userAccessLevelNew = await AccessLevelHandler.get(
|
||||||
|
editedUser.access_level_id
|
||||||
|
)
|
||||||
|
if not userAccessLevel or not userAccessLevelNew:
|
||||||
|
logger.error("Уровень доступа не найден")
|
||||||
|
return {"error": "Уровень доступа не найден"}
|
||||||
|
if not userAccessLevel.get("available_own_toolbox"):
|
||||||
|
if userAccessLevelNew.get("available_own_toolbox"):
|
||||||
newToolboxData = {
|
newToolboxData = {
|
||||||
"title": editedUser.username,
|
"title": editedUser.username,
|
||||||
"description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность",
|
"description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность",
|
||||||
@@ -118,10 +140,10 @@ class UserHandler:
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
|
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
|
||||||
)
|
)
|
||||||
await ServiceRecordsHandler.add(
|
editUserData = editedUser.toDict()
|
||||||
user_id, {"Изменен пользователь": editedUser.toDict()}
|
editUserData.pop("hashed_password")
|
||||||
)
|
await ServiceRecordsHandler.add(user_id, {"Изменен пользователь": editUserData})
|
||||||
return editedUser.toDict()
|
return editUserData
|
||||||
|
|
||||||
async def getAll() -> list[dict]:
|
async def getAll() -> list[dict]:
|
||||||
query = select(User)
|
query = select(User)
|
||||||
@@ -136,23 +158,30 @@ class UserHandler:
|
|||||||
return {}
|
return {}
|
||||||
return user.toDict()
|
return user.toDict()
|
||||||
|
|
||||||
async def delete(id: int, user_id: int = None) -> bool:
|
async def delete(id: int, user_id: int = None) -> dict:
|
||||||
|
userRecordsCount = await StocksRecordsHandler.getUserRecords(id)
|
||||||
|
if userRecordsCount > 0:
|
||||||
|
logger.error(f"У пользователя {id} есть записи: {userRecordsCount}")
|
||||||
|
return {"error": "У пользователя есть записи"}
|
||||||
query = select(User).where(User.id == id)
|
query = select(User).where(User.id == id)
|
||||||
user = await CRUD.read(query)
|
user = await CRUD.read(query)
|
||||||
if not user:
|
if not user:
|
||||||
logger.error("Пользователь с таким id не найден")
|
logger.error("Пользователь с таким id не найден")
|
||||||
return False
|
return {"error": "Пользователь не найден"}
|
||||||
try:
|
try:
|
||||||
userName = user.username
|
userName = user.username
|
||||||
|
photoFile = user.photo
|
||||||
result = await CRUD.delete(user)
|
result = await CRUD.delete(user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка удаления пользователя: {str(e)}")
|
logger.error(f"Ошибка удаления пользователя: {str(e)}")
|
||||||
return False
|
return {"error": "Ошибка удаления пользователя"}
|
||||||
|
if result:
|
||||||
|
deleteImage(photoFile)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Пользователь {userName} {'успешно удален' if result else 'не удален'}"
|
f"Пользователь {userName} {'успешно удален' if result else 'не удален'}"
|
||||||
)
|
)
|
||||||
await ServiceRecordsHandler.add(user_id, {"Удален пользователь": userName})
|
await ServiceRecordsHandler.add(user_id, {"Удален пользователь": userName})
|
||||||
return result
|
return {"error": "Ошибка удаления пользователя"} if not result else {}
|
||||||
|
|
||||||
async def deletePhoto(id: int, user_id: int = None) -> bool:
|
async def deletePhoto(id: int, user_id: int = None) -> bool:
|
||||||
query = select(User).where(User.id == id)
|
query = select(User).where(User.id == id)
|
||||||
@@ -205,25 +234,25 @@ class UserHandler:
|
|||||||
baseUsers = {
|
baseUsers = {
|
||||||
"admin": {
|
"admin": {
|
||||||
"login": "admin",
|
"login": "admin",
|
||||||
"username": "Администратор - Demo",
|
"username": "Администратор",
|
||||||
"password": password,
|
"password": password,
|
||||||
"access_level_id": acessLevels["Администратор"],
|
"access_level_id": acessLevels["Администратор"],
|
||||||
},
|
},
|
||||||
"manager": {
|
"manager": {
|
||||||
"login": "manager",
|
"login": "manager",
|
||||||
"username": "Менеджер - Demo",
|
"username": "Менеджер",
|
||||||
"password": password,
|
"password": password,
|
||||||
"access_level_id": acessLevels["Менеджер"],
|
"access_level_id": acessLevels["Менеджер"],
|
||||||
},
|
},
|
||||||
"storekeeper": {
|
"storekeeper": {
|
||||||
"login": "storekeeper",
|
"login": "storekeeper",
|
||||||
"username": "Кладовщик - Demo",
|
"username": "Кладовщик",
|
||||||
"password": password,
|
"password": password,
|
||||||
"access_level_id": acessLevels["Кладовщик"],
|
"access_level_id": acessLevels["Кладовщик"],
|
||||||
},
|
},
|
||||||
"employee": {
|
"employee": {
|
||||||
"login": "employee",
|
"login": "employee",
|
||||||
"username": "Сотрудник - Demo",
|
"username": "Сотрудник",
|
||||||
"password": password,
|
"password": password,
|
||||||
"access_level_id": acessLevels["Сотрудник"],
|
"access_level_id": acessLevels["Сотрудник"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ async def main():
|
|||||||
from db.initialize import DatabaseInitializer
|
from db.initialize import DatabaseInitializer
|
||||||
|
|
||||||
try:
|
try:
|
||||||
force = False
|
force = True
|
||||||
reNewDB = False
|
reNewDB = True
|
||||||
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
|
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
|
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -34,6 +34,9 @@ def saveImage(file_bytes: bytes, file_name: str) -> bool:
|
|||||||
|
|
||||||
logger.debug(f"[ImageSave] Saving image to {target_path}")
|
logger.debug(f"[ImageSave] Saving image to {target_path}")
|
||||||
img.save(target_path, "PNG")
|
img.save(target_path, "PNG")
|
||||||
|
if not os.path.isfile(target_path):
|
||||||
|
logger.error(f"[ImageSave] File {target_path} not found")
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+44
-10
@@ -1,24 +1,54 @@
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
from .loggers import logger
|
||||||
|
|
||||||
# Простая транслитерация
|
# Простая транслитерация
|
||||||
TRANSLIT_MAP = {
|
TRANSLIT_MAP = {
|
||||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
|
"а": "a",
|
||||||
'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i',
|
"б": "b",
|
||||||
'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n',
|
"в": "v",
|
||||||
'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't',
|
"г": "g",
|
||||||
'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch',
|
"д": "d",
|
||||||
'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '',
|
"е": "e",
|
||||||
'э': 'e', 'ю': 'yu', 'я': 'ya'
|
"ё": "yo",
|
||||||
|
"ж": "zh",
|
||||||
|
"з": "z",
|
||||||
|
"и": "i",
|
||||||
|
"й": "y",
|
||||||
|
"к": "k",
|
||||||
|
"л": "l",
|
||||||
|
"м": "m",
|
||||||
|
"н": "n",
|
||||||
|
"о": "o",
|
||||||
|
"п": "p",
|
||||||
|
"р": "r",
|
||||||
|
"с": "s",
|
||||||
|
"т": "t",
|
||||||
|
"у": "u",
|
||||||
|
"ф": "f",
|
||||||
|
"х": "h",
|
||||||
|
"ц": "c",
|
||||||
|
"ч": "ch",
|
||||||
|
"ш": "sh",
|
||||||
|
"щ": "sch",
|
||||||
|
"ъ": "",
|
||||||
|
"ы": "y",
|
||||||
|
"ь": "",
|
||||||
|
"э": "e",
|
||||||
|
"ю": "yu",
|
||||||
|
"я": "ya",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Заглавные буквы → в ту же латиницу, но без capital (ниже мы в lower() всё равно переводим)
|
# Заглавные буквы → в ту же латиницу, но без capital (ниже мы в lower() всё равно переводим)
|
||||||
TRANSLIT_MAP.update({k.upper(): v for k, v in TRANSLIT_MAP.items()})
|
TRANSLIT_MAP.update({k.upper(): v for k, v in TRANSLIT_MAP.items()})
|
||||||
|
|
||||||
|
|
||||||
def transliterate(text: str) -> str:
|
def transliterate(text: str) -> str:
|
||||||
return ''.join(TRANSLIT_MAP.get(ch, ch) for ch in text)
|
return "".join(TRANSLIT_MAP.get(ch, ch) for ch in text)
|
||||||
|
|
||||||
|
|
||||||
def safeFilename(name: str) -> str:
|
def safeFilename(name: str) -> str:
|
||||||
|
try:
|
||||||
# 1. Транслитерация кириллицы
|
# 1. Транслитерация кириллицы
|
||||||
name = transliterate(name)
|
name = transliterate(name)
|
||||||
|
|
||||||
@@ -26,10 +56,10 @@ def safeFilename(name: str) -> str:
|
|||||||
name = name.lower()
|
name = name.lower()
|
||||||
|
|
||||||
# 3. Заменяем всё, что не буква/цифра, на "_"
|
# 3. Заменяем всё, что не буква/цифра, на "_"
|
||||||
name = re.sub(r'[^a-z0-9]+', '_', name)
|
name = re.sub(r"[^a-z0-9]+", "_", name)
|
||||||
|
|
||||||
# 4. Убираем повторяющиеся "_"
|
# 4. Убираем повторяющиеся "_"
|
||||||
name = re.sub(r'_+', '_', name).strip('_')
|
name = re.sub(r"_+", "_", name).strip("_")
|
||||||
|
|
||||||
# 5. Ограничиваем длину
|
# 5. Ограничиваем длину
|
||||||
name = name[:80] or "file"
|
name = name[:80] or "file"
|
||||||
@@ -37,3 +67,7 @@ def safeFilename(name: str) -> str:
|
|||||||
# 6. Добавляем таймштамп
|
# 6. Добавляем таймштамп
|
||||||
timestamp = int(time.time() * 1000) # миллисекунды
|
timestamp = int(time.time() * 1000) # миллисекунды
|
||||||
return f"{name}_{timestamp}"
|
return f"{name}_{timestamp}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка создания названия файла: {str(e)}")
|
||||||
|
return "file"
|
||||||
|
|||||||
Reference in New Issue
Block a user