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 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+77 -48
View File
@@ -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,50 +84,66 @@ 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 != "":
if photoFile: login = user.login if "login" not in userData else userData["login"]
changedUserData["photo"] = photoFile photoFile = handleUserPhoto(imageData, login)
if 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)
newToolboxData = { userAccessLevelNew = await AccessLevelHandler.get(
"title": editedUser.username, editedUser.access_level_id
"description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность", )
"owner_id": editedUser.id, if not userAccessLevel or not userAccessLevelNew:
} logger.error("Уровень доступа не найден")
newToolbox = await ToolboxHandler.addNewToolbox(newToolboxData) return {"error": "Уровень доступа не найден"}
logger.info( if not userAccessLevel.get("available_own_toolbox"):
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}" if userAccessLevelNew.get("available_own_toolbox"):
) newToolboxData = {
await ServiceRecordsHandler.add( "title": editedUser.username,
user_id, {"Изменен пользователь": editedUser.toDict()} "description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность",
) "owner_id": editedUser.id,
return editedUser.toDict() }
newToolbox = await ToolboxHandler.addNewToolbox(newToolboxData)
logger.info(
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
)
editUserData = editedUser.toDict()
editUserData.pop("hashed_password")
await ServiceRecordsHandler.add(user_id, {"Изменен пользователь": editUserData})
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["Сотрудник"],
}, },
+2 -2
View File
@@ -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.
+3
View File
@@ -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:
+55 -21
View File
@@ -1,39 +1,73 @@
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:
# 1. Транслитерация кириллицы try:
name = transliterate(name) # 1. Транслитерация кириллицы
name = transliterate(name)
# 2. Приводим к нижнему регистру # 2. Приводим к нижнему регистру
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"
# 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"