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

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 { body {
background-image: url("../images/background.png"); background-image: url("../images/background.svg");
background-repeat: no-repeat; background-repeat: repeat;
background-size: cover; 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

+341 -79
View File
@@ -173,9 +173,9 @@ function prepareTabs() {
role="tabpanel"> role="tabpanel">
<div class="card border-0 shadow-sm mb-1"> <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="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 me-3">
<i class="${tabData.icon} fs-3 text-primary"></i> <i class="${tabData.icon} fs-3 text-primary"></i>
@@ -187,7 +187,7 @@ function prepareTabs() {
</div> </div>
</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> </div>
@@ -1122,21 +1122,107 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`); const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
const hiddenToolCount = toolsList.filter(tool => tool.hidden).length; 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) { if (accessData.tools_creation) {
tabOptionalContent.innerHTML = ` tabOptionalContent.innerHTML = `
<div class="row"> <div class="row align-items-start">
<div class="col-12"> <!-- Категории слева -->
<div> <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"> <button class="btn btn-outline-secondary" id="manageCategoryBtn">
<i class="bi bi-gear-wide-connected me-2"></i>Категории <i class="bi bi-gear-wide-connected me-2"></i>Категории
</button> </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>Добавить инструмент <i class="bi bi-plus-circle me-2"></i>Добавить инструмент
</button> </button>
</div> </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"> <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> </div>
</div> </div>
@@ -1160,42 +1246,55 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
`; `;
} }
let categoriesData = {}; // Создаем HTML структуру с двумя выпадающими списками
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 структуру
tabContent.innerHTML = ` tabContent.innerHTML = `
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-body"> <div class="card-body">
<!-- Блок фильтров --> <!-- Блок фильтров -->
<div class="row mb-4"> <div class="row mb-4 align-items-center">
<div class="col-12 col-md-8 mb-3 mb-md-0"> <!-- Фильтры по параметрам -->
<div class="d-flex flex-wrap gap-2"> <div class="col-12 col-lg-8 mb-3 mb-lg-0">
<button class="btn filter-btn active" <div class="row g-2">
data-category="all"> <div class="col-12 col-md-6 col-lg-4">
Все категории <div class="input-group">
</button> <span class="input-group-text">
${categoriesArray.map(category => ` <i class="bi bi-tags"></i>
<button class="btn filter-btn" </span>
data-category="${category.id}"> <select class="form-select" id="${tabId}-param-select">
${category.title} <option value="">Все параметры</option>
</button> ${Object.keys(sortedSpecData).map(param => `
<option value="${param}">${param}</option>
`).join('')} `).join('')}
</select>
</div> </div>
</div> </div>
<div class="col-12 col-md-4"> <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-lg-4">
<div class="input-group"> <div class="input-group">
<span class="input-group-text"> <span class="input-group-text">
<i class="bi bi-search"></i> <i class="bi bi-search"></i>
@@ -1204,6 +1303,11 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
class="form-control" class="form-control"
id="${tabId}-search-input" id="${tabId}-search-input"
placeholder="Поиск по названию и описанию..."> 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> </div>
</div> </div>
@@ -1222,28 +1326,44 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) {
renderToolkitCards(tabId, toolsList, categoriesData); 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 container = document.getElementById(`${tabId}-cards-container`);
// Фильтруем инструменты // Фильтруем инструменты
const filteredTools = tools.filter(tool => { 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 || if (!showHiddenTools && tool.hidden) {
(tool.title && tool.title.toLowerCase().includes(filterText.toLowerCase())) || return false;
(tool.description && tool.description.toLowerCase().includes(filterText.toLowerCase())); }
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} ${description}
</p> </p>
<div class="mt-2"> <div class="mt-2">
${tool.quantity_min ? ` ${tool.quantity_min && accessData.view_all_toolboxes ? `
<small class="text-muted"> <small class="text-muted">
<i class="bi bi-box me-1"></i> <i class="bi bi-box me-1"></i>
Мин: ${tool.quantity_min} Мин: ${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 searchInput = document.getElementById(`${tabId}-search-input`);
const filterButtons = document.querySelectorAll(`#${tabId}-tab-content .filter-btn`); const filterButtons = document.querySelectorAll(`#${tabId}-tab-optional-content .filter-btn`);
const showHiddenToolsCheckbox = document.getElementById(`showHiddenTools`); 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);
// Текущие значения фильтров const currentFilter = {
let currentFilter = { category: savedFilters.category || 'all',
category: 'all', search: savedFilters.search || '',
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 => { filterButtons.forEach(button => {
button.addEventListener('click', function () { button.addEventListener('click', function () {
// Убираем активный класс у всех кнопок
filterButtons.forEach(btn => btn.classList.remove('active')); filterButtons.forEach(btn => btn.classList.remove('active'));
// Добавляем активный класс текущей кнопке
this.classList.add('active'); this.classList.add('active');
currentFilter.category = this.dataset.category; currentFilter.category = this.dataset.category;
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category); render();
}); });
}); });
// Обработчик для поля поиска /* ---------- Поиск ---------- */
if (searchInput) { if (searchInput) {
let searchTimeout; let searchTimeout;
searchInput.addEventListener('input', function () { searchInput.addEventListener('input', function () {
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
currentFilter.search = this.value.trim(); currentFilter.search = this.value.trim();
renderToolkitCards(tabId, tools, categoriesMap, currentFilter.search, currentFilter.category); render();
}, 300); }, 300);
}); });
// Очистка поиска // Кнопка очистки поиска
searchInput.insertAdjacentHTML('afterend', ` if (clearSearchBtn) {
<button class="btn btn-outline-secondary d-none" clearSearchBtn.addEventListener('click', () => {
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 () {
searchInput.value = ''; searchInput.value = '';
currentFilter.search = ''; currentFilter.search = '';
renderToolkitCards(tabId, tools, categoriesMap, '', currentFilter.category); clearSearchBtn.classList.add('d-none');
this.classList.add('d-none'); render();
}); });
searchInput.addEventListener('input', function () { 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) { function addToolbox(editData = null) {
// Проверяем, существует ли уже модальное окно // Проверяем, существует ли уже модальное окно
let modal = document.getElementById('addToolboxModal'); let modal = document.getElementById('addToolboxModal');
@@ -1741,6 +1997,11 @@ function renderToolboxTab(tabData) {
} }
tabContent.innerHTML = toolboxContent; 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'); selectedBtn.classList.add('active');
} }
saveToStorage('toolbox', { toolboxId });
// Загружаем содержимое склада // Загружаем содержимое склада
await loadToolboxContent(toolboxId); await loadToolboxContent(toolboxId);
} }
+2 -2
View File
@@ -23,8 +23,8 @@ async def main():
from db.initialize import DatabaseInitializer from db.initialize import DatabaseInitializer
try: try:
force = True force = False
reNewDB = True reNewDB = False
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)