From 1199407a728ef279b5e6c301294bfb0aed2f1989 Mon Sep 17 00:00:00 2001 From: Sergey Korotonozhko <_serezhka_@mail.ru> Date: Tue, 2 Jun 2026 21:25:12 +0300 Subject: [PATCH] =?UTF-8?q?v1.8.6:=20=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?,=20=D1=81=D0=BF=D0=B8=D0=BD=D0=BD=D0=B5=D1=80=D1=8B=20number?= =?UTF-8?q?=20input,=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=20=E2=80=94=20fix=20broken=20HTML,=20modal-a?= =?UTF-8?q?ctions=20=E2=86=92=20form-actions,=20card-wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + max_bot/version.txt | 2 +- py_service/CHANGELOG.md | 14 ++ py_service/SERVICE_STYLE_GUIDE.md | 2 + py_service/app/static/css/service.css | 11 ++ py_service/app/static/js/sort-table.js | 164 ++++++++++++++++++ py_service/app/templates/base.html | 1 + .../app/templates/pages/assignments.html | 38 +++- py_service/app/templates/pages/blog.html | 2 +- .../app/templates/pages/bot_consent.html | 5 +- .../app/templates/pages/bot_dialogues.html | 48 +++-- py_service/app/templates/pages/bot_kb.html | 5 +- py_service/app/templates/pages/bot_test.html | 4 +- .../app/templates/pages/bot_tickets.html | 1 + .../app/templates/pages/bot_unknown.html | 5 +- py_service/app/templates/pages/cases.html | 2 +- py_service/app/templates/pages/charts.html | 10 +- py_service/app/templates/pages/checklist.html | 2 +- .../app/templates/pages/coefficients.html | 2 +- py_service/app/templates/pages/customers.html | 5 +- py_service/app/templates/pages/formulas.html | 2 +- py_service/app/templates/pages/ideas.html | 2 +- py_service/app/templates/pages/objects.html | 2 +- .../templates/pages/questionnaire_config.html | 2 +- py_service/app/templates/pages/tasks.html | 2 +- py_service/app/templates/pages/users.html | 2 +- py_service/version.txt | 2 +- 27 files changed, 279 insertions(+), 60 deletions(-) create mode 100644 py_service/app/static/js/sort-table.js diff --git a/AGENTS.md b/AGENTS.md index ec01650..0a88e29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -235,6 +235,8 @@ docker logs aegisone-app --tail 10 - **Модалки:** `.modal-overlay` + `.modal`, открытие через `classList.add('open')` - **Структура страницы:** `card` > `card-header` > `filter-bar` > `table-wrap` > `table` - **CSS классы кнопок:** `.btn-primary` (создать), `.btn-secondary` (редактировать), `.btn-danger` (удалить), `.btn-success` (выполнено) +- **Сортировка таблиц:** `sort-table.js` подключён глобально в `base.html`, автоматически сортирует все `` по клику на `
` (текст/число/дата) +- **Number input:** спиннеры скрыты глобально через CSS (`service.css`), `type="number"` выглядит как обычный input --- diff --git a/max_bot/version.txt b/max_bot/version.txt index ff2fd4f..9eadd6b 100644 --- a/max_bot/version.txt +++ b/max_bot/version.txt @@ -1 +1 @@ -1.8.5 \ No newline at end of file +1.8.6 \ No newline at end of file diff --git a/py_service/CHANGELOG.md b/py_service/CHANGELOG.md index 8a17d9c..d776e70 100644 --- a/py_service/CHANGELOG.md +++ b/py_service/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog — AegisOne Service Portal +## 1.8.6 (02.06.2026) +### Новые функции +- **sort-table.js:** универсальный модуль сортировки таблиц по столбцам (текст/число/дата), подключён глобально +- **Number input:** спиннеры скрыты через CSS (`appearance: textfield`) на всех страницах + +### Исправления +- **customers.html:** восстановлена сломанная HTML-структура — `.card` > `.card-header`, поиск в `.filter-bar` +- **bot_dialogues.html:** обёрнут в `.card` > `.card-header`, фильтры в `.filter-bar`, удалены кастомные `.page-header`/`.header-controls` +- **checklist.html:** `

` обёрнут в `.card-header` +- **18 страниц:** `modal-actions` → `form-actions` (единый класс для кнопок в модалках) +- **charts.html:** `btn-xs` → `btn-sm` +- **assignments.html:** добавлена модалка подтверждения отвязки сотрудника +- **4 AJAX-страницы:** `sortTables()` вызывается после загрузки данных (bot_kb, bot_unknown, bot_consent, bot_tickets) + ## 1.8.5 (02.06.2026) ### Новые функции - **AGENTS.md (раздел 12):** правила по Идеям — агент может видеть, добавлять, менять статус, предлагать к реализации, реализовывать diff --git a/py_service/SERVICE_STYLE_GUIDE.md b/py_service/SERVICE_STYLE_GUIDE.md index 4acac07..f9b5121 100644 --- a/py_service/SERVICE_STYLE_GUIDE.md +++ b/py_service/SERVICE_STYLE_GUIDE.md @@ -227,4 +227,6 @@ showNotification('Текст уведомления', 'info'); // сине - [ ] Кнопки редактирования: `✎` (btn-secondary) - [ ] Порядок кнопок в модалках: Действие → Отмена - [ ] Уведомления через `showNotification()` +- [ ] Таблицы используют `sort-table.js` (автоматически через base.html) +- [ ] Number input без спиннеров (CSS в service.css) - [ ] CSS-классы модалок: `.modal-overlay` / `.modal` diff --git a/py_service/app/static/css/service.css b/py_service/app/static/css/service.css index 87585d5..e83536e 100644 --- a/py_service/app/static/css/service.css +++ b/py_service/app/static/css/service.css @@ -164,6 +164,17 @@ img { max-width:100%; height:auto; } resize: vertical; } +/* Скрытие стандартных спиннеров на number input */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + .btn { display: inline-flex; align-items: center; diff --git a/py_service/app/static/js/sort-table.js b/py_service/app/static/js/sort-table.js new file mode 100644 index 0000000..f4bf8b7 --- /dev/null +++ b/py_service/app/static/js/sort-table.js @@ -0,0 +1,164 @@ +/** + * sort-table.js — Универсальный модуль сортировки таблиц. + * + * Автоматически находит все на странице и делает + * заголовки + */ +(function(){ + 'use strict'; + + var SORT_CSS = [ + '.sort-th { cursor:pointer; user-select:none; position:relative; padding-right:18px !important; }', + '.sort-th:hover { color:var(--accent); }', + '.sort-th::after { content:""; position:absolute; right:6px; top:50%; transform:translateY(-50%); width:0; height:0; border-left:4px solid transparent; border-right:4px solid transparent; border-top:5px solid var(--text-muted); opacity:0.4; }', + '.sort-th.sort-asc::after { border-top:none; border-bottom:5px solid var(--accent); opacity:1; }', + '.sort-th.sort-desc::after { border-top:5px solid var(--accent); opacity:1; }' + ].join('\n'); + + /* Встраиваем стили один раз */ + function injectStyles(){ + if(document.getElementById('sort-table-styles')) return; + var style = document.createElement('style'); + style.id = 'sort-table-styles'; + style.textContent = SORT_CSS; + document.head.appendChild(style); + } + + /* Определение типа значения ячейки */ + function getCellValue(tr, idx){ + var td = tr.children[idx]; + if(!td) return ''; + var text = (td.textContent || '').trim(); + return text; + } + + /* Парсинг числа из текста */ + function parseNumber(text){ + if(!text || text === '—' || text === '-') return NaN; + /* Убираем пробелы,.currency-символы, % */ + var cleaned = text.replace(/[^\d,\.\-]/g, '').replace(',', '.'); + var num = parseFloat(cleaned); + return isNaN(num) ? NaN : num; + } + + /* Парсинг даты из текста (DD.MM.YYYY или YYYY-MM-DD) */ + function parseDate(text){ + if(!text) return NaN; + /* DD.MM.YYYY HH:MM */ + var m = text.match(/(\d{2})\.(\d{2})\.(\d{4})/); + if(m) return new Date(m[3], m[2]-1, m[1]).getTime(); + /* YYYY-MM-DD */ + m = text.match(/(\d{4})-(\d{2})-(\d{2})/); + if(m) return new Date(m[1], m[2]-1, m[3]).getTime(); + return NaN; + } + + /* Сравнение двух значений */ + function compare(a, b, type){ + if(type === 'number'){ + var na = parseNumber(a), nb = parseNumber(b); + if(isNaN(na) && isNaN(nb)) return 0; + if(isNaN(na)) return 1; + if(isNaN(nb)) return -1; + return na - nb; + } + if(type === 'date'){ + var da = parseDate(a), db = parseDate(b); + if(isNaN(da) && isNaN(db)) return 0; + if(isNaN(da)) return 1; + if(isNaN(db)) return -1; + return da - db; + } + /* Текст */ + return a.localeCompare(b, 'ru'); + } + + /* Определение типа столбца по первым строкам */ + function detectColumnType(tbody, idx){ + var rows = tbody.children; + var samples = []; + var limit = Math.min(rows.length, 10); + for(var i = 0; i < limit; i++){ + samples.push(getCellValue(rows[i], idx)); + } + var numCount = 0, dateCount = 0; + for(var j = 0; j < samples.length; j++){ + if(parseNumber(samples[j]) !== NaN && samples[j] !== '') numCount++; + if(parseDate(samples[j]) !== NaN && samples[j] !== '') dateCount++; + } + if(samples.length > 0 && numCount / samples.length > 0.6) return 'number'; + if(samples.length > 0 && dateCount / samples.length > 0.6) return 'date'; + return 'text'; + } + + /* Привязка сортировки к таблице */ + function attachSort(table){ + if(table.dataset.sortAttached) return; + table.dataset.sortAttached = '1'; + + var thead = table.querySelector('thead'); + var tbody = table.querySelector('tbody'); + if(!thead || !tbody) return; + + var headers = thead.querySelectorAll('th'); + headers.forEach(function(th, idx){ + /* Пропускаем колонку действий */ + if(th.classList.contains('actions')) return; + + th.classList.add('sort-th'); + th.dataset.sortIdx = idx; + th.dataset.sortDir = 'none'; + + th.addEventListener('click', function(){ + var colIdx = parseInt(this.dataset.sortIdx); + var currentDir = this.dataset.sortDir; + var newDir = currentDir === 'asc' ? 'desc' : 'asc'; + + /* Сброс других столбцов */ + headers.forEach(function(h){ h.classList.remove('sort-asc', 'sort-desc'); h.dataset.sortDir = 'none'; }); + this.classList.remove('sort-asc', 'sort-desc'); + this.classList.add('sort-' + newDir); + this.dataset.sortDir = newDir; + + /* Определение типа данных */ + var colType = detectColumnType(tbody, colIdx); + + /* Сбор строк с значениями */ + var rowsArray = Array.prototype.slice.call(tbody.querySelectorAll('tr')); + rowsArray.sort(function(rowA, rowB){ + var valA = getCellValue(rowA, colIdx); + var valB = getCellValue(rowB, colIdx); + var result = compare(valA, valB, colType); + return newDir === 'asc' ? result : -result; + }); + + /* Перестановка строк */ + rowsArray.forEach(function(row){ tbody.appendChild(row); }); + }); + }); + } + + /* Публичная функция: инициализация/переинициализация всех таблиц */ + function sortTables(){ + injectStyles(); + var tables = document.querySelectorAll('table'); + for(var i = 0; i < tables.length; i++){ + attachSort(tables[i]); + } + } + + /* Автозапуск после загрузки DOM */ + if(document.readyState === 'loading'){ + document.addEventListener('DOMContentLoaded', sortTables); + } else { + sortTables(); + } + + /* Экспорт в глобальную область */ + window.sortTables = sortTables; +})(); diff --git a/py_service/app/templates/base.html b/py_service/app/templates/base.html index afc25b6..d2b5be9 100644 --- a/py_service/app/templates/base.html +++ b/py_service/app/templates/base.html @@ -55,6 +55,7 @@
+ {% endblock %} diff --git a/py_service/app/templates/pages/blog.html b/py_service/app/templates/pages/blog.html index 32053f0..c86830b 100644 --- a/py_service/app/templates/pages/blog.html +++ b/py_service/app/templates/pages/blog.html @@ -39,7 +39,7 @@

будет удалён навсегда. Это действие нельзя отменить.

-
кликабельными для сортировки по столбцам. + * + * Использование: + * 1. Подключить + * 2. Таблицы автоматически получают сортировку при загрузке + * 3. Для AJAX-таблиц: вызвать sortTables() после заполнения
{% for d in customers %} @@ -59,7 +58,7 @@

будет удалён навсегда. Это действие нельзя отменить.

-
НазваниеИННКонтактыСтатусОбъектыАктивные SLA