diff --git a/CHANGELOG.md b/CHANGELOG.md index 40039fc..da12aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,56 +1,91 @@ # Changelog -## [sync2 vibe] — 2026-03-23 +## [statistica vibe 3] — 2026-03-23 -### Новое: «Служебный статус в интре» — двусторонняя синхронизация +> Правки и улучшения статистики после `statistica vibe 2` -#### Колонка в таблице заявок +### Улучшения -- Добавлена новая колонка **«Служ. статус в интре»** (`servstatus`) — отображает значение дополнительного поля `addfield_2f6eaa2vnutrennij_statusSingleSelect` из IntraDesk -- Колонка интерактивная: при наведении открывается выпадающий пикер с полным списком внутренних статусов (аналогично колонке «Статус») -- Текущее значение подсвечивается цветом соответствующего внутреннего статуса (если совпадает по label) -- Есть пункт «— очистить» для сброса значения в IntraDesk +- **Тепловая карта по месяцам** переработана: вместо сетки месяц×день-недели теперь отображается одна строка из 12 ячеек (по одной на месяц), каждая окрашена по числу заявок. Значение показывается при наведении. +- **Блок сравнения** («сейчас / неделю назад / начало месяца») теперь растягивается в высоту графика (`align-items: stretch`), элементы равномерно распределяются по высоте — цифры больше не выступают за пределы графика. +- **Повторные клиенты** — один счётчик «открытых» через `isTaskOpen` (в т.ч. «в работе», «новая», «назначена» и т.п.); одна акцентная полоса и бейдж `N откр.` без разделения цветов. -#### Синхронизация IntraDesk → наш статус (входящая) +### Исправления -- При каждом обновлении задачи в ходе инкрементального или полного сканирования проверяется значение доп. поля -- Если значение доп. поля совпадает с label одного из активных внутренних статусов — этот статус автоматически выставляется через `setExclusiveMark` (без участия пользователя) -- Управляется настройкой `sync_addfield_status` (по умолчанию выключена) +- **Карточка «Клиенты» + «Только с открытыми»**: при включённом фильтре внутренняя полоса теперь заполняет внешнюю полностью (`opPct = 100%`), чтобы не было двойного масштабирования и визуально «другой» закраски относительно режима без фильтра. -#### Синхронизация наш статус → IntraDesk (исходящая) +### Затронутые файлы -- При смене внутреннего статуса через `POST /api/marks` значение нового статуса (его `label`) автоматически записывается в доп. поле IntraDesk через `PUT /changes/v3/tasks/` -- Если статус снимается без замены — доп. поле очищается (`null`) -- Работает только при включённом `sync_addfield_status` -#### Прямое редактирование из UI +| Файл | Изменения | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `public/js/statistics.js` | `MONTH_COUNTS` вместо `MONTH_HM`; новый HTML месячной тепловой карты; `renderClientsList` с единой логикой `open` и правкой `opPct` для `clientsOnlyOpen` | +| `public/css/app.css` | `.stats-month-hm`*; `align-items: stretch` в `.stats-open-wrap`; уменьшен `.soc-val`; стили бейджа клиентов без раздельных полос | -- Новый эндпоинт `POST /api/tasks/:taskId/intra-addfield` — позволяет записать произвольное значение в доп. поле IntraDesk напрямую, независимо от внутреннего статуса -- После успешной записи локальный кеш задачи обновляется немедленно (без ожидания следующего сканирования) -- SSE-событие `intra_addfield` рассылается всем подключённым клиентам — обновление отображается в реальном времени на других вкладках/у других пользователей -- Тост-уведомление показывается другим пользователям при изменении -#### Настройки +--- -- Новая настройка **«Синхронизировать служебный статус с доп. полем IntraDesk (двусторонняя)»** в панели администратора → Status Automation -- Ключ: `sync_addfield_status`, булева строка `'0'`/`'1'` +## [123 → statistica vibe 2] — 2026-03-23 -#### Запуск локально +> Коммиты: `fc2b7f7 (123)` · `232a6b4 (sync2 vibe)` · `45773f1 (statistica vibe 1)` · `77d3ac9 (statistica vibe 2)` -- Добавлен `docker-compose.local.yml` — compose-файл для локальной сборки из исходников (вместо пулла готового образа из registry), данные хранятся в `./data` -- Добавлен `start-local.bat` — батник для запуска локального Docker одним двойным кликом +--- + +### Новые функции + +#### Колонка «Служ. статус в интре» — двусторонняя синхронизация с IntraDesk + +- Добавлена колонка **«Служ. статус в интре»** (`servstatus`) в таблице заявок — отображает значение дополнительного поля `addfield_2f6eaa2vnutrennij_statusSingleSelect` из IntraDesk; цвет подбирается по совпадению с label внутреннего статуса +- Колонка интерактивная: при наведении открывается пикер для смены значения поля напрямую в IntraDesk (независимо от внутреннего статуса), с пунктом «— очистить» +- **Исходящая синхронизация**: при смене внутреннего статуса через `POST /api/marks` значение автоматически записывается в доп. поле IntraDesk через `PUT /changes/v3/tasks/`; при снятии статуса — поле очищается (`null`) +- **Входящая синхронизация**: при каждом обновлении задачи значение доп. поля сравнивается с label активных внутренних статусов — при совпадении статус выставляется автоматически +- Новый эндпоинт `POST /api/tasks/:taskId/intra-addfield` для прямой записи значения поля без смены внутреннего статуса +- SSE-событие `intra_addfield` рассылается всем клиентам при изменении — обновление отображается в реальном времени, тост-уведомление для других пользователей +- Настройка **«Синхронизировать служебный статус с доп. полем IntraDesk»** в панели администратора → Status Automation (ключ `sync_addfield_status`, по умолчанию выключена) + +#### Страница статистики — новые элементы + +- **Тепловая карта по месяцам** (месяц × день недели) — 12×7 сетка, показывает распределение нагрузки по месяцам и дням недели; размещена в одном ряду с картой «час × день» +- **Блок сравнения** в карточке «Открытых заявок на конец недели» — три значения: сейчас / неделю назад / начало месяца; цветной бейдж роста (↑ красный) или снижения (↓ зелёный); расположен справа от графика как компактная боковая панель +- **Кликабельные клиенты** в карточке «Клиенты» — клик по строке клиента закрывает статистику и открывает главную страницу с автоматически выставленным фильтром «Объекты» по данному клиенту + +#### Страница статистики — улучшения UX + +- При наведении на столбик в графике «Создано / Закрыто по неделям»: столбики подсвечиваются (`brightness`), над ними появляется метка со значениями (создано / закрыто) + +#### Фильтры на главной странице + +- Поиск внутри выпадающего фильтра **«Объекты»** — текстовое поле для быстрой фильтрации длинного списка объектов; появляется при открытии дропдауна, автофокус + +#### Локальный запуск + +- `docker-compose.local.yml` — compose-файл для сборки образа из исходников (вместо пулла из registry), данные монтируются из `./data` +- `start-local.bat` — батник для Windows: проверяет наличие `.env`, создаёт `./data`, запускает `docker compose ... up -d --build` одним двойным кликом + +--- + +### Исправления + +- **Тепловая карта (час × день)**: CSS-селектор `[style*="--hi:0"]` некорректно совпадал с любым значением начинающимся с `0` (т.е. `0.200`, `0.400` и т.д.) — в результате окрашивался только один максимальный квадрат. Исправлено на `[style*="--hi:0.000"]` +- **Кнопка тёмной темы**: скрыта (`display:none`); логика `toggleTheme` и состояние темы сохранены +- **Клиенты с кавычками в названии** (напр. `БЦ "Луч"`): двойные кавычки разрушали HTML-атрибут `data-client`, из-за чего фильтр не применялся. Добавлен `escAttr` с экранированием `"` → `"` --- ### Затронутые файлы -| Файл | Изменения | -|---|---| -| `refresh-service.js` | Константа алиаса доп. поля; извлечение `_addfield_status` при fetch; `updateIntraAdditionalField()`; inbound-синхронизация в `applyStatusAutomationForChangedTasks`; `sync_addfield_status` в `normalizeSettings` | -| `server.js` | Outbound-вызов после `POST /api/marks`; новый эндпоинт `POST /api/tasks/:taskId/intra-addfield`; настройка `sync_addfield_status` в `ALLOWED_SETTINGS` и `GET/POST /api/settings` | -| `public/js/ui.js` | Колонка `servstatus` в `ALL_COLS`; функции `_addfieldBadgeHtml`, `intraAddfieldPickerHtml`, `setIntraAddfield` | -| `public/js/sse.js` | Обработчик события `intra_addfield` | -| `public/js/auth.js` | Загрузка/сохранение чекбокса `sync_addfield_status` | -| `public/index.html` | Чекбокс настройки в панели Status Automation | -| `docker-compose.local.yml` | Новый файл — compose для локальной разработки | -| `start-local.bat` | Новый файл — скрипт запуска локального Docker | + +| Файл | Изменения | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `refresh-service.js` | Константа `INTRA_ADDFIELD_ALIAS`; извлечение `_addfield_status` при fetch; `updateIntraAdditionalField()`; inbound-синхронизация в `applyStatusAutomationForChangedTasks`; `sync_addfield_status` в `normalizeSettings` | +| `server.js` | Outbound-вызов после `POST /api/marks`; новый эндпоинт `POST /api/tasks/:taskId/intra-addfield`; настройка `sync_addfield_status` в `ALLOWED_SETTINGS` и `GET/POST /api/settings` | +| `public/js/ui.js` | Колонка `servstatus` в `ALL_COLS`; `intraAddfieldBadge`, `intraAddfieldPickerHtml`, `setIntraAddfield`; `hasSearch: true` для фильтра «Объекты»; поисковый блок в `buildAdvancedFilterPanel` | +| `public/js/sse.js` | Обработчик события `intra_addfield` | +| `public/js/auth.js` | Загрузка и сохранение чекбокса `sync_addfield_status` | +| `public/js/statistics.js` | Блок сравнения открытых заявок (wDiffBadge, openAtMonthStart); тепловая карта по месяцам (MONTH_HM, MONTH_NAMES); кликабельные клиенты (`filterByClient`, `escAttr`); hover-label на столбиках активности | +| `public/css/app.css` | Стили блока сравнения (`.stats-open-wrap`, `.stats-open-compare`, `.soc-`*, `.stats-wdiff`); hover-label (`.stats-act-hover-label`, `.sahl-*`); поиск фильтра (`.adv-filter-search-wrap`, `.adv-filter-search`); кликабельные строки клиентов; исправление тепловой карты (`--hi:0.000`) | +| `public/index.html` | Чекбокс настройки `sync_addfield_status`; `display:none` на кнопке темы | +| `docker-compose.local.yml` | Новый файл — compose для локальной разработки | +| `start-local.bat` | Новый файл — скрипт запуска локального Docker | + + diff --git a/public/css/app.css b/public/css/app.css index 19a5189..a08f1e8 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -974,6 +974,44 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-h .stats-timing-lbl { font-size: 11px; color: var(--text3); margin-top: 3px; } .stats-timing-note { font-size: 11px; color: var(--text3); margin-top: 8px; } +/* ── Month heatmap (12 cells) ───────────────────────────────── */ +.stats-month-hm { + display: flex; + gap: 5px; + flex-wrap: wrap; + margin-top: 10px; +} +.stats-month-hm-cell { + flex: 1; + min-width: 40px; + height: 56px; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + cursor: default; + transition: transform .12s; + background: color-mix(in srgb, var(--accent) calc(var(--hi) * 100%), var(--surface2)); +} +.stats-month-hm-cell[style*="--hi:0.000"] { background: var(--surface2); } +.stats-month-hm-cell:hover { transform: scale(1.08); z-index: 5; position: relative; } +.stats-month-hm-val { + font-size: 14px; + font-weight: 800; + color: rgba(255,255,255,0); + line-height: 1; + transition: color .12s; +} +.stats-month-hm-cell:hover .stats-month-hm-val { color: white; } +.stats-month-hm-lbl { + font-size: 10px; + font-weight: 700; + color: var(--text2); +} +html.dark .stats-month-hm-cell:hover .stats-month-hm-val { color: rgba(255,255,255,.95); } + /* ── Heatmap ─────────────────────────────────────────────────── */ .stats-heatmap-wrap { display:flex; flex-direction:column; gap:2px; overflow-x:auto; } .stats-heatmap-hours { display:flex; align-items:center; gap:2px; padding-left:34px; } @@ -1037,7 +1075,7 @@ html.dark .stats-hm-cell:hover span { color: rgba(255,255,255,.95); } } /* ── Open-tickets week/month compare ────────────────────────── */ -.stats-open-wrap { display:flex; gap:16px; align-items:flex-start; } +.stats-open-wrap { display:flex; gap:16px; align-items: stretch; } .stats-open-chart { flex:1; min-width:0; } .stats-open-compare { display: flex; @@ -1046,25 +1084,27 @@ html.dark .stats-hm-cell:hover span { color: rgba(255,255,255,.95); } border: 1px solid var(--border); border-radius: var(--radius); flex-shrink: 0; - min-width: 108px; + min-width: 100px; overflow: hidden; } .soc-item { + flex: 1; display: flex; flex-direction: column; align-items: center; - gap: 3px; - padding: 12px 16px; + justify-content: center; + gap: 2px; + padding: 6px 14px; } -.soc-divider { height:1px; background:var(--border); } +.soc-divider { height:1px; background:var(--border); flex-shrink:0; } .soc-val { - font-size: 24px; + font-size: 18px; font-weight: 800; color: var(--text); line-height: 1; } .soc-label { - font-size: 10px; + font-size: 9px; color: var(--text3); text-transform: uppercase; letter-spacing: .04em; diff --git a/public/js/statistics.js b/public/js/statistics.js index b6acf2b..2ac595f 100644 --- a/public/js/statistics.js +++ b/public/js/statistics.js @@ -133,7 +133,7 @@ function renderStatsPage() { data.forEach(item=>{const d=(now-new Date(item.task.createdat||item.task.updatedat))/86400000;for(const b of AB){if(d<=b.max){b.count++;break;}}}); const maxAB=Math.max(...AB.map(b=>b.count),1); - // Клиенты + // Клиенты: «открытые» = одна логика (открыта / в работе / новая / назначена — как isTaskOpen) const cMap = {}; data.forEach(({task:t})=>{ const n=t._clientname||t.clientname||'Неизвестно'; @@ -169,14 +169,13 @@ function renderStatsPage() { const HM_ORDER = [1,2,3,4,5,6,0]; const HM_LABELS = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс']; - // Тепловая карта по месяцам (месяц × день недели) - const MONTH_HM = Array.from({length:12}, () => new Array(7).fill(0)); + // Тепловая карта по месяцам (только 12 ячеек) + const MONTH_COUNTS = new Array(12).fill(0); data.forEach(({task:t}) => { if (!t.createdat) return; - const d = new Date(t.createdat); - MONTH_HM[d.getMonth()][d.getDay()]++; + MONTH_COUNTS[new Date(t.createdat).getMonth()]++; }); - const monthHmMax = Math.max(...MONTH_HM.flat(), 1); + const monthCountMax = Math.max(...MONTH_COUNTS, 1); const MONTH_NAMES = ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек']; // 4. Без исполнителя @@ -468,21 +467,19 @@ function renderStatsPage() { -