diff --git a/api/static/css/layout.css b/api/static/css/layout.css index 232bc82..b7d1727 100644 --- a/api/static/css/layout.css +++ b/api/static/css/layout.css @@ -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; } diff --git a/api/static/images/background.png b/api/static/images/background.png deleted file mode 100644 index 90306e4..0000000 Binary files a/api/static/images/background.png and /dev/null differ diff --git a/api/static/images/background.svg b/api/static/images/background.svg new file mode 100644 index 0000000..ba9e805 --- /dev/null +++ b/api/static/images/background.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/static/images/tools/plastina_2_1765571207912.png b/api/static/images/tools/plastina_2_1765571207912.png deleted file mode 100644 index 4824077..0000000 Binary files a/api/static/images/tools/plastina_2_1765571207912.png and /dev/null differ diff --git a/api/static/images/tools/test2_1765562971246.png b/api/static/images/tools/skrytyy_instrument_1765613075463.png similarity index 100% rename from api/static/images/tools/test2_1765562971246.png rename to api/static/images/tools/skrytyy_instrument_1765613075463.png diff --git a/api/static/images/tools/test_1765562719945.png b/api/static/images/tools/test_1765562719945.png deleted file mode 100644 index a33524b..0000000 Binary files a/api/static/images/tools/test_1765562719945.png and /dev/null differ diff --git a/api/static/js/index.js b/api/static/js/index.js index ffa0ede..096b881 100644 --- a/api/static/js/index.js +++ b/api/static/js/index.js @@ -173,9 +173,9 @@ function prepareTabs() { role="tabpanel">
-
+
-
+
@@ -187,7 +187,7 @@ function prepareTabs() {
-
+
@@ -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 = ` -
-
-
+
+ +
+
+ ${categoriesArray.map(category => ` + + `).join('')} + +
+
+ + +
+ +
-
-
+ + +
- +
@@ -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 = `
-
-
-
- - ${categoriesArray.map(category => ` - - `).join('')} +
+ +
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
+
+ + +
+
-
+ + +
@@ -1204,6 +1303,11 @@ function renderToolkitsTab(tabId, toolsList, categoriesArray) { class="form-control" id="${tabId}-search-input" placeholder="Поиск по названию и описанию..."> +
@@ -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}

- ${tool.quantity_min ? ` + ${tool.quantity_min && accessData.view_all_toolboxes ? ` Мин: ${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 = ''; + 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 = ''; + 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', ` - - `); - - 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); } diff --git a/main.py b/main.py index 5e2761f..598c13e 100644 --- a/main.py +++ b/main.py @@ -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)