`;
detailsHtml += `
`;
actionData.image.additional.forEach(img => {
- detailsHtml += `
`;
+ detailsHtml += `
`;
});
detailsHtml += `
`;
}
@@ -6227,6 +6198,839 @@ function renderJurnalServicesTab(tabId, tabData) {
renderServicesTable();
}
+function renderUsersTab(tabId, tabData) {
+ const tabContent = document.getElementById(`${tabId}-tab-content`);
+ const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
+
+ const { users, accessLevels } = tabData;
+
+ users.sort((a, b) => a.username.localeCompare(b.username));
+
+ // Создаем мапу уровней доступа для быстрого доступа
+ const accessLevelMap = {};
+ const accessLevelTitles = [];
+ accessLevels.forEach(level => {
+ accessLevelMap[level.id] = level.title;
+ accessLevelTitles.push(level.title);
+ });
+
+ const userUsernames = [];
+ const userLogins = [];
+ users.forEach(user => {
+ userUsernames.push(user.username);
+ userLogins.push(user.login);
+ });
+
+ // Рендерим опциональный блок с кнопками
+ tabOptionalContent.innerHTML = `
+
+
+
+ ${accessData.users_creation ? `
+
+ Добавить пользователя
+
+ ` : ''}
+ ${accessData.access_level_view ? `
+
+ Уровни доступа
+
+ ` : ''}
+
+
+
+ `;
+
+ // Функция для показа модального окна с подтверждением
+ function showConfirmationModal(title, message, confirmCallback) {
+ const modalId = `${tabId}-confirmation-modal`;
+
+ // Удаляем старую модалку, если есть
+ const existingModal = document.getElementById(modalId);
+ if (existingModal) {
+ existingModal.remove();
+ }
+
+ const modalHTML = `
+
+ `;
+
+ 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 = `
+
+
+ Нет пользователей в системе
+
+ `;
+ return;
+ }
+
+ tabContent.innerHTML = `
+
+ ${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 `
+
+
+
+
+
+
+
+
+
${user.username || 'Без имени'}
+ ${user.login || 'Без логина'}
+
+
+
+
+
+
+
+
+ ${levelTitle}
+
+
+
+ Создан: ${createdAt}
+
+
+
+ Обновлен: ${user.updated_at ? new Date(user.updated_at).toLocaleDateString('ru-RU') : 'Нет данных'}
+
+
+
+
+ ${accessData.users_edit ? `
+
+ Изменить
+
+ ` : ''}
+
+ ${accessData.users_disabling ? `
+
+
+ ${isActive ? 'Заблокировать' : 'Разблокировать'}
+
+ ` : ''}
+
+
+
+
+ `;
+ }).join('')}
+
+ `;
+
+ // Назначаем обработчики для кнопок в карточках
+ 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 = `
+
+ `;
+
+ 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 = `
+
+
+
+
+
+
+
+ ${editableLevels.map((level, index) => `
+
+ ${level.title}
+
+ `).join('')}
+ ${accessData.access_level_edit ? `
+
+ Добавить
+
+ ` : ''}
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ 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 = `
+
+
Название уровня
+ ${isNew || isEditable ? `
+
+ ` : `
+
${level.title}
+ `}
+
+
+
+
Описание
+ ${isNew || isEditable ? `
+
+ ` : `
+
${level.description || 'Нет описания'}
+ `}
+
+
+
+
+
Основные права:
+
+ ${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)}
+
+
+
+
+
Дополнительные права:
+
+ ${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)}
+
+
+
+ `;
+
+ // Вспомогательная функция для генерации чекбоксов
+ function generateCheckbox(name, label, checked) {
+ return `
+
+ `;
+ }
+
+ // Показываем/скрываем кнопки сохранения
+ document.getElementById(`${modalId}-save-btn`).style.display = (isEditable && !isNew) ? 'block' : 'none';
+ document.getElementById(`${modalId}-add-save-btn`).style.display = (isEditable && isNew) ? 'block' : 'none';
+ }
+
+ // Обработчики для вкладок уровней
+ document.querySelectorAll(`.level-tab-btn`).forEach((btn, index) => {
+ btn.addEventListener('click', () => {
+ // Обновляем активную вкладку
+ document.querySelectorAll(`.level-tab-btn`).forEach(b => {
+ b.classList.remove('btn-primary');
+ b.classList.add('btn-outline-primary');
+ });
+ btn.classList.remove('btn-outline-primary');
+ btn.classList.add('btn-primary');
+
+ // Загружаем содержимое уровня
+ const levelId = btn.dataset.levelId;
+ newLevelMode = false;
+ loadLevelContent(levelId, false);
+ });
+ });
+
+ // Обработчик для добавления нового уровня
+ if (accessData.access_level_edit) {
+ document.getElementById(`${modalId}-add-level-btn`).addEventListener('click', () => {
+ // Создаем новую вкладку
+ const tabsContainer = document.getElementById(`${modalId}-level-tabs`);
+ const newTabId = 'new-' + Date.now();
+
+ // Обновляем все вкладки
+ document.querySelectorAll(`.level-tab-btn`).forEach(b => {
+ b.classList.remove('btn-primary');
+ b.classList.add('btn-outline-primary');
+ });
+
+ // Добавляем новую вкладку
+ const newTab = document.createElement('button');
+ newTab.className = 'btn btn-primary level-tab-btn';
+ newTab.textContent = 'Новый уровень';
+ newTab.dataset.levelId = newTabId;
+ newTab.addEventListener('click', () => {
+ document.querySelectorAll(`.level-tab-btn`).forEach(b => {
+ b.classList.remove('btn-primary');
+ b.classList.add('btn-outline-primary');
+ });
+ newTab.classList.remove('btn-outline-primary');
+ newTab.classList.add('btn-primary');
+ loadLevelContent(newTabId, true);
+ });
+
+ // Убираем кнопку добавления
+ document.getElementById(`${modalId}-add-level-btn`).style.display = 'none';
+
+ tabsContainer.appendChild(newTab);
+ loadLevelContent(newTabId, true);
+ });
+ }
+
+ async function sendLevelData(isNew) {
+ const level = editableLevels.find(l => l.id == currentLevelId);
+ if (!level && !isNew) return;
+
+ // Собираем данные из формы
+ const levelData = {
+ title: document.getElementById(`${modalId}-level-title`).value,
+ description: document.getElementById(`${modalId}-level-description`).value
+ };
+
+ if (!levelData.title.trim() || (!isNew && levelData.title !== level.title && accessLevelTitles.includes(levelData.title)) || (isNew && accessLevelTitles.includes(levelData.title))) {
+ showInfo('Ошибка при сохранении', 'error');
+ showInfo('Введите уникальное название уровня доступа', 'warning');
+ return;
+ }
+
+ if (!levelData.description.trim()) {
+ showInfo('Ошибка при сохранении', 'error');
+ showInfo('Введите описание уровня доступа', 'warning');
+ return;
+ }
+
+ if (!isNew) {
+ levelData.id = level.id;
+ }
+
+ // Проверяем обязательные поля и уникальность
+ if (isNew) {
+ if (!levelData.title.trim() || editableLevels.includes(levelData.title)) {
+ showInfo('Ошибка при добавлении', 'error');
+ showInfo('Введите уникальное название уровня доступа', 'warning');
+ return;
+ }
+ } else {
+ if (levelData.title !== level.title && (!levelData.title.trim() || editableLevels.includes(levelData.title))) {
+ showInfo('Ошибка при сохранении', 'error');
+ showInfo('Введите уникальное название уровня доступа', 'warning');
+ return;
+ }
+ }
+
+ // Собираем все чекбоксы
+ const checkboxNames = [
+ 'tools_creation', 'tools_registration', 'tools_edit', 'tools_delete',
+ 'users_creation', 'users_edit', 'users_disabling', 'users_view',
+ 'available_own_toolbox', 'view_all_toolboxes', 'view_requests',
+ 'view_services', 'access_level_view', 'access_level_edit',
+ 'manage_toolboxes', 'debit_request_confirm', 'refund_request_confirm'
+ ];
+
+ checkboxNames.forEach(name => {
+ levelData[name] = document.getElementById(`${modalId}-${name}`).checked;
+ });
+
+ let changedLevelData = {};
+ if (isNew) {
+ changedLevelData = levelData;
+ } else {
+ changedLevelData = Object.keys(levelData).reduce((acc, key) => {
+ if (levelData[key] !== level[key]) {
+ acc[key] = levelData[key];
+ }
+ return acc;
+ }, {});
+ }
+
+ if (Object.keys(changedLevelData).length === 0) {
+ showInfo('Нет изменений', 'info');
+ return;
+ }
+ if (!isNew) {
+ changedLevelData.id = level.id;
+ }
+
+ const result = await apiRequest('/user/level', { action: isNew ? 'create' : 'update', changedLevelData, userId: currentUserId });
+
+ if (result && result.status === 'ok') {
+ modal.hide();
+ showInfo('Данные сохранены', 'success');
+ // Обновляем данные
+ await refreshThisTab();
+ } else {
+ const errorMsg = result?.message || 'Произошла ошибка при сохранении';
+ showInfo(errorMsg, 'error');
+ }
+ }
+
+ // Обработчик для сохранения изменений существующего уровня
+ if (accessData.access_level_edit) {
+ document.getElementById(`${modalId}-save-btn`).addEventListener('click', async () => {
+ await sendLevelData(newLevelMode);
+ });
+
+ // Обработчик для добавления нового уровня
+ document.getElementById(`${modalId}-add-save-btn`).addEventListener('click', async () => {
+ await sendLevelData(newLevelMode);
+ });
+ }
+
+ // Загружаем первый уровень по умолчанию
+ if (editableLevels.length > 0) {
+ loadLevelContent(editableLevels[0].id);
+ }
+
+ modal.show();
+
+ // Удаляем модалку после скрытия
+ modal._element.addEventListener('hidden.bs.modal', () => {
+ modal._element.remove();
+ });
+ }
+
+ // Обработчики для кнопок в опциональном блоке
+ if (accessData.users_creation) {
+ document.getElementById(`${tabId}-add-user-btn`).addEventListener('click', () => {
+ openEditUserModal();
+ });
+ }
+
+ if (accessData.access_level_view) {
+ document.getElementById(`${tabId}-access-levels-btn`).addEventListener('click', () => {
+ openAccessLevelsModal();
+ });
+ }
+
+ // Первоначальный рендеринг карточек
+ renderUsersCards();
+}
+
document.addEventListener('DOMContentLoaded', async () => {
await getCookieData();
diff --git a/db/handlers/__pycache__/access.cpython-313.pyc b/db/handlers/__pycache__/access.cpython-313.pyc
index 96388f2..003b3c0 100644
Binary files a/db/handlers/__pycache__/access.cpython-313.pyc and b/db/handlers/__pycache__/access.cpython-313.pyc differ
diff --git a/db/handlers/__pycache__/records.cpython-313.pyc b/db/handlers/__pycache__/records.cpython-313.pyc
index efdd21b..5fd7b10 100644
Binary files a/db/handlers/__pycache__/records.cpython-313.pyc and b/db/handlers/__pycache__/records.cpython-313.pyc differ
diff --git a/db/handlers/__pycache__/user.cpython-313.pyc b/db/handlers/__pycache__/user.cpython-313.pyc
index a59aea7..3287e26 100644
Binary files a/db/handlers/__pycache__/user.cpython-313.pyc and b/db/handlers/__pycache__/user.cpython-313.pyc differ
diff --git a/db/handlers/access.py b/db/handlers/access.py
index da16f38..b43d36b 100644
--- a/db/handlers/access.py
+++ b/db/handlers/access.py
@@ -6,21 +6,20 @@ from db.handlers.records import ServiceRecordsHandler
class AccessLevelHandler:
- async def add(newData):
+ async def add(newData, userId: int = None):
title = newData.get("title", None)
if not title:
logger.error("Не указано название уровня доступа")
- return {}
+ return {"error": "Не указано название уровня доступа"}
exists = await CRUD.read(select(AccessLevel).where(AccessLevel.title == title))
if exists:
logger.error("Уровень доступа с таким названием уже существует")
- return {}
+ return {"error": "Уровень доступа с таким названием уже существует"}
try:
logger.info(f"Создание уровня доступа {title}")
- user_id = newData.pop("user_id", None)
accessData = await AccessLevel(**newData).save()
await ServiceRecordsHandler.add(
- user_id, {"Добавлен уровень доступа": accessData.toDict()}
+ userId, {"Добавлен уровень доступа": accessData.toDict()}
)
except Exception as e:
logger.error(f"Ошибка создания уровня доступа: {str(e)}")
@@ -36,23 +35,22 @@ class AccessLevelHandler:
return {}
return accessData.toDict()
- async def edit(accessId: int, **kwargs):
- query = select(AccessLevel).where(AccessLevel.id == accessId)
+ async def edit(levelData, userId: int = None):
+ query = select(AccessLevel).where(AccessLevel.id == levelData.pop("id"))
accessData = await CRUD.read(query)
if not accessData:
logger.error("Уровень доступа не найден")
- return {}
+ return {"error": "Уровень доступа не найден"}
try:
- user_id = kwargs.pop("user_id", None)
- editedAccessData = await accessData.edit(**kwargs)
+ editedAccessData = await accessData.edit(**levelData)
await ServiceRecordsHandler.add(
- user_id, {"Обновлен уровень доступа": editedAccessData.toDict()}
+ userId, {"Обновлен уровень доступа": editedAccessData.toDict()}
)
except Exception as e:
logger.error(f"Ошибка обновления уровня доступа: {str(e)}")
- return {}
+ return {"error": "Ошибка обновления уровня доступа"}
logger.info(
- f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}"
+ f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {levelData.keys()}"
)
return editedAccessData.toDict()
diff --git a/db/handlers/records.py b/db/handlers/records.py
index 190836f..b366292 100644
--- a/db/handlers/records.py
+++ b/db/handlers/records.py
@@ -1,6 +1,6 @@
from datetime import date, datetime, time, timedelta
-from sqlalchemy import select
+from sqlalchemy import func, select
from db.handlers.stock import StockHandler
from db.schemas.records import StocksRecords, ServicesRecords
@@ -175,6 +175,21 @@ class StocksRecordsHandler:
logger.exception("Ошибка получения записей")
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):
from db import CRUD
diff --git a/db/handlers/user.py b/db/handlers/user.py
index f4df3c2..88b2855 100644
--- a/db/handlers/user.py
+++ b/db/handlers/user.py
@@ -4,13 +4,20 @@ from db.handlers.access import AccessLevelHandler
from db.handlers.toolbox import ToolboxHandler
from utils import logger, pwd_hash, saveImage, safeFilename, deleteImage, pwd_verify
from db.schemas.user import User
-from db.handlers.records import ServiceRecordsHandler
+from db.handlers.records import ServiceRecordsHandler, StocksRecordsHandler
def handleUserPhoto(imageData, login: str):
+ import base64
+
login = safeFilename(login)
- fileName = f"users/{login}.png"
- if not saveImage(imageData, fileName):
+ fileName = f"static/images/users/{login}.png"
+ 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 fileName
@@ -39,9 +46,9 @@ class UserHandler:
userData["hashed_password"] = pwd_hash(userData.pop("password"))
if "photo" in userData:
imageData = userData.pop("photo")
- photoFile = handleUserPhoto(imageData, login)
- if photoFile:
- userData["photo"] = photoFile
+ imageFileName = handleUserPhoto(imageData, login)
+ if imageFileName:
+ userData["photo"] = imageFileName
try:
newUser = await User(**userData).save()
except Exception as e:
@@ -67,10 +74,9 @@ class UserHandler:
logger.info(
f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}"
)
- await ServiceRecordsHandler.add(
- user_id, {"Добавлен пользователь": newUser.toDict()}
- )
newUserData = newUser.toDict()
+ newUserData.pop("hashed_password")
+ await ServiceRecordsHandler.add(user_id, {"Добавлен пользователь": newUserData})
newUserData["access_level_data"] = userAccessLevel
return newUserData
@@ -78,50 +84,66 @@ class UserHandler:
id = userData.get("id", None)
if not id:
logger.error("Не указан id пользователя")
- return {}
+ return {"error": "Не указан id пользователя"}
query = select(User).where(User.id == id)
user = await CRUD.read(query)
if not user:
logger.error("Пользователь с таким id не найден")
- return {}
- changedUserData = userData.get("changedUserData", {})
- if len(changedUserData.keys()) == 0:
+ return {"error": "Пользователь не найден"}
+ if len(userData.keys()) == 0:
logger.error("Не указаны изменяемые данные")
- return {}
- if "password" in changedUserData:
- userData["hashed_password"] = pwd_hash(changedUserData.pop("password"))
- if "photo" in changedUserData:
- imageData = changedUserData.pop("photo")
- photoFile = handleUserPhoto(imageData, user.login)
- if photoFile:
- changedUserData["photo"] = photoFile
+ return {"error": "Не указаны изменяемые данные"}
+ if "password" in userData:
+ userData["hashed_password"] = pwd_hash(userData.pop("password"))
+ if "photo" in userData:
+ imageData = userData.pop("photo")
+ if imageData != "":
+ login = user.login if "login" not in userData else userData["login"]
+ 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)
try:
- editedUser = await user.edit(**changedUserData)
+ userData.pop("id")
+ editedUser = await user.edit(**userData)
except Exception as e:
logger.error(f"Ошибка обновления пользователя: {str(e)}")
- return {}
+ return {"error": "Ошибка обновления пользователя"}
if not editedUser:
logger.error("Ошибка обновления пользователя")
- return {}
+ return {"error": "Ошибка обновления пользователя"}
logger.info(
- f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {changedUserData.keys()}"
+ f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {userData.keys()}"
)
- if not user.available_own_toolbox:
- if editedUser.available_own_toolbox:
- newToolboxData = {
- "title": editedUser.username,
- "description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность",
- "owner_id": editedUser.id,
- }
- newToolbox = await ToolboxHandler.addNewToolbox(newToolboxData)
- logger.info(
- f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}"
- )
- await ServiceRecordsHandler.add(
- user_id, {"Изменен пользователь": editedUser.toDict()}
- )
- return editedUser.toDict()
+ if user.access_level_id != editedUser.access_level_id:
+ 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 = {
+ "title": editedUser.username,
+ "description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность",
+ "owner_id": editedUser.id,
+ }
+ 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]:
query = select(User)
@@ -136,23 +158,30 @@ class UserHandler:
return {}
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)
user = await CRUD.read(query)
if not user:
logger.error("Пользователь с таким id не найден")
- return False
+ return {"error": "Пользователь не найден"}
try:
userName = user.username
+ photoFile = user.photo
result = await CRUD.delete(user)
except Exception as e:
logger.error(f"Ошибка удаления пользователя: {str(e)}")
- return False
+ return {"error": "Ошибка удаления пользователя"}
+ if result:
+ deleteImage(photoFile)
logger.info(
f"Пользователь {userName} {'успешно удален' if result else 'не удален'}"
)
await ServiceRecordsHandler.add(user_id, {"Удален пользователь": userName})
- return result
+ return {"error": "Ошибка удаления пользователя"} if not result else {}
async def deletePhoto(id: int, user_id: int = None) -> bool:
query = select(User).where(User.id == id)
@@ -205,25 +234,25 @@ class UserHandler:
baseUsers = {
"admin": {
"login": "admin",
- "username": "Администратор - Demo",
+ "username": "Администратор",
"password": password,
"access_level_id": acessLevels["Администратор"],
},
"manager": {
"login": "manager",
- "username": "Менеджер - Demo",
+ "username": "Менеджер",
"password": password,
"access_level_id": acessLevels["Менеджер"],
},
"storekeeper": {
"login": "storekeeper",
- "username": "Кладовщик - Demo",
+ "username": "Кладовщик",
"password": password,
"access_level_id": acessLevels["Кладовщик"],
},
"employee": {
"login": "employee",
- "username": "Сотрудник - Demo",
+ "username": "Сотрудник",
"password": password,
"access_level_id": acessLevels["Сотрудник"],
},
diff --git a/main.py b/main.py
index 598c13e..5e2761f 100644
--- a/main.py
+++ b/main.py
@@ -23,8 +23,8 @@ async def main():
from db.initialize import DatabaseInitializer
try:
- force = False
- reNewDB = False
+ force = True
+ reNewDB = True
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
diff --git a/utils/__pycache__/image.cpython-313.pyc b/utils/__pycache__/image.cpython-313.pyc
index 12deb4c..8e6c328 100644
Binary files a/utils/__pycache__/image.cpython-313.pyc and b/utils/__pycache__/image.cpython-313.pyc differ
diff --git a/utils/__pycache__/safe_filename.cpython-313.pyc b/utils/__pycache__/safe_filename.cpython-313.pyc
index 542d9ec..440399c 100644
Binary files a/utils/__pycache__/safe_filename.cpython-313.pyc and b/utils/__pycache__/safe_filename.cpython-313.pyc differ
diff --git a/utils/image.py b/utils/image.py
index e7f2a55..6253956 100644
--- a/utils/image.py
+++ b/utils/image.py
@@ -34,6 +34,9 @@ def saveImage(file_bytes: bytes, file_name: str) -> bool:
logger.debug(f"[ImageSave] Saving image to {target_path}")
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
except Exception as e:
diff --git a/utils/safe_filename.py b/utils/safe_filename.py
index 5cb4c11..78783e1 100644
--- a/utils/safe_filename.py
+++ b/utils/safe_filename.py
@@ -1,39 +1,73 @@
import re
import time
+from .loggers import logger
# Простая транслитерация
TRANSLIT_MAP = {
- 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
- 'е': 'e', 'ё': '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'
+ "а": "a",
+ "б": "b",
+ "в": "v",
+ "г": "g",
+ "д": "d",
+ "е": "e",
+ "ё": "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() всё равно переводим)
TRANSLIT_MAP.update({k.upper(): v for k, v in TRANSLIT_MAP.items()})
+
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:
- # 1. Транслитерация кириллицы
- name = transliterate(name)
+ try:
+ # 1. Транслитерация кириллицы
+ name = transliterate(name)
- # 2. Приводим к нижнему регистру
- name = name.lower()
+ # 2. Приводим к нижнему регистру
+ name = name.lower()
- # 3. Заменяем всё, что не буква/цифра, на "_"
- name = re.sub(r'[^a-z0-9]+', '_', name)
+ # 3. Заменяем всё, что не буква/цифра, на "_"
+ name = re.sub(r"[^a-z0-9]+", "_", name)
- # 4. Убираем повторяющиеся "_"
- name = re.sub(r'_+', '_', name).strip('_')
+ # 4. Убираем повторяющиеся "_"
+ name = re.sub(r"_+", "_", name).strip("_")
- # 5. Ограничиваем длину
- name = name[:80] or "file"
+ # 5. Ограничиваем длину
+ name = name[:80] or "file"
- # 6. Добавляем таймштамп
- timestamp = int(time.time() * 1000) # миллисекунды
- return f"{name}_{timestamp}"
\ No newline at end of file
+ # 6. Добавляем таймштамп
+ timestamp = int(time.time() * 1000) # миллисекунды
+ return f"{name}_{timestamp}"
+
+ except Exception as e:
+ logger.error(f"Ошибка создания названия файла: {str(e)}")
+ return "file"