Управление категориями

This commit is contained in:
2025-12-11 23:07:22 +03:00
parent 56584cc8ff
commit 8b38d69980
11 changed files with 902 additions and 38 deletions
+1
View File
@@ -62,6 +62,7 @@ async def post_requests(
"toolkits": toolkits,
"categories": categories,
}
# logger.info(resultData)
case "jurnal_toolkits":
jurnal_toolkits = await StocksRecordsHandler.get()
if jurnal_toolkits:
Binary file not shown.
Binary file not shown.
+31 -4
View File
@@ -11,17 +11,17 @@ router = APIRouter()
@router.post("/", summary="Запрос остатка инструмента")
async def toolkit_request(
request_data: dict = Depends(requestDict),
reqData: dict = Depends(requestDict),
):
response = {"status": "error", "data": {}}
toolkitId = request_data.get("body").get("toolkitId")
toolkitId = reqData.get("body").get("toolkitId")
logger.info(f"Получение запроса остатка инструмента #{toolkitId}")
# logger.info(request_data)
stocks = await StockHandler.getByToolkitId(toolkitId)
if not stocks:
return response
userId = request_data.get("body").get("userId")
allToolboxes = request_data.get("body").get("allToolboxes")
userId = reqData.get("body").get("userId")
allToolboxes = reqData.get("body").get("allToolboxes")
toolboxes = (
await ToolboxHandler.getByOwner(userId)
if not allToolboxes
@@ -73,3 +73,30 @@ async def fill_toolbox():
"placements": [placement.toDict() for placement in placements],
}
return response
@router.post("/categories_batch", summary="Управление категориями")
async def categories_batch(reqData: dict = Depends(requestDict)):
logger.info(f"Управление категориями")
response = {"status": "error"}
userId = reqData.get("body").get("userId")
changesData = reqData.get("body").get("changes")
success = True
for newCategoryData in changesData.get("create", []):
logger.info(f"Добавление категории: {newCategoryData.get('title')}")
result = await CategoryHandler.add(newCategoryData, userId)
if not result:
success = False
for updateCategoryData in changesData.get("update", []):
logger.info(f"Обновление категории: {updateCategoryData.get('title')}")
result = await CategoryHandler.edit(updateCategoryData, userId)
if not result:
success = False
for deleteCategoryId in changesData.get("delete", []):
logger.info(f"Удаление категории: {deleteCategoryId}")
result = await CategoryHandler.delete(deleteCategoryId, userId)
if not result:
success = False
if success:
response["status"] = "ok"
return response
+848 -28
View File
@@ -240,37 +240,857 @@ function renderSimpleTab(tabId, tabData, title) {
`;
}
function renderToolkitsTab(tabId, toolsArray, categoriesList) {
async function manageCategory(categoriesList) {
// Удаляем старое модальное окно, если оно существует
let modal = document.getElementById('manageCategoryModal');
if (modal) modal.remove();
// Храним изменения
const changes = {
create: [], // новые категории
update: [], // измененные категории
delete: [] // id категорий для удаления
};
// Делаем копию списка категорий для работы с дополнительными полями
const categories = categoriesList.map(cat => ({
...cat,
status: 'unchanged', // unchanged, new, edited, deleted
originalData: null
}));
// Создаём модальное окно
modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'manageCategoryModal';
modal.tabIndex = -1;
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-folder me-2"></i>Управление категориями</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div id="manageCategoryError" class="alert alert-danger d-none m-3" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<span id="manageCategoryErrorMessage"></span>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="categoriesTable">
<thead class="table-light sticky-top">
<tr>
<th style="width: 35%;">Название</th>
<th style="width: 45%;">Описание</th>
<th style="width: 20%;" class="text-center">Действия</th>
</tr>
</thead>
<tbody id="categoriesListBody">
<!-- Список категорий будет здесь -->
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="3" class="text-center">
<button type="button" class="btn btn-sm btn-outline-primary" id="addCategoryBtn">
<i class="bi bi-plus-circle me-1"></i>Добавить категорию
</button>
</td>
</tr>
<tr id="newCategoryFormRow" class="d-none">
<td colspan="3">
<div class="p-3 border border-primary rounded">
<h6 class="mb-3 text-primary"><i class="bi bi-plus-lg me-1"></i>Новая категория</h6>
<div class="row g-2">
<div class="col-md-12">
<label for="newCategoryTitle" class="form-label required">Название категории</label>
<input type="text" class="form-control form-control-sm" id="newCategoryTitle"
placeholder="Введите название категории" required minlength="2" maxlength="100">
</div>
<div class="col-md-12">
<label for="newCategoryDescription" class="form-label required">Описание категории</label>
<textarea class="form-control form-control-sm" id="newCategoryDescription"
rows="2" placeholder="Описание категории" required maxlength="500"></textarea>
</div>
<div class="col-md-12">
<div class="d-flex justify-content-end gap-2 mt-2">
<button type="button" class="btn btn-sm btn-secondary" id="cancelNewCategoryBtn">
Отмена
</button>
<button type="button" class="btn btn-sm btn-success" id="saveNewCategoryBtn">
<i class="bi bi-check-lg me-1"></i>Сохранить
</button>
</div>
</div>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<!-- Панель изменений -->
<div id="changesPanel" class="border-top p-3">
<h6 class="mb-3"><i class="bi bi-list-check me-2"></i>Планируемые изменения</h6>
<div id="addChangesList" class="mb-3">
<h6 class="text-success mb-2"><i class="bi bi-plus-circle me-1"></i>Добавляем:</h6>
<div id="addChangesItems" class="ps-3">
<!-- Новые категории будут здесь -->
</div>
</div>
<div id="editChangesList" class="mb-3">
<h6 class="text-warning mb-2"><i class="bi bi-pencil-square me-1"></i>Меняем:</h6>
<div id="editChangesItems" class="ps-3">
<!-- Измененные категории будут здесь -->
</div>
</div>
<div id="deleteChangesList" class="mb-3">
<h6 class="text-danger mb-2"><i class="bi bi-trash me-1"></i>Удаляем:</h6>
<div id="deleteChangesItems" class="ps-3">
<!-- Категории для удаления будут здесь -->
</div>
</div>
<div id="noChangesMessage" class="text-muted text-center py-2">
<i class="bi bi-info-circle me-1"></i>Нет запланированных изменений
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
<button type="button" class="btn btn-primary" id="saveAllChangesBtn">
<span class="spinner-border spinner-border-sm me-1" id="saveChangesSpinner" style="display: none;"></span>
<span id="saveChangesText">Сохранить все изменения</span>
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
const categoriesListBody = modal.querySelector('#categoriesListBody');
const newCategoryFormRow = modal.querySelector('#newCategoryFormRow');
const addCategoryBtn = modal.querySelector('#addCategoryBtn');
const cancelNewCategoryBtn = modal.querySelector('#cancelNewCategoryBtn');
const saveNewCategoryBtn = modal.querySelector('#saveNewCategoryBtn');
const saveAllChangesBtn = modal.querySelector('#saveAllChangesBtn');
const saveChangesSpinner = modal.querySelector('#saveChangesSpinner');
const saveChangesText = modal.querySelector('#saveChangesText');
const addChangesItems = modal.querySelector('#addChangesItems');
const editChangesItems = modal.querySelector('#editChangesItems');
const deleteChangesItems = modal.querySelector('#deleteChangesItems');
const noChangesMessage = modal.querySelector('#noChangesMessage');
const addChangesList = modal.querySelector('#addChangesList');
const editChangesList = modal.querySelector('#editChangesList');
const deleteChangesList = modal.querySelector('#deleteChangesList');
// Функция для обновления панели изменений
function updateChangesPanel() {
// Очищаем панели
addChangesItems.innerHTML = '';
editChangesItems.innerHTML = '';
deleteChangesItems.innerHTML = '';
// Собираем изменения
const newCategories = categories.filter(cat => cat.status === 'new');
const editedCategories = categories.filter(cat => cat.status === 'edited');
const deletedCategories = categories.filter(cat => cat.status === 'deleted');
// Обновляем отображение панелей
if (newCategories.length > 0) {
addChangesList.classList.remove('d-none');
newCategories.forEach((category, index) => {
const item = document.createElement('div');
item.className = 'd-flex justify-content-between align-items-center mb-1';
item.innerHTML = `
<span>${escapeHtml(category.title)}</span>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="cancelNewCategoryAction(${categories.indexOf(category)})">
<i class="bi bi-x"></i> Отменить
</button>
`;
addChangesItems.appendChild(item);
});
} else {
addChangesList.classList.add('d-none');
}
if (editedCategories.length > 0) {
editChangesList.classList.remove('d-none');
editedCategories.forEach((category, index) => {
const item = document.createElement('div');
item.className = 'd-flex justify-content-between align-items-center mb-1';
item.innerHTML = `
<span>${escapeHtml(category.title)}</span>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="cancelEditCategoryAction(${categories.indexOf(category)})">
<i class="bi bi-x"></i> Отменить
</button>
`;
editChangesItems.appendChild(item);
});
} else {
editChangesList.classList.add('d-none');
}
if (deletedCategories.length > 0) {
deleteChangesList.classList.remove('d-none');
deletedCategories.forEach((category, index) => {
const item = document.createElement('div');
item.className = 'd-flex justify-content-between align-items-center mb-1';
item.innerHTML = `
<span>${escapeHtml(category.title)}</span>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="cancelDeleteCategoryAction(${categories.indexOf(category)})">
<i class="bi bi-x"></i> Отменить
</button>
`;
deleteChangesItems.appendChild(item);
});
} else {
deleteChangesList.classList.add('d-none');
}
// Показываем/скрываем сообщение "нет изменений"
const hasChanges = newCategories.length > 0 || editedCategories.length > 0 || deletedCategories.length > 0;
if (hasChanges) {
noChangesMessage.classList.add('d-none');
} else {
noChangesMessage.classList.remove('d-none');
}
}
// Функция для рендеринга списка категорий
function renderCategoriesList() {
categoriesListBody.innerHTML = '';
categories.forEach((category, index) => {
const status = category.status;
// Определяем стили в зависимости от статуса
let rowClass = '';
let badge = '';
switch (status) {
case 'new':
rowClass = 'table-success';
badge = '<span class="badge bg-success">Новая</span>';
break;
case 'edited':
rowClass = 'table-warning';
badge = '<span class="badge bg-warning">Изменена</span>';
break;
case 'deleted':
rowClass = 'table-danger';
badge = '<span class="badge bg-danger">Удалена</span>';
break;
default:
rowClass = '';
badge = '';
}
// Для удаленных категорий показываем только с кнопкой восстановления
if (status === 'new') {
// Для новых категорий показываем только кнопку отмены
categoriesListBody.innerHTML += `
<tr id="category-row-${index}" class="${rowClass}">
<td>
<div class="fw-semibold">${escapeHtml(category.title)}</div>
${badge}
</td>
<td>
<div class="text-muted small">${escapeHtml(category.description)}</div>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="cancelNewCategoryAction(${index})" title="Отменить добавление">
<i class="bi bi-x-circle"></i> Отменить
</button>
</td>
</tr>
`;
} else if (status === 'deleted') {
// Для удаленных категорий показываем кнопку восстановления
categoriesListBody.innerHTML += `
<tr id="category-row-${index}" class="${rowClass}">
<td>
<div class="fw-semibold">${escapeHtml(category.title)}</div>
${badge}
</td>
<td>
<div class="text-muted small">${escapeHtml(category.description)}</div>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-success" onclick="restoreCategory(${index})" title="Восстановить">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</td>
</tr>
`;
} else {
// Для остальных категорий (unchanged, edited) показываем обычные кнопки
categoriesListBody.innerHTML += `
<tr id="category-row-${index}" class="${rowClass}">
<td>
<div class="fw-semibold">${escapeHtml(category.title)}</div>
${badge}
</td>
<td>
<div class="text-muted small">${escapeHtml(category.description)}</div>
<div class="text-muted mt-1">
<small>
<i class="bi bi-clock me-1"></i>
${category.created_at ? new Date(category.created_at).toLocaleDateString('ru-RU') : 'Новая'}
</small>
<small class="ms-3">
<i class="bi bi-pencil me-1"></i>
${category.updated_at ? new Date(category.updated_at).toLocaleDateString('ru-RU') : 'Новая'}
</small>
</div>
</td>
<td class="text-center">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="editCategory(${index})" title="Редактировать">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="deleteCategory(${index})" title="Удалить">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
`;
}
});
}
// Функция для экранирования HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Функции для работы с категориями
modal.editCategory = function (index) {
// Сохраняем оригинальные данные если еще не сохранены
if (!categories[index].originalData) {
categories[index].originalData = {
title: categories[index].title,
description: categories[index].description
};
}
// Создаем модальное окно для редактирования
const editModal = document.createElement('div');
editModal.className = 'modal fade';
editModal.id = 'editCategoryModal';
editModal.innerHTML = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-pencil-square me-2"></i>Редактирование категории</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="editCategoryTitle" class="form-label required">Название категории</label>
<input type="text" class="form-control" id="editCategoryTitle"
value="${escapeHtml(categories[index].title)}" required minlength="2" maxlength="100">
</div>
<div class="mb-3">
<label for="editCategoryDescription" class="form-label required">Описание категории</label>
<textarea class="form-control" id="editCategoryDescription"
rows="3" required maxlength="500">${escapeHtml(categories[index].description)}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-success" id="saveEditCategoryBtn">
Сохранить изменения
</button>
</div>
</div>
</div>
`;
document.body.appendChild(editModal);
const bsEditModal = new bootstrap.Modal(editModal);
// Обработчик сохранения изменений
editModal.querySelector('#saveEditCategoryBtn').addEventListener('click', () => {
const titleInput = editModal.querySelector('#editCategoryTitle');
const descriptionInput = editModal.querySelector('#editCategoryDescription');
const title = titleInput.value.trim();
const description = descriptionInput.value.trim();
// Валидация
if (!title || title.length < 2) {
showInfo('Название категории должно содержать минимум 2 символа', 'error');
titleInput.focus();
return;
}
if (!description || description.length < 2) {
showInfo('Описание категории должно содержать минимум 2 символа', 'error');
descriptionInput.focus();
return;
}
// Проверяем уникальность названия среди других категорий
const duplicate = categories.find((cat, i) =>
i !== index &&
cat.id !== categories[index].id &&
cat.title.toLowerCase() === title.toLowerCase() &&
cat.status !== 'deleted'
);
if (duplicate) {
showInfo('Категория с таким названием уже существует', 'error');
titleInput.focus();
return;
}
// Обновляем данные категории
categories[index].title = title;
categories[index].description = description;
categories[index].status = 'edited';
// Добавляем в изменения, если это существующая категория
if (categories[index].id) {
const updateIndex = changes.update.findIndex(item => item.id === categories[index].id);
if (updateIndex === -1) {
changes.update.push({
id: categories[index].id,
title: title,
description: description
});
} else {
changes.update[updateIndex] = {
id: categories[index].id,
title: title,
description: description
};
}
}
bsEditModal.hide();
setTimeout(() => {
editModal.remove();
renderCategoriesList();
updateChangesPanel();
showInfo('Изменения сохранены', 'success');
}, 300);
});
// Очистка при закрытии модалки
editModal.addEventListener('hidden.bs.modal', () => {
setTimeout(() => {
if (editModal.parentNode) editModal.remove();
}, 300);
});
bsEditModal.show();
setTimeout(() => {
editModal.querySelector('#editCategoryTitle').focus();
}, 100);
};
modal.cancelEditCategoryAction = function (index) {
if (categories[index].originalData) {
// Восстанавливаем исходные данные
categories[index].title = categories[index].originalData.title;
categories[index].description = categories[index].originalData.description;
categories[index].status = 'unchanged';
categories[index].originalData = null;
// Удаляем из изменений
if (categories[index].id) {
const updateIndex = changes.update.findIndex(item => item.id === categories[index].id);
if (updateIndex !== -1) {
changes.update.splice(updateIndex, 1);
}
}
renderCategoriesList();
updateChangesPanel();
showInfo('Изменения отменены', 'info');
}
};
modal.deleteCategory = function (index) {
// Сохраняем оригинальные данные если еще не сохранены
if (!categories[index].originalData) {
categories[index].originalData = {
title: categories[index].title,
description: categories[index].description
};
}
// Создаем модальное окно подтверждения удаления
const deleteModal = document.createElement('div');
deleteModal.className = 'modal fade';
deleteModal.id = 'deleteCategoryModal';
deleteModal.innerHTML = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-danger"><i class="bi bi-trash me-2"></i>Удаление категории</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning mb-3">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Внимание!</strong> Категория может быть удалена только если в ней нет инструментов.
При попытке удаления категории, используемой в инструментах, вы получите увдомление об успехе,
при этом категория не будет удалена!
</div>
<div class="mb-2">
<strong>Название:</strong> ${escapeHtml(categories[index].title)}
</div>
<div class="mb-3">
<strong>Описание:</strong> ${escapeHtml(categories[index].description)}
</div>
</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="confirmDeleteCategoryBtn">
Удалить
</button>
</div>
</div>
</div>
`;
document.body.appendChild(deleteModal);
const bsDeleteModal = new bootstrap.Modal(deleteModal);
// Обработчик подтверждения удаления
deleteModal.querySelector('#confirmDeleteCategoryBtn').addEventListener('click', () => {
categories[index].status = 'deleted';
// Добавляем в изменения, если это существующая категория
if (categories[index].id) {
const deleteIndex = changes.delete.indexOf(categories[index].id);
if (deleteIndex === -1) {
changes.delete.push(categories[index].id);
}
} else {
// Если это новая категория - удаляем из изменений на создание
const createIndex = changes.create.findIndex(item =>
item.title === categories[index].originalData.title &&
item.description === categories[index].originalData.description
);
if (createIndex !== -1) {
changes.create.splice(createIndex, 1);
}
}
bsDeleteModal.hide();
setTimeout(() => {
deleteModal.remove();
renderCategoriesList();
updateChangesPanel();
showInfo('Категория помечена для удаления', 'warning');
}, 300);
});
// Очистка при закрытии модалки
deleteModal.addEventListener('hidden.bs.modal', () => {
setTimeout(() => {
if (deleteModal.parentNode) deleteModal.remove();
}, 300);
});
bsDeleteModal.show();
};
modal.cancelDeleteCategoryAction = function (index) {
// Восстанавливаем исходные данные
categories[index].title = categories[index].originalData.title;
categories[index].description = categories[index].originalData.description;
categories[index].status = 'unchanged';
categories[index].originalData = null;
// Удаляем из изменений на удаление
if (categories[index].id) {
const deleteIndex = changes.delete.indexOf(categories[index].id);
if (deleteIndex !== -1) {
changes.delete.splice(deleteIndex, 1);
}
}
renderCategoriesList();
updateChangesPanel();
showInfo('Удаление отменено', 'info');
};
modal.restoreCategory = function (index) {
// Восстанавливаем категорию
categories[index].status = 'unchanged';
categories[index].originalData = null;
// Удаляем из изменений на удаление
if (categories[index].id) {
const deleteIndex = changes.delete.indexOf(categories[index].id);
if (deleteIndex !== -1) {
changes.delete.splice(deleteIndex, 1);
}
}
renderCategoriesList();
updateChangesPanel();
showInfo('Категория восстановлена', 'success');
};
modal.cancelNewCategoryAction = function (index) {
// Удаляем новую категорию
if (categories[index].status === 'new') {
// Удаляем из изменений на создание
const createIndex = changes.create.findIndex(item =>
item.title === categories[index].title &&
item.description === categories[index].description
);
if (createIndex !== -1) {
changes.create.splice(createIndex, 1);
}
// Удаляем из массива категорий
categories.splice(index, 1);
renderCategoriesList();
updateChangesPanel();
showInfo('Новая категория удалена', 'info');
}
};
// Инициализируем список категорий
renderCategoriesList();
updateChangesPanel();
// Обработчики событий
addCategoryBtn.addEventListener('click', () => {
newCategoryFormRow.classList.remove('d-none');
addCategoryBtn.disabled = true;
setTimeout(() => {
const titleInput = modal.querySelector('#newCategoryTitle');
if (titleInput) titleInput.focus();
}, 10);
});
cancelNewCategoryBtn.addEventListener('click', () => {
newCategoryFormRow.classList.add('d-none');
addCategoryBtn.disabled = false;
modal.querySelector('#newCategoryTitle').value = '';
modal.querySelector('#newCategoryDescription').value = '';
});
saveNewCategoryBtn.addEventListener('click', () => {
const titleInput = modal.querySelector('#newCategoryTitle');
const descriptionInput = modal.querySelector('#newCategoryDescription');
const title = titleInput.value.trim();
const description = descriptionInput.value.trim();
// Валидация
if (!title || title.length < 2) {
showInfo('Название категории должно содержать минимум 2 символа', 'error');
titleInput.focus();
return;
}
if (!description || description.length < 2) {
showInfo('Описание категории должно содержать минимум 2 символа', 'error');
descriptionInput.focus();
return;
}
// Проверяем уникальность названия
const duplicate = categories.find(cat =>
cat.title.toLowerCase() === title.toLowerCase() &&
cat.status !== 'deleted'
);
if (duplicate) {
showInfo('Категория с таким названием уже существует', 'error');
titleInput.focus();
return;
}
// Добавляем новую категорию
const newCategory = {
title: title,
description: description,
status: 'new',
originalData: null
};
categories.push(newCategory);
changes.create.push({
title: title,
description: description
});
// Сбрасываем форму
titleInput.value = '';
descriptionInput.value = '';
newCategoryFormRow.classList.add('d-none');
addCategoryBtn.disabled = false;
// Обновляем отображение
renderCategoriesList();
updateChangesPanel();
showInfo('Категория добавлена', 'success');
});
// Сохранение всех изменений
saveAllChangesBtn.addEventListener('click', async function () {
// Проверяем, есть ли изменения
const hasChanges = changes.create.length > 0 ||
changes.update.length > 0 ||
changes.delete.length > 0;
if (!hasChanges) {
showInfo('Нет изменений для сохранения', 'info');
return;
}
// Сохраняем исходное состояние кнопки
const originalText = saveChangesText.textContent;
const originalDisabledState = saveAllChangesBtn.disabled;
// Двойное подтверждение
saveChangesText.textContent = 'Нажмите еще раз для подтверждения (10 сек)';
saveAllChangesBtn.disabled = true;
let confirmed = false;
const timeout = setTimeout(() => {
if (!confirmed) {
// Возвращаем кнопку в исходное состояние
saveChangesText.textContent = originalText;
saveAllChangesBtn.disabled = originalDisabledState;
saveChangesSpinner.style.display = 'none';
}
}, 10000);
const confirmHandler = async function () {
confirmed = true;
clearTimeout(timeout);
saveAllChangesBtn.disabled = true;
saveChangesSpinner.style.display = 'inline-block';
try {
// Отправляем запрос на сохранение изменений
const response = await apiRequest('/toolkit/categories_batch', {
changes: changes,
userId: userData.id
}, 'POST');
if (response.status === 'ok') {
showInfo('Изменения успешно сохранены', 'success');
bsModal.hide();
// Обновляем вкладку инструментов
await uploadTab('toolkits');
} else {
throw new Error(response.message || 'Ошибка при сохранении изменений');
}
} catch (error) {
console.error('Ошибка при сохранении изменений:', error);
// Возвращаем кнопку в исходное состояние
saveAllChangesBtn.disabled = false;
saveChangesSpinner.style.display = 'none';
saveChangesText.textContent = originalText;
const errorDiv = modal.querySelector('#manageCategoryError');
const errorMessage = modal.querySelector('#manageCategoryErrorMessage');
if (errorDiv && errorMessage) {
errorMessage.textContent = error.message || 'Произошла ошибка при сохранении изменений';
errorDiv.classList.remove('d-none');
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Сбрасываем состояние подтверждения
confirmed = false;
}
};
// Обработчик для второго клика
const secondClickHandler = function () {
saveAllChangesBtn.removeEventListener('click', secondClickHandler);
confirmHandler();
};
saveAllChangesBtn.addEventListener('click', secondClickHandler);
saveAllChangesBtn.disabled = false;
saveChangesText.textContent = 'Подтвердите сохранение (10 сек)';
});
// Очистка при закрытии модалки
modal.addEventListener('hidden.bs.modal', () => {
setTimeout(() => {
if (modal.parentNode) modal.remove();
}, 300);
});
// Делаем функции глобально доступными для обработчиков onclick
window.editCategory = modal.editCategory;
window.cancelEditCategoryAction = modal.cancelEditCategoryAction;
window.deleteCategory = modal.deleteCategory;
window.cancelDeleteCategoryAction = modal.cancelDeleteCategoryAction;
window.restoreCategory = modal.restoreCategory;
window.cancelNewCategoryAction = modal.cancelNewCategoryAction;
// Показываем модалку
bsModal.show();
return new Promise((resolve) => {
modal.addEventListener('hidden.bs.modal', () => {
resolve(null);
});
});
}
function renderToolkitsTab(tabId, toolsList, categoriesArray) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
// Преобразуем объект в массив, если передан объект
const tools = Array.isArray(toolsArray) ? toolsArray : Object.values(toolsArray);
if (accessData.tools_creation) {
tabOptionalContent.innerHTML = `
<div class="row mb-4">
<div class="col-12">
<button class="btn btn-outline-secondary" id="manageCategoryBtn">
<i class="bi bi-gear-wide-connected me-2"></i>Категории
</button>
</div>
</div>
`;
let uniqueCategories = {};
categoriesList.forEach(cat => {
uniqueCategories[cat.id] = { id: cat.id, title: cat.title, description: cat.description };
const manageCategoryBtn = document.getElementById('manageCategoryBtn');
manageCategoryBtn.addEventListener('click', () => manageCategory(categoriesArray));
}
let categoriesData = {};
categoriesArray.forEach(cat => {
categoriesData[cat.id] = { id: cat.id, title: cat.title, description: cat.description };
});
tools.forEach(tool => {
tool['category'] = uniqueCategories[tool.category_id]?.title || '';
tool['category_desc'] = uniqueCategories[tool.category_id]?.description || '';
toolsList.forEach(tool => {
tool['category'] = categoriesData[tool.category_id]?.title || '';
tool['category_desc'] = categoriesData[tool.category_id]?.description || '';
});
// Сортируем инструменты: сначала по названию категории, затем по названию
const sortedTools = [...tools].sort((a, b) => {
// Получаем названия категорий
const catA = uniqueCategories[a.category_id]?.title || '';
const catB = uniqueCategories[b.category_id]?.title || '';
// Сначала сравниваем категории
if (catA < catB) return -1;
if (catA > catB) return 1;
// Если категории одинаковые, сравниваем названия инструментов
const titleA = a.title || '';
const titleB = b.title || '';
return titleA.localeCompare(titleB, 'ru');
});
toolsList.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
categoriesArray.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
// Создаем HTML структуру
tabContent.innerHTML = `
@@ -286,7 +1106,7 @@ function renderToolkitsTab(tabId, toolsArray, categoriesList) {
data-category="all">
Все категории
</button>
${Object.values(uniqueCategories).map(category => `
${categoriesArray.map(category => `
<button class="btn filter-btn"
data-category="${category.id}">
${category.title}
@@ -318,12 +1138,13 @@ function renderToolkitsTab(tabId, toolsArray, categoriesList) {
`;
// Рендерим карточки
renderToolkitCards(tabId, sortedTools, uniqueCategories);
renderToolkitCards(tabId, toolsList, categoriesData);
// Добавляем обработчики событий для фильтров
setupFilters(tabId, tools, uniqueCategories);
setupFilters(tabId, toolsList, categoriesData);
}
// Функция для рендеринга карточек
function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', categoryFilter = 'all') {
const container = document.getElementById(`${tabId}-cards-container`);
@@ -404,7 +1225,6 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
// Функция для настройки фильтров
function setupFilters(tabId, tools, categoriesMap) {
const container = document.getElementById(`${tabId}-cards-container`);
const searchInput = document.getElementById(`${tabId}-search-input`);
const filterButtons = document.querySelectorAll(`#${tabId}-tab-content .filter-btn`);
Binary file not shown.
Binary file not shown.
+15 -4
View File
@@ -1,4 +1,5 @@
from sqlalchemy import select
from db.handlers.toolkit import ToolkitHandler
from utils import logger
from db import CRUD
from db.schemas.categories import Category
@@ -32,15 +33,21 @@ class CategoryHandler:
)
return newCategory.toDict()
async def edit(categoryId: int, **kwargs):
async def edit(categoryData: dict, userId: int):
categoryId = categoryData.pop("id", None)
if not categoryId:
logger.error("Не указан id категории")
return {}
query = select(Category).where(Category.id == categoryId)
category = await CRUD.read(query)
if not category:
logger.error("Категория не найдена")
return {}
try:
user_id = kwargs.get("user_id", None)
editedCategory = await category.edit(**kwargs)
logger.info(
f"Обновление категории {category.title} -> {categoryData.get('title')}"
)
editedCategory = await category.edit(**categoryData)
except Exception as e:
logger.error(f"Ошибка обновления категории: {str(e)}")
return {}
@@ -48,7 +55,7 @@ class CategoryHandler:
logger.error("Категория не обновлена")
return {}
await ServiceRecordsHandler.add(
user_id, {f"Обновлена категория {category.title}": editedCategory.toDict()}
userId, {f"Обновлена категория {category.title}": editedCategory.toDict()}
)
logger.info(f"Категория {editedCategory.title} успешно обновлена")
return editedCategory.toDict()
@@ -64,6 +71,10 @@ class CategoryHandler:
return [category.toDict() for category in categories] if categories else []
async def delete(categoryId: int, user_id: int = None):
categoryInUse = await ToolkitHandler.checkCatogoryUse(categoryId)
if categoryInUse:
logger.error("Категория используется в инструментах")
return True
query = select(Category).where(Category.id == categoryId)
category = await CRUD.read(query)
if not category:
+5
View File
@@ -160,6 +160,11 @@ class ToolkitHandler:
toolkits = await CRUD.read(query, True)
return [toolkit.toDict() for toolkit in toolkits] if toolkits else []
async def checkCatogoryUse(category_id: int):
query = select(Toolkit).where(Toolkit.category_id == category_id)
toolkit = await CRUD.read(query)
return True if toolkit else False
async def delete(toolkitId: int, user_id: int = None):
query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query)
Binary file not shown.
+2 -2
View File
@@ -23,5 +23,5 @@ class Category(Base):
async def save(self):
return await CRUD.create(self, refresh=True)
async def edit(id: int, **kwargs):
return await CRUD.update(Category, id, **kwargs)
async def edit(self, **kwargs):
return await CRUD.update(Category, self.id, **kwargs)