закончил раздел работы с инструментом

This commit is contained in:
2025-12-13 14:53:38 +03:00
parent f85ca7d002
commit de572396a6
8 changed files with 494 additions and 85 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
body {
background-image: url("../images/background.png");
background-repeat: no-repeat;
background-size: cover;
background-image: url("../images/background.svg");
background-repeat: repeat;
background-size: 512px auto;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

+342 -80
View File
@@ -173,9 +173,9 @@ function prepareTabs() {
role="tabpanel">
<div class="card border-0 shadow-sm mb-1">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2">
<div class="row d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2">
<div class="card-body py-2">
<div class="card-body py-2 col-12 col-md-3">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-3 rounded me-3">
<i class="${tabData.icon} fs-3 text-primary"></i>
@@ -187,7 +187,7 @@ function prepareTabs() {
</div>
</div>
<div id="${tabId}-tab-optional-content" class="px-4 mt-3" style="max-width: 65%;"></div>
<div id="${tabId}-tab-optional-content" class="px-4 mt-3 col-12 col-md-9"></div>
</div>
@@ -1122,21 +1122,107 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
const hiddenToolCount = toolsList.filter(tool => tool.hidden).length;
let categoriesData = {};
categoriesArray.forEach(cat => {
categoriesData[cat.id] = { id: cat.id, title: cat.title, description: cat.description };
});
let specData = {}
toolsList.forEach(tool => {
tool['category'] = categoriesData[tool.category_id]?.title || '';
tool['category_desc'] = categoriesData[tool.category_id]?.description || '';
Object.entries(tool.specifications || {}).forEach(([name, value]) => {
if (specData[name]) {
if (!specData[name].includes(value)) {
specData[name].push(value);
}
} else {
specData[name] = [value];
}
});
});
function smartCompare(a, b, locale = 'ru') {
const normalizeNumber = (v) => {
if (typeof v === 'number') return v;
if (typeof v === 'string') {
const n = v.replace(',', '.').trim();
if (!isNaN(n) && n !== '') return Number(n);
}
return null;
};
const numA = normalizeNumber(a);
const numB = normalizeNumber(b);
// Оба — числа
if (numA !== null && numB !== null) {
return numA - numB;
}
// Один число, другой строка → число выше
if (numA !== null) return -1;
if (numB !== null) return 1;
// Оба строки
return String(a).localeCompare(String(b), locale, {
numeric: true,
sensitivity: 'base'
});
}
// Сортируем ключи и значения specData
const sortedSpecData = Object.fromEntries(
Object.entries(specData)
.sort(([keyA], [keyB]) => smartCompare(keyA, keyB))
.map(([key, values]) => [
key,
[...values].sort((a, b) => smartCompare(a, b))
])
);
toolsList.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
categoriesArray.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
if (accessData.tools_creation) {
tabOptionalContent.innerHTML = `
<div class="row">
<div class="col-12">
<div>
<div class="row align-items-start">
<!-- Категории слева -->
<div class="col-12 col-md-7 mb-3 mb-md-0">
<div class="d-flex flex-wrap gap-2">
${categoriesArray.map(category => `
<button class="btn filter-btn"
data-category="${category.id}">
${category.title}
</button>
`).join('')}
<button class="btn filter-btn active" data-category="all">
Все категории
</button>
</div>
</div>
<!-- Управления справа -->
<div class="col-12 col-md-5 d-flex flex-column align-items-md-end gap-2">
<!-- Группа кнопок -->
<div class="btn-group" role="group" aria-label="Управление категориями и инструментами">
<button class="btn btn-outline-secondary" id="manageCategoryBtn">
<i class="bi bi-gear-wide-connected me-2"></i>Категории
</button>
<button class="btn btn-outline-secondary ms-2" id="addToolBtn">
<button class="btn btn-outline-secondary" id="addToolBtn">
<i class="bi bi-plus-circle me-2"></i>Добавить инструмент
</button>
</div>
<div class="form-check form-switch d-flex flex-column flex-md-row align-items-md-center justify-content-left mt-1">
<!-- Переключатель -->
<div class="form-check form-switch d-flex align-items-center justify-content-md-end">
<input class="form-check-input" type="checkbox" role="switch" id="showHiddenTools">
<label class="form-check-label ms-2 text-muted" for="showHiddenTools">Отображать скрытые (${hiddenToolCount})</label>
<label class="form-check-label ms-2 text-muted" for="showHiddenTools">
Отображать скрытые (${hiddenToolCount})
</label>
</div>
</div>
</div>
@@ -1160,42 +1246,55 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
`;
}
let categoriesData = {};
categoriesArray.forEach(cat => {
categoriesData[cat.id] = { id: cat.id, title: cat.title, description: cat.description };
});
toolsList.forEach(tool => {
tool['category'] = categoriesData[tool.category_id]?.title || '';
tool['category_desc'] = categoriesData[tool.category_id]?.description || '';
});
toolsList.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
categoriesArray.sort((a, b) => a.title.localeCompare(b.title, 'ru'));
// Создаем HTML структуру
// Создаем HTML структуру с двумя выпадающими списками
tabContent.innerHTML = `
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<!-- Блок фильтров -->
<div class="row mb-4">
<div class="col-12 col-md-8 mb-3 mb-md-0">
<div class="d-flex flex-wrap gap-2">
<button class="btn filter-btn active"
data-category="all">
Все категории
</button>
${categoriesArray.map(category => `
<button class="btn filter-btn"
data-category="${category.id}">
${category.title}
</button>
`).join('')}
<div class="row mb-4 align-items-center">
<!-- Фильтры по параметрам -->
<div class="col-12 col-lg-8 mb-3 mb-lg-0">
<div class="row g-2">
<div class="col-12 col-md-6 col-lg-4">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-tags"></i>
</span>
<select class="form-select" id="${tabId}-param-select">
<option value="">Все параметры</option>
${Object.keys(sortedSpecData).map(param => `
<option value="${param}">${param}</option>
`).join('')}
</select>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-filter"></i>
</span>
<select class="form-select" id="${tabId}-value-select" disabled>
<option value="">Все значения</option>
</select>
</div>
</div>
<div class="col-12 col-md-12 col-lg-4">
<div class="btn-group w-100" role="group" aria-label="Фильтр спецификаций">
<button class="btn btn-primary" type="button" id="${tabId}-find-spec-btn" disabled>
<i class="bi bi-search me-1"></i>Найти
</button>
<button class="btn btn-outline-secondary" type="button" id="${tabId}-reset-spec-btn">
<i class="bi bi-x-circle me-1"></i>Сброс
</button>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<!-- Поиск -->
<div class="col-12 col-lg-4">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-search"></i>
@@ -1204,6 +1303,11 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
class="form-control"
id="${tabId}-search-input"
placeholder="Поиск по названию и описанию...">
<button class="btn btn-outline-secondary d-none"
type="button"
id="${tabId}-clear-search">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
@@ -1222,28 +1326,44 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
renderToolkitCards(tabId, toolsList, categoriesData);
// Добавляем обработчики событий для фильтров
setupFilters(tabId, toolsList, categoriesData);
setupFilters(tabId, toolsList, categoriesData, sortedSpecData);
}
// Функция для рендеринга карточек
function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', categoryFilter = 'all', showHiddenTools = false) {
function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', categoryFilter = 'all', showHiddenTools = false, specParam = '', specValue = '') {
const container = document.getElementById(`${tabId}-cards-container`);
// Фильтруем инструменты
const filteredTools = tools.filter(tool => {
// Показываем скрытые инструменты, если флаг установлен
const showHidden = showHiddenTools || !tool.hidden;
// Фильтр по категории
const categoryMatch = categoryFilter === 'all' || tool.category_id == categoryFilter;
if (categoryFilter !== 'all' && tool.category_id !== parseInt(categoryFilter)) {
return false;
}
// Фильтр по тексту
const searchMatch = !filterText ||
(tool.title && tool.title.toLowerCase().includes(filterText.toLowerCase())) ||
(tool.description && tool.description.toLowerCase().includes(filterText.toLowerCase()));
// Фильтр по скрытым инструментам
if (!showHiddenTools && tool.hidden) {
return false;
}
return categoryMatch && searchMatch && showHidden;
// Фильтр по поисковому запросу
if (filterText) {
const searchLower = filterText.toLowerCase();
const titleMatch = tool.title.toLowerCase().includes(searchLower);
const descriptionMatch = tool.description.toLowerCase().includes(searchLower);
if (!titleMatch && !descriptionMatch) {
return false;
}
}
// Фильтр по спецификациям
if (specParam && specValue) {
if (!tool.specifications || tool.specifications[specParam] !== specValue) {
return false;
}
}
return true;
});
// Рендерим карточки
@@ -1288,7 +1408,7 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
${description}
</p>
<div class="mt-2">
${tool.quantity_min ? `
${tool.quantity_min && accessData.view_all_toolboxes ? `
<small class="text-muted">
<i class="bi bi-box me-1"></i>
Мин: ${tool.quantity_min}
@@ -1312,71 +1432,207 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
}
// Функция для настройки фильтров
function setupFilters(tabId, tools, categoriesMap) {
function setupFilters(tabId, tools, categoriesMap, specData) {
const searchInput = document.getElementById(`${tabId}-search-input`);
const filterButtons = document.querySelectorAll(`#${tabId}-tab-content .filter-btn`);
const showHiddenToolsCheckbox = document.getElementById(`showHiddenTools`);
const filterButtons = document.querySelectorAll(`#${tabId}-tab-optional-content .filter-btn`);
const showHiddenToolsCheckbox = document.getElementById('showHiddenTools');
showHiddenToolsCheckbox.addEventListener('change', () => {
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category, showHiddenToolsCheckbox.checked);
});
// Новые элементы для фильтрации по спецификациям
const paramSelect = document.getElementById(`${tabId}-param-select`);
const valueSelect = document.getElementById(`${tabId}-value-select`);
const findSpecBtn = document.getElementById(`${tabId}-find-spec-btn`);
const resetSpecBtn = document.getElementById(`${tabId}-reset-spec-btn`);
const clearSearchBtn = document.getElementById(`${tabId}-clear-search`);
const savedFilters = loadFromStorage(tabId);
// Текущие значения фильтров
let currentFilter = {
category: 'all',
search: ''
const currentFilter = {
category: savedFilters.category || 'all',
search: savedFilters.search || '',
showHidden: savedFilters.showHidden ?? false,
specParam: savedFilters.specParam || '',
specValue: savedFilters.specValue || ''
};
// Обработчик для кнопок категорий
/* ---------- Восстановление UI ---------- */
if (searchInput) {
searchInput.value = currentFilter.search;
}
if (showHiddenToolsCheckbox) {
showHiddenToolsCheckbox.checked = currentFilter.showHidden;
}
filterButtons.forEach(btn => {
btn.classList.toggle(
'active',
btn.dataset.category === currentFilter.category
);
});
// Восстановление выбранного параметра
if (paramSelect && currentFilter.specParam) {
paramSelect.value = currentFilter.specParam;
updateValueSelect(currentFilter.specParam);
// Восстановление выбранного значения после обновления списка значений
setTimeout(() => {
if (valueSelect && currentFilter.specValue) {
valueSelect.value = currentFilter.specValue;
}
}, 0);
}
const render = () => {
renderToolkitCards(
tabId,
tools,
categoriesMap,
currentFilter.search,
currentFilter.category,
currentFilter.showHidden,
currentFilter.specParam,
currentFilter.specValue
);
saveToStorage(tabId, currentFilter);
};
/* ---------- Обновление списка значений при выборе параметра ---------- */
function updateValueSelect(selectedParam) {
if (valueSelect) {
valueSelect.innerHTML = '<option value="">Все значения</option>';
findSpecBtn.disabled = true;
if (selectedParam && specData[selectedParam]) {
valueSelect.disabled = false;
specData[selectedParam].forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
valueSelect.appendChild(option);
});
} else {
valueSelect.disabled = true;
}
}
}
/* ---------- Сброс фильтра спецификаций ---------- */
function resetSpecFilter() {
if (paramSelect) {
paramSelect.value = '';
}
if (valueSelect) {
valueSelect.innerHTML = '<option value="">Все значения</option>';
valueSelect.disabled = true;
}
currentFilter.specParam = '';
currentFilter.specValue = '';
render();
}
/* ---------- Обработчик выбора параметра ---------- */
if (paramSelect) {
paramSelect.addEventListener('change', function () {
const selectedParam = this.value;
updateValueSelect(selectedParam);
// Сбрасываем выбранное значение при изменении параметра
if (valueSelect) {
valueSelect.value = '';
}
});
}
/* ---------- Обработчик выбора значения ---------- */
if (valueSelect) {
valueSelect.addEventListener('change', function () {
findSpecBtn.disabled = !this.value;
});
}
/* ---------- Обработчик кнопки "Найти" для спецификаций ---------- */
if (findSpecBtn) {
findSpecBtn.addEventListener('click', function () {
currentFilter.specParam = paramSelect ? paramSelect.value : '';
currentFilter.specValue = valueSelect && !valueSelect.disabled ? valueSelect.value : '';
render();
});
}
/* ---------- Обработчик кнопки "Сброс" для спецификаций ---------- */
if (resetSpecBtn) {
resetSpecBtn.addEventListener('click', resetSpecFilter);
findSpecBtn.disabled = true;
}
/* ---------- Чекбокс ---------- */
if (showHiddenToolsCheckbox) {
showHiddenToolsCheckbox.addEventListener('change', () => {
currentFilter.showHidden = showHiddenToolsCheckbox.checked;
render();
});
}
/* ---------- Категории ---------- */
filterButtons.forEach(button => {
button.addEventListener('click', function () {
// Убираем активный класс у всех кнопок
filterButtons.forEach(btn => btn.classList.remove('active'));
// Добавляем активный класс текущей кнопке
this.classList.add('active');
currentFilter.category = this.dataset.category;
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category);
render();
});
});
// Обработчик для поля поиска
/* ---------- Поиск ---------- */
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentFilter.search = this.value.trim();
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category);
render();
}, 300);
});
// Очистка поиска
searchInput.insertAdjacentHTML('afterend', `
<button class="btn btn-outline-secondary d-none"
type="button"
id="${tabId}-clear-search">
<i class="bi bi-x-lg"></i>
</button>
`);
const clearBtn = document.getElementById(`${tabId}-clear-search`);
if (clearBtn) {
clearBtn.addEventListener('click', function () {
// Кнопка очистки поиска
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', () => {
searchInput.value = '';
currentFilter.search = '';
renderToolkitCards(tabId, tools, categoriesMap, '', currentFilter.category);
this.classList.add('d-none');
clearSearchBtn.classList.add('d-none');
render();
});
searchInput.addEventListener('input', function () {
clearBtn.classList.toggle('d-none', !this.value);
clearSearchBtn.classList.toggle('d-none', !this.value);
});
clearSearchBtn.classList.toggle('d-none', !searchInput.value);
}
}
/* ---------- Первый рендер ---------- */
render();
}
function loadFromStorage(tabId) {
try {
return JSON.parse(localStorage.getItem(`toolboxStotage:${tabId}`)) || {};
} catch {
return {};
}
}
function saveToStorage(tabId, storageData) {
localStorage.setItem(
`toolboxStotage:${tabId}`,
JSON.stringify(storageData)
);
}
function addToolbox(editData = null) {
// Проверяем, существует ли уже модальное окно
let modal = document.getElementById('addToolboxModal');
@@ -1741,6 +1997,11 @@ function renderToolboxTab(tabData) {
}
tabContent.innerHTML = toolboxContent;
const choiceToolbox = loadFromStorage('toolbox');
if (choiceToolbox.toolboxId) {
window.selectToolbox(choiceToolbox.toolboxId).then(() => { }).catch(() => { });
}
}
// Функция для выбора склада
@@ -1756,6 +2017,7 @@ window.selectToolbox = async function (toolboxId, index) {
selectedBtn.classList.add('active');
}
saveToStorage('toolbox', { toolboxId });
// Загружаем содержимое склада
await loadToolboxContent(toolboxId);
}
+2 -2
View File
@@ -23,8 +23,8 @@ async def main():
from db.initialize import DatabaseInitializer
try:
force = True
reNewDB = True
force = False
reNewDB = False
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)