diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7ffa49a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +## 2026-03-23 + +--- + +### ИнтраDesk: служебный статус и синхронизация + +- Колонка **«Служ. статус в интре»** — значение доп. поля "Внутренний статус" в интре; цвет по совпадению label с внутренними статусами. Имена жестко привязаны в интре +- Интерактивный пикер: смена значения в IntraDesk напрямую, пункт «— очистить». +- **Исходящая синхронизация**: при смене внутреннего статуса; при снятии статуса — очистка поля. +- **Входящая синхронизация**: при обновлении задач — выставление внутреннего статуса по label доп. поля. + +### Таблица заявок, фильтры, UI + +- Поиск в выпадающем фильтре **«Объекты»** (поле при открытии панели, автофокус). +- Кнопка переключения **тёмной темы** скрыта +- Выпадающие списки **внутреннего статуса** и **служебного статуса в интре**: при нехватке места снизу список открывается **вверх**; + +### Страница «Статистика» + +- **Открытых заявок**: боковая панель «сейчас / неделю или день назад / начало месяца» с бейджами отклонения; панель **справа от графика**, по высоте выровнена с графиком. +- **Открытых заявок**: Переключатель **«По неделям» / «По дням»** +- **Тепловая карта**: «час × день недели» пофиксил раскрашивание +- **Тепловая карта**: добавлена «активность по месяцам» +- **Создано / Закрыто по неделям**: при наведении — подсветка столбиков и **метка** с числами создано/закрыто над колонкой. +- **Клиенты**: клик по строке — переход на заявки с фильтром **«Объекты»** по этому клиенту +- **Повторные клиенты**: пофиксил окрашивание строк + +### Локальный запуск (для отладки) + +- `**docker-compose.local.yml`** — сборка из исходников, данные в `./data`. +- `**start-local.bat**` — запуск локального Docker под Windows. + diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..611a241 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,22 @@ +services: + intradesk-monitor: + build: + context: . + dockerfile: Dockerfile + container_name: intradesk-monitor-local + restart: unless-stopped + env_file: .env + ports: + - "${APP_PORT:-3000}:3000" + volumes: + - ./data:/data + environment: + NODE_ENV: production + PORT: 3000 + DB_PATH: /data/intradesk.db + healthcheck: + test: ["CMD", "node", "-e", "const http=require('http');const req=http.get('http://127.0.0.1:3000/healthz',res=>process.exit(res.statusCode===200?0:1));req.on('error',()=>process.exit(1));"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s diff --git a/intradesk-monitor.zip b/intradesk-monitor.zip deleted file mode 100644 index 50cfa9f..0000000 Binary files a/intradesk-monitor.zip and /dev/null differ diff --git a/public/css/app.css b/public/css/app.css index 9ba5f0b..0044808 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -843,6 +843,20 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-h position: fixed; /* fixed so it escapes any overflow:hidden parent */ z-index: 500; } +/* Невидимая зона между бейджем и списком — курсор не выходит из «цепочки» hover */ +.inline-status-picker .isp-dropdown::before { + content: ''; + position: absolute; + left: 0; + right: 0; + top: -10px; + height: 10px; +} +.inline-status-picker .isp-dropdown.isp-drop-up::before { + top: auto; + bottom: -10px; + height: 10px; +} .inline-status-picker:hover .isp-dropdown, .inline-status-picker.isp-open .isp-dropdown { display: block; @@ -974,6 +988,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; } @@ -993,7 +1045,7 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-h background: color-mix(in srgb, var(--accent) calc(var(--hi) * 100%), var(--surface2)); color: transparent; /* hide number by default, show on hover via title */ } -.stats-hm-cell[style*="--hi:0"] { background: var(--surface2); } +.stats-hm-cell[style*="--hi:0.000"] { background: var(--surface2); } .stats-hm-cell:hover { transform: scale(1.35); z-index: 5; position: relative; } /* show number only when there's activity */ .stats-hm-cell span { pointer-events: none; color: rgba(255,255,255,.0); } @@ -1036,6 +1088,99 @@ html.dark .stats-hm-cell:hover span { color: rgba(255,255,255,.95); } box-shadow: 0 3px 10px rgba(0,0,0,.2); } +/* ── Open-tickets week/month compare ────────────────────────── */ +.stats-open-wrap { display:flex; gap:16px; align-items: stretch; } +.stats-open-chart { flex:1; min-width:0; } +.stats-open-compare { + display: flex; + flex-direction: column; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + flex-shrink: 0; + min-width: 100px; + overflow: hidden; +} +.soc-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + padding: 6px 14px; +} +.soc-divider { height:1px; background:var(--border); flex-shrink:0; } +.soc-val { + font-size: 18px; + font-weight: 800; + color: var(--text); + line-height: 1; +} +.soc-label { + font-size: 9px; + color: var(--text3); + text-transform: uppercase; + letter-spacing: .04em; + white-space: nowrap; +} +.soc-sep { display: none; } +.stats-wdiff { + font-size: 11px; + font-weight: 700; + padding: 1px 6px; + border-radius: 8px; + margin-top: 3px; + white-space: nowrap; +} +.stats-wdiff-up { background: color-mix(in srgb,var(--red) 15%,transparent); color: var(--red); } +.stats-wdiff-down { background: color-mix(in srgb,var(--green) 15%,transparent); color: var(--green); } +.stats-wdiff-flat { background: var(--surface2); color: var(--text3); } + +/* ── Activity hover label ────────────────────────────────────── */ +.stats-act-hover-label { + position: absolute; + top: -20px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 2px; + font-size: 9px; + font-weight: 700; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity .15s; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 4px; + padding: 2px 5px; + z-index: 10; +} +.stats-activity-col:hover .stats-act-hover-label { opacity: 1; } +.stats-activity-col:hover .stats-activity-bar { filter: brightness(1.2) saturate(1.15); } +.stats-activity-bar { transition: filter .15s, height .2s; } +.sahl-cr { color: var(--accent); } +.sahl-sep { color: var(--text3); margin: 0 1px; } +.sahl-cl { color: var(--green); } + +/* ── Activity tooltip (floating, kept for other use) ─────────── */ +.stats-act-tooltip { + display: none; + position: fixed; + background: var(--text); + color: var(--surface); + font-size: 12px; + line-height: 1.5; + padding: 6px 12px; + border-radius: 7px; + pointer-events: none; + white-space: nowrap; + z-index: 9999; + box-shadow: 0 3px 12px rgba(0,0,0,.22); +} + /* ── Bar chart (generic) ────────────────────────────────────── */ .stats-bar-chart { display:flex; flex-direction:column; gap:9px; } .stats-bar-row { display:flex; align-items:center; gap:10px; } @@ -1211,6 +1356,8 @@ html.dark .stats-card { background: var(--surface); } .stats-clients-check-wrap input { cursor:pointer; accent-color:var(--accent); } .stats-clients-list { display:flex; flex-direction:column; gap:7px; } .stats-clients-row { display:flex; align-items:center; gap:10px; } +.stats-clients-row-clickable { cursor:pointer; border-radius:6px; transition:background .13s; padding:3px 4px; margin:-3px -4px; } +.stats-clients-row-clickable:hover { background:var(--surface2); } .stats-clients-name { font-size:12px; color:var(--text2); width:200px; flex-shrink:0; @@ -1430,6 +1577,25 @@ html.dark .stats-card { background:var(--surface); } color:var(--text3); font-size:12px; } +.adv-filter-search-wrap{ + padding:4px 2px 6px; + margin-bottom:4px; + border-bottom:1px solid var(--border); +} +.adv-filter-search{ + width:100%; + box-sizing:border-box; + border:1px solid var(--border); + border-radius:6px; + background:var(--surface2); + color:var(--text); + font-family:inherit; + font-size:12px; + padding:5px 9px; + outline:none; + transition:border-color .15s; +} +.adv-filter-search:focus{border-color:var(--accent);} /* ── Chip Dropdown ────────────────────────────────────── */ .chip-dropdown-wrap { position: relative; flex-shrink: 0; } diff --git a/public/index.html b/public/index.html index 0765aba..8a69f34 100644 --- a/public/index.html +++ b/public/index.html @@ -76,6 +76,12 @@ Sync Done with Intra by status_id +