docs: уточнения PRD/TRD по итогам обсуждения
- замещающий сотрудник при отпуске - edge cases (нет дублёра, все в отпуске) - общая история задач + комментарии + копирование - интеграция с системой «Симбейс» (приказы об отпусках) - двуязычный интерфейс RU/EN - пересмотр границ MVP / scope v0.1-v0.5 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7f3e01d7c4
commit
726497fe99
127
PRD.md
127
PRD.md
@ -0,0 +1,127 @@
|
|||||||
|
# PRD: Помощник руководителя по распределению задач
|
||||||
|
|
||||||
|
## 1. Проблема
|
||||||
|
|
||||||
|
Руководитель получает задачи от бизнеса (через чат, почту, устно) и вручную решает, кому их передать. Это занимает время, требует помнить зоны ответственности каждого сотрудника и приводит к ошибкам (задача уходит не туда → задержки, переделки).
|
||||||
|
|
||||||
|
## 2. Цель
|
||||||
|
|
||||||
|
Сервис-помощник, который принимает на вход формулировку задачи от бизнеса и автоматически предлагает ответственного сотрудника (или пару дублёров) на основе зон ответственности.
|
||||||
|
|
||||||
|
## 3. Целевой пользователь
|
||||||
|
|
||||||
|
Руководитель команды/отдела (1 человек на старте). Возможное расширение — тимлиды смежных команд.
|
||||||
|
|
||||||
|
## 4. Сценарий использования (MVP)
|
||||||
|
|
||||||
|
1. Руководитель открывает веб-страницу помощника.
|
||||||
|
2. Вставляет текст задачи от бизнеса (1-2 абзаца).
|
||||||
|
3. Нажимает «Определить ответственного».
|
||||||
|
4. Получает:
|
||||||
|
- основного ответственного (ФИО) + дублёра;
|
||||||
|
- категорию задачи (мобилка / CRM / SMS / …);
|
||||||
|
- короткое обоснование («в задаче упомянуто push-уведомление → мобильное приложение → Алина»);
|
||||||
|
- кнопку «Скопировать сообщение для сотрудника» (готовый текст для пересылки).
|
||||||
|
|
||||||
|
## 5. Справочник зон ответственности
|
||||||
|
|
||||||
|
**Источник данных — руководитель вводит сам.** Помощник предоставляет UI-форму, в которой руководитель добавляет/редактирует/удаляет сотрудников и их зоны ответственности.
|
||||||
|
|
||||||
|
Поля по каждому сотруднику:
|
||||||
|
- ФИО;
|
||||||
|
- основная зона ответственности (одна из существующих или новая);
|
||||||
|
- роль в зоне: основной / дублёр;
|
||||||
|
- статус: активен / в отпуске / недоступен (флажок, ставится руководителем вручную ИЛИ автоматически из системы «Симбейс» — см. §7);
|
||||||
|
- **взаимозаменяющий сотрудник** (`delegate`) — заполняется при статусе «в отпуске». Указывает руководитель. Может быть кем угодно из справочника, не обязательно дублёр из той же зоны;
|
||||||
|
- (опц.) контакт для пересылки сообщения (Telegram / e-mail).
|
||||||
|
|
||||||
|
Хранение: JSON-файл в репо или `localStorage` на MVP. Экспорт/импорт справочника одной кнопкой (JSON) — чтобы не потерять при переустановке.
|
||||||
|
|
||||||
|
Стартовый список зон (заполняется руководителем):
|
||||||
|
|
||||||
|
| Направление | Основной | Дублёр |
|
||||||
|
|---|---|---|
|
||||||
|
| Мобильное приложение | Алина | Адлет |
|
||||||
|
| CRM-система | Динара | Камила |
|
||||||
|
| SMS-сообщения / рассылки | Михаил | Мухамед |
|
||||||
|
| *(добавляется по мере роста)* | | |
|
||||||
|
|
||||||
|
Каждое направление содержит:
|
||||||
|
- список ключевых слов / синонимов («push», «iOS», «Android», «приложение» → мобилка);
|
||||||
|
- список систем («Bitrix», «amoCRM» → CRM);
|
||||||
|
- примеры задач (для few-shot подсказки LLM).
|
||||||
|
|
||||||
|
## 6. Логика подбора
|
||||||
|
|
||||||
|
**Вариант A (MVP, без LLM):** поиск по ключевым словам из справочника. Категории ранжируются по числу совпадений / весу ключевых слов.
|
||||||
|
|
||||||
|
**Вариант B (next):** LLM-классификатор (Claude). На вход — текст задачи + справочник, на выход — список категорий с оценкой уверенности + обоснование. Лучше работает с размытыми формулировками («сделайте так, чтобы клиенты получали уведомления о платежах»).
|
||||||
|
|
||||||
|
**Обработка нескольких категорий.** Если задача попадает более чем в одно направление (или уверенность ниже порога — **< 70%, зафиксировано**), помощник **не выбирает за руководителя**, а показывает все подходящие варианты с обоснованием и весами. Руководитель кликом выбирает, куда направить задачу. Выбор фиксируется как обучающий пример для будущих улучшений классификатора.
|
||||||
|
|
||||||
|
**Edge cases:**
|
||||||
|
- **В зоне один сотрудник без дублёра** → показать его + предупреждение «нет резервного исполнителя».
|
||||||
|
- **Основной в отпуске** → задача уходит взаимозаменяющему сотруднику, указанному руководителем (см. §5). Если не указан — дублёру по зоне.
|
||||||
|
- **Все сотрудники зоны в отпуске и нет замещающих** → результат «нет доступного ответственного, требуется решение руководителя». Помощник предлагает руководителю либо взять на себя, либо назначить из соседней зоны вручную.
|
||||||
|
|
||||||
|
Старт с A, переход на B при появлении ошибок классификации.
|
||||||
|
|
||||||
|
## 7. Функциональные требования
|
||||||
|
|
||||||
|
- [ ] Веб-форма ввода задачи (textarea + кнопка).
|
||||||
|
- [ ] Вывод результата: ответственный, дублёр, категория, обоснование.
|
||||||
|
- [ ] **Режим выбора при нескольких категориях:** карточки всех подходящих направлений с обоснованием и весом → клик руководителя выбирает финального ответственного.
|
||||||
|
- [ ] CRUD-форма для справочника сотрудников (добавить / редактировать / удалить / отметить «в отпуске»). **Доступ на изменение — только у руководителя** (вход по пин-коду / паролю). Остальные пользователи видят справочник только в режиме чтения (если им вообще откроют ссылку).
|
||||||
|
- [ ] Сценарий смены сотрудника: руководитель открывает раздел «Сотрудники» → находит запись → меняет ФИО / зону / статус → сохраняет. Изменения применяются мгновенно к следующим задачам. Прошлые решения в истории не переписываются (фиксируют, кто был назначен на момент задачи).
|
||||||
|
- [ ] Журнал изменений справочника: кто / когда / что поменял (для аудита, на MVP — простой список в JSON).
|
||||||
|
- [ ] Экспорт / импорт справочника в JSON (бэкап).
|
||||||
|
- [ ] Кнопка «Скопировать сообщение» (формирует текст: «@Алина, прилетела задача от бизнеса: <текст>. Ты основная по мобилке, посмотри плиз»).
|
||||||
|
- [ ] Редактируемый справочник зон ответственности (JSON/YAML в репо или простая форма в UI).
|
||||||
|
- [ ] **История задач** (последние 100 записей):
|
||||||
|
- читаема и руководителю, и взаимозаменяющему сотруднику (общий доступ по ссылке, не в `localStorage` одного браузера);
|
||||||
|
- для каждой задачи: исходный текст, выбранная зона, назначенный исполнитель, дата, комментарии руководителя/исполнителя;
|
||||||
|
- **кнопка «Скопировать историю задачи»** — выгружает текст всей задачи + комментариев в буфер обмена для пересылки.
|
||||||
|
- [ ] Возможность вручную переопределить ответственного (если помощник ошибся) — фиксируется как «обучающий пример».
|
||||||
|
- [ ] **Указание взаимозаменяющего сотрудника** при отметке «в отпуске». Без замещающего сотрудник в отпуске не может быть сохранён, если он единственный «основной» в зоне.
|
||||||
|
- [ ] **Интеграция с системой «Симбейс»** (приказы об отпусках сотрудников):
|
||||||
|
- помощник периодически (раз в сутки) опрашивает Симбейс по списку сотрудников из справочника;
|
||||||
|
- если найден активный приказ на отпуск — статус автоматически переключается в «в отпуске» с датами;
|
||||||
|
- руководителю приходит уведомление и просьба указать замещающего, если он ещё не указан;
|
||||||
|
- при ручном расхождении со статусом из Симбейс — приоритет у Симбейс (источник истины).
|
||||||
|
- [ ] **Двуязычный интерфейс RU / EN.** Переключатель языка в шапке. Все надписи в UI и шаблоны сообщений для пересылки — на двух языках.
|
||||||
|
|
||||||
|
## 8. Нефункциональные требования
|
||||||
|
|
||||||
|
- Хостинг: статика на Gitea Pages (вариант A — чистый JS).
|
||||||
|
- Без внешних БД на MVP.
|
||||||
|
- Время ответа: < 1 сек (вариант A), < 5 сек (вариант B с LLM).
|
||||||
|
- Доступ к классификатору задач — по ссылке (без авторизации на MVP).
|
||||||
|
- **Доступ к разделу «Сотрудники» (CRUD) — защищён пин-кодом / паролем, известным только руководителю.** Без него — режим только чтения.
|
||||||
|
|
||||||
|
## 9. Метрики успеха
|
||||||
|
|
||||||
|
- Доля задач, по которым руководитель **согласился** с предложенным ответственным без правок — целевое значение ≥ 70% после первых 30 задач.
|
||||||
|
- Сокращение времени на распределение одной задачи: с ~2-3 мин (вручную) до < 30 сек.
|
||||||
|
|
||||||
|
## 10. Вне scope MVP
|
||||||
|
|
||||||
|
- Интеграция с почтой / Telegram / Slack как **канал входа задачи** (на MVP — только веб-форма; пересылка ответственному — копированием текста).
|
||||||
|
- Постановка задачи в трекер (Jira/Bitrix).
|
||||||
|
- Многопользовательский режим с ролями (на MVP — только разделение «руководитель / все остальные»).
|
||||||
|
- Учёт больничных и точной загрузки (часы). Только бинарный статус «в отпуске / активен».
|
||||||
|
|
||||||
|
## 11. План реализации
|
||||||
|
|
||||||
|
1. **v0.1** — статическая страница + JSON-справочник + поиск по ключевым словам + i18n RU/EN + кнопка «Скопировать сообщение». Деплой на Gitea Pages.
|
||||||
|
2. **v0.2** — CRUD справочника, пин-код, журнал изменений, поле «замещающий», история задач (общая, не в `localStorage`), копирование истории, edge cases (зона без дублёра / все в отпуске).
|
||||||
|
3. **v0.3** — LLM-классификатор (вариант B) для размытых формулировок.
|
||||||
|
4. **v0.4** — интеграция с системой «Симбейс» (приказы на отпуска), уведомления руководителю о новых приказах.
|
||||||
|
5. **v0.5** — внешние каналы входа задачи (Telegram / e-mail).
|
||||||
|
|
||||||
|
## 12. Открытые вопросы
|
||||||
|
|
||||||
|
- **Симбейс**: какой формат API (REST / SOAP / выгрузка)? Какие поля приказа доступны (ФИО, табельный №, даты, тип отпуска)? Как сопоставлять сотрудников из справочника помощника с записями в Симбейс — по ФИО, табельному номеру, e-mail?
|
||||||
|
- **Общая история задач** требует серверного хранения (не `localStorage`). Где хранить — простой JSON в репо `gitea-pages` (push при каждом изменении) или отдельный лёгкий бэкенд? Решение влияет на архитектуру v0.2.
|
||||||
|
- **Контакты сотрудников** (Telegram / e-mail) лежат в публичном `directory.json` на Gitea Pages — допустимо ли это политикой безопасности? Если нет — выносим контакты в приватный слой (бэкенд) уже в v0.2.
|
||||||
|
- Нужна ли модерация LLM-ответа человеком на 100% задач или только при низкой уверенности?
|
||||||
|
- Уведомления руководителю о новом приказе из Симбейс — куда слать (e-mail / Telegram-бот)?
|
||||||
586
TRD.md
586
TRD.md
@ -0,0 +1,586 @@
|
|||||||
|
# TRD: Помощник руководителя по распределению задач
|
||||||
|
|
||||||
|
Технический документ к [PRD.md](PRD.md). Описывает архитектуру, структуры данных, алгоритмы и план реализации MVP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Архитектура
|
||||||
|
|
||||||
|
### 1.1. Высокоуровневая схема
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────────────────┐
|
||||||
|
│ Браузер (Gitea Pages) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ Задача (UI) │ │ Сотрудники(UI) │ │ История (UI) │ │
|
||||||
|
│ └──────┬───────┘ └────────┬───────┘ └────────┬─────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ app.js — классификация, CRUD, история │ │
|
||||||
|
│ └────┬─────────────────────┬─────────────────────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ directory. │ │ localStorage │ │
|
||||||
|
│ │ json (репо) │ │ (history, │ │
|
||||||
|
│ │ │ │ overrides) │ │
|
||||||
|
│ └─────────────┘ └─────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2. Стек
|
||||||
|
|
||||||
|
- **Хостинг:** Gitea Pages (ветка `gitea-pages`, топик `gitea-pages-allowall`, см. [GITEA.md](GITEA.md)).
|
||||||
|
- **Фронтенд:** чистый HTML/CSS/JS без сборки (Vanilla). Никакого Node, никакого билда — деплой = `git push origin gitea-pages`.
|
||||||
|
- **Хранилище:**
|
||||||
|
- Справочник зон + сотрудников — `directory.json` в репозитории (ветка `gitea-pages`). Источник истины.
|
||||||
|
- История задач, переопределения, журнал изменений — `localStorage` в браузере руководителя.
|
||||||
|
- Экспорт/импорт — скачать/загрузить `directory.json` через `<a download>` и `<input type="file">`.
|
||||||
|
- **LLM (v0.3):** API Claude (`claude-haiku-4-5-20251001` для классификатора — быстрый и дешёвый). Вызов из браузера через прокси (см. §6) или временно через CORS-разрешённый прямой вызов.
|
||||||
|
|
||||||
|
### 1.3. Почему не докер / не бэкенд
|
||||||
|
|
||||||
|
PRD §8: MVP на статике. Gitea Pages бесплатен, автодеплой. Бэкенд понадобится только для:
|
||||||
|
- защиты API-ключа Claude (v0.3+);
|
||||||
|
- многопользовательского доступа (вне scope MVP);
|
||||||
|
- настоящей авторизации (на MVP пин-код в браузере — компромисс).
|
||||||
|
|
||||||
|
Когда понадобится — поднимаем сервис через `docker-compose.yml` + Traefik по правилам [GITEA.md](GITEA.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Структуры данных
|
||||||
|
|
||||||
|
### 2.1. `directory.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"updatedAt": "2026-05-22T10:00:00Z",
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"id": "mobile",
|
||||||
|
"title": "Мобильное приложение",
|
||||||
|
"keywords": ["push", "ios", "android", "приложение", "мобилка", "apk", "ipa"],
|
||||||
|
"systems": ["AppStore", "Google Play", "Firebase"],
|
||||||
|
"examples": [
|
||||||
|
"Сделать push-уведомление по платежам",
|
||||||
|
"Поменять иконку приложения в iOS"
|
||||||
|
],
|
||||||
|
"weight": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crm",
|
||||||
|
"title": "CRM-система",
|
||||||
|
"keywords": ["crm", "црм", "клиент", "сделка", "карточка", "amocrm", "bitrix", "битрикс"],
|
||||||
|
"systems": ["Bitrix24", "amoCRM"],
|
||||||
|
"examples": ["Добавить поле «промо-код» в карточку клиента"],
|
||||||
|
"weight": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sms",
|
||||||
|
"title": "SMS-сообщения / рассылки",
|
||||||
|
"keywords": ["sms", "смс", "рассылка", "уведомление по смс", "сообщение"],
|
||||||
|
"systems": ["SMSC", "Twilio"],
|
||||||
|
"examples": ["Запустить SMS-рассылку по базе клиентов"],
|
||||||
|
"weight": 1.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"people": [
|
||||||
|
{ "id": "p1", "name": "Алина", "nameEn": "Alina", "zoneId": "mobile", "role": "primary", "status": "active", "delegateId": null, "vacationUntil": null, "simbaseId": "EMP-001", "contact": "@alina_tg" },
|
||||||
|
{ "id": "p2", "name": "Адлет", "nameEn": "Adlet", "zoneId": "mobile", "role": "backup", "status": "active", "delegateId": null, "vacationUntil": null, "simbaseId": "EMP-002", "contact": "@adlet_tg" },
|
||||||
|
{ "id": "p3", "name": "Динара", "nameEn": "Dinara", "zoneId": "crm", "role": "primary", "status": "vacation", "delegateId": "p4", "vacationUntil": "2026-06-15", "simbaseId": "EMP-003", "contact": "dinara@x.kz" },
|
||||||
|
{ "id": "p4", "name": "Камила", "nameEn": "Kamila", "zoneId": "crm", "role": "backup", "status": "active", "delegateId": null, "vacationUntil": null, "simbaseId": "EMP-004", "contact": "kamila@x.kz" },
|
||||||
|
{ "id": "p5", "name": "Михаил", "nameEn": "Mikhail","zoneId": "sms", "role": "primary", "status": "active", "delegateId": null, "vacationUntil": null, "simbaseId": "EMP-005", "contact": "@mikhail_tg" },
|
||||||
|
{ "id": "p6", "name": "Мухамед","nameEn": "Mukhamed","zoneId":"sms", "role": "backup", "status": "active", "delegateId": null, "vacationUntil": null, "simbaseId": "EMP-006", "contact": "@muhamed_tg" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Поля:
|
||||||
|
- `delegateId` — id замещающего сотрудника. Обязателен при `status == "vacation"`, если человек — единственный `primary` в зоне без активного дублёра.
|
||||||
|
- `vacationUntil` — дата окончания отпуска (ISO). Заполняется автоматически из Симбейс (см. §6.2) или вручную.
|
||||||
|
- `simbaseId` — внешний идентификатор сотрудника в системе «Симбейс». Используется для сопоставления при синхронизации приказов.
|
||||||
|
- `nameEn` — ФИО латиницей для англоязычного UI (i18n).
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. localStorage (только локальные настройки руководителя)
|
||||||
|
|
||||||
|
| Ключ | Содержимое |
|
||||||
|
|---|---|
|
||||||
|
| `app.pinHash` | SHA-256 пин-кода руководителя (см. §5) |
|
||||||
|
| `app.lang` | `"ru"` или `"en"` — выбранный язык интерфейса |
|
||||||
|
| `app.directoryOverride` | локальный патч справочника, если правки в UI ещё не выгружены в репо (см. §4) |
|
||||||
|
|
||||||
|
### 2.3. Общая история задач — `history.json` в репо
|
||||||
|
|
||||||
|
Чтобы историю видел **и руководитель, и взаимозаменяющий сотрудник** (PRD §7), история не лежит в `localStorage`. Хранится файлом `history.json` в ветке `gitea-pages` рядом с `directory.json`. Изменения коммитятся через Gitea API (см. §7.4).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type HistoryEntry = {
|
||||||
|
id: string; // uuid
|
||||||
|
createdAt: string; // ISO
|
||||||
|
taskText: string;
|
||||||
|
suggestions: Array<{ zoneId: string; score: number; reason: string }>;
|
||||||
|
chosenZoneId: string | null;
|
||||||
|
chosenPersonId: string | null;
|
||||||
|
delegateUsed: boolean; // true, если задача ушла замещающему вместо основного
|
||||||
|
manualOverride: boolean;
|
||||||
|
comments: Array<{
|
||||||
|
at: string;
|
||||||
|
authorRole: "manager" | "assignee" | "delegate";
|
||||||
|
authorName: string;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
status: "open" | "in_progress" | "done" | "cancelled";
|
||||||
|
};
|
||||||
|
|
||||||
|
type AuditEntry = { // в audit.json в репо
|
||||||
|
at: string;
|
||||||
|
action: "create" | "update" | "delete";
|
||||||
|
entity: "zone" | "person";
|
||||||
|
entityId: string;
|
||||||
|
before: object | null;
|
||||||
|
after: object | null;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Файлы в репо: `directory.json`, `history.json`, `audit.json` (все в корне ветки `gitea-pages`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Алгоритм классификации (вариант A, MVP)
|
||||||
|
|
||||||
|
### 3.1. Нормализация текста
|
||||||
|
|
||||||
|
```js
|
||||||
|
function normalize(s) {
|
||||||
|
return s.toLowerCase()
|
||||||
|
.replace(/ё/g, "е")
|
||||||
|
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
||||||
|
.split(/\s+/).filter(Boolean);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2. Скоринг зоны
|
||||||
|
|
||||||
|
Для каждой зоны:
|
||||||
|
```
|
||||||
|
score(zone) = Σ ( hits(keyword) * weight(keyword) )
|
||||||
|
+ Σ ( hits(system) * 1.5 )
|
||||||
|
where hits(token) = число вхождений токена в нормализованный текст задачи
|
||||||
|
```
|
||||||
|
|
||||||
|
Базовый `weight(keyword) = 1.0`. Имена систем имеют повышенный вес (1.5), т.к. это сильный сигнал (упомянули «amoCRM» — почти точно CRM).
|
||||||
|
|
||||||
|
### 3.3. Решение
|
||||||
|
|
||||||
|
```
|
||||||
|
sorted = zones.sort(desc by score).filter(score > 0)
|
||||||
|
if len(sorted) == 0: → «не удалось определить», показать все зоны
|
||||||
|
elif len(sorted) == 1: → показать одну зону
|
||||||
|
elif sorted[0].score >= 2 * sorted[1]: → показать топ-1 как основной, остальные «также возможно»
|
||||||
|
else: → режим выбора (PRD §6): показать топ-N (N ≤ 3) карточками
|
||||||
|
```
|
||||||
|
|
||||||
|
Порог `2x` подобран эмпирически; вынесен в конфиг (`config.dominanceFactor = 2.0`).
|
||||||
|
|
||||||
|
### 3.4. Назначение конкретного человека (с учётом отпусков и замещений)
|
||||||
|
|
||||||
|
```
|
||||||
|
function assignPerson(zoneId):
|
||||||
|
candidates = people.filter(p => p.zoneId == zoneId)
|
||||||
|
primary = candidates.find(p => p.role == "primary")
|
||||||
|
backup = candidates.find(p => p.role == "backup")
|
||||||
|
|
||||||
|
// Шаг 1: основной активен → берём его
|
||||||
|
if primary && primary.status == "active":
|
||||||
|
return { main: primary, fallback: backup if backup && backup.status == "active" else null }
|
||||||
|
|
||||||
|
// Шаг 2: основной в отпуске + указан замещающий → берём замещающего
|
||||||
|
if primary && primary.status == "vacation" && primary.delegateId:
|
||||||
|
delegate = people.find(p => p.id == primary.delegateId)
|
||||||
|
if delegate && delegate.status == "active":
|
||||||
|
return { main: delegate, fallback: null, note: "замещает " + primary.name }
|
||||||
|
|
||||||
|
// Шаг 3: основной недоступен / без замещающего → дублёр зоны
|
||||||
|
if backup && backup.status == "active":
|
||||||
|
return { main: backup, fallback: null, note: "основной недоступен" }
|
||||||
|
|
||||||
|
// Шаг 4: все в зоне неактивны
|
||||||
|
return { main: null, fallback: null, error: "NO_ASSIGNEE", message: "Все сотрудники зоны недоступны. Решение требуется от руководителя." }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5. Edge case: все в зоне в отпуске (PRD §6)
|
||||||
|
|
||||||
|
Если `assignPerson()` вернул `error == "NO_ASSIGNEE"`, UI выводит карточку:
|
||||||
|
- заголовок «Нет доступного ответственного»;
|
||||||
|
- список сотрудников зоны со статусами и датами возвращения из отпуска (`vacationUntil`);
|
||||||
|
- две кнопки:
|
||||||
|
- «Назначить вручную» → модалка со всем справочником, руководитель выбирает любого;
|
||||||
|
- «Принять на себя» → исполнитель = руководитель.
|
||||||
|
|
||||||
|
В историю запись пишется с `manualOverride: true` и комментарием о причине.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. UI
|
||||||
|
|
||||||
|
### 4.1. Страницы (одностраничник с табами)
|
||||||
|
|
||||||
|
1. **«Задача»** (по умолчанию):
|
||||||
|
- `<textarea>` — ввод задачи.
|
||||||
|
- Кнопка «Определить ответственного».
|
||||||
|
- Блок результата: карточка(и) зон → выбор → карточка человека → кнопка «Скопировать сообщение».
|
||||||
|
|
||||||
|
2. **«Сотрудники»** (защищено пин-кодом):
|
||||||
|
- Таблица сотрудников: ФИО, зона, роль, статус, контакт, действия (✎ / 🗑).
|
||||||
|
- Кнопка «+ Добавить сотрудника».
|
||||||
|
- Раздел «Зоны»: список зон, редактирование keywords/systems/examples.
|
||||||
|
- Кнопки «Экспорт JSON» / «Импорт JSON».
|
||||||
|
- Подраздел «Журнал изменений» — табличный вывод `app.audit`.
|
||||||
|
|
||||||
|
3. **«История»** (доступна и руководителю, и замещающему сотруднику):
|
||||||
|
- Список последних 100 задач: дата, текст, зона, исполнитель, статус (open / in_progress / done / cancelled).
|
||||||
|
- Клик по записи → раскрытие: полный текст задачи + хронологическая лента комментариев (руководитель / исполнитель / замещающий).
|
||||||
|
- Поле ввода нового комментария (для исполнителя и замещающего — без пин-кода, только подпись именем из URL-параметра `?as=<personId>` или выбор из дропдауна).
|
||||||
|
- **Кнопка «Скопировать историю задачи»** — выгружает в буфер обмена форматированный текст: задача + все комментарии + статус. Удобно переслать в чат.
|
||||||
|
- Фильтры: по зоне, по исполнителю, по статусу.
|
||||||
|
|
||||||
|
### 4.4. Локализация (RU / EN)
|
||||||
|
|
||||||
|
- Переключатель `RU | EN` в шапке.
|
||||||
|
- Все строки UI вынесены в `i18n.json`:
|
||||||
|
```json
|
||||||
|
{ "ru": { "btn.classify": "Определить ответственного", ... },
|
||||||
|
"en": { "btn.classify": "Find assignee", ... } }
|
||||||
|
```
|
||||||
|
- ФИО хранятся в `name` (RU) + `nameEn` (EN, опц.). Если `nameEn` пуст — отображается `name`.
|
||||||
|
- Шаблоны сообщений для пересылки — два варианта по языку.
|
||||||
|
- Хранение выбранного языка в `localStorage["app.lang"]`.
|
||||||
|
|
||||||
|
### 4.2. Компоненты (без фреймворка)
|
||||||
|
|
||||||
|
```
|
||||||
|
index.html
|
||||||
|
├─ <header> навигация (3 таба)
|
||||||
|
├─ <main id="view-task"> ← по умолчанию
|
||||||
|
├─ <main id="view-people" hidden>
|
||||||
|
└─ <main id="view-history" hidden>
|
||||||
|
```
|
||||||
|
|
||||||
|
`app.js` подписывается на клики по навигации, переключает `hidden` и рендерит контент из шаблонов (`<template>`-теги в `index.html`).
|
||||||
|
|
||||||
|
### 4.3. Сообщение для пересылки
|
||||||
|
|
||||||
|
Шаблон RU:
|
||||||
|
```
|
||||||
|
@{{name}}, прилетела задача от бизнеса:
|
||||||
|
|
||||||
|
«{{taskText}}»
|
||||||
|
|
||||||
|
Ты {{role_label}} по направлению «{{zone}}».{{delegate_note}} Посмотри, пожалуйста.
|
||||||
|
Ссылка на историю задачи: {{history_url}}
|
||||||
|
```
|
||||||
|
Шаблон EN:
|
||||||
|
```
|
||||||
|
@{{nameEn}}, a new business task:
|
||||||
|
|
||||||
|
"{{taskText}}"
|
||||||
|
|
||||||
|
You are {{role_label}} for "{{zone}}".{{delegate_note}} Please take a look.
|
||||||
|
Task history: {{history_url}}
|
||||||
|
```
|
||||||
|
`role_label`: «основной(ая)» / «дублёр» / «замещающий» (или EN: `primary` / `backup` / `delegate`).
|
||||||
|
`delegate_note` появляется, если задача ушла замещающему: « Ты замещаешь {{primaryName}}, который в отпуске до {{vacationUntil}}.»
|
||||||
|
`history_url` — прямая ссылка на запись в истории: `https://zhanakova.telecom.quest/zhanakova/#/history/{{taskId}}`.
|
||||||
|
|
||||||
|
Копирование через `navigator.clipboard.writeText`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Защита раздела «Сотрудники»
|
||||||
|
|
||||||
|
### 5.1. Модель угроз
|
||||||
|
|
||||||
|
Ссылка на сайт может быть публичной (Gitea Pages). Нельзя пускать рандомных людей в CRUD справочника. Нужна простая аутентификация без бэкенда.
|
||||||
|
|
||||||
|
### 5.2. Решение (MVP)
|
||||||
|
|
||||||
|
1. При первом открытии вкладки «Сотрудники» — модалка «Задайте пин-код руководителя» (4-6 символов). Хэшируется SHA-256 → `app.pinHash` в `localStorage`.
|
||||||
|
2. Дальнейший вход в раздел — ввод пин-кода. Сверка с хэшем.
|
||||||
|
3. Пин-код **не** улучшает security перед злоумышленником с доступом к `localStorage`/devtools. Это барьер от случайных правок коллегами, которые откроют ссылку. **Явная пометка в UI:** «Это не криптографическая защита».
|
||||||
|
4. Сам `directory.json` лежит в публичном репо Gitea — данные сотрудников видны всем, у кого есть ссылка. Если данные чувствительные — переходим на бэкенд (v0.4+, см. §6).
|
||||||
|
|
||||||
|
### 5.3. Реализация хэша
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function sha256(text) {
|
||||||
|
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
||||||
|
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. LLM-классификатор (вариант B, v0.3)
|
||||||
|
|
||||||
|
### 6.1. Архитектура
|
||||||
|
|
||||||
|
Прямой вызов Claude API из браузера **не** допускается — ключ утечёт. Поднимаем минимальный прокси:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Браузер] ──HTTPS──▶ [proxy.telecom.quest] ──HTTPS──▶ [api.anthropic.com]
|
||||||
|
(Docker + Traefik,
|
||||||
|
хранит ANTHROPIC_API_KEY)
|
||||||
|
```
|
||||||
|
|
||||||
|
`docker-compose.yml` в репо помощника (см. [GITEA.md](GITEA.md) §«Сервисы и динамика»):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
task-router-proxy:
|
||||||
|
image: node:22-alpine
|
||||||
|
working_dir: /app
|
||||||
|
volumes: [".:/app"]
|
||||||
|
command: node proxy.js
|
||||||
|
environment:
|
||||||
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||||
|
expose: ["8080"]
|
||||||
|
networks: [traefik]
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.taskrouter.rule=Host(`task-router.telecom.quest`)"
|
||||||
|
- "traefik.http.routers.taskrouter.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.taskrouter.tls=true"
|
||||||
|
- "traefik.http.services.taskrouter.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2. Промпт классификатора (черновик)
|
||||||
|
|
||||||
|
```
|
||||||
|
Ты — классификатор задач. Дан список направлений и текст задачи от бизнеса.
|
||||||
|
Верни JSON-массив топ-3 направлений, отсортированный по уверенности.
|
||||||
|
|
||||||
|
Направления:
|
||||||
|
{{zones_json}}
|
||||||
|
|
||||||
|
Задача:
|
||||||
|
"""
|
||||||
|
{{task_text}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
Формат ответа (строго JSON, без префиксов/постфиксов):
|
||||||
|
[{ "zoneId": "...", "confidence": 0.0-1.0, "reason": "одно предложение почему" }, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Модель: `claude-haiku-4-5-20251001`. Кэшируем системную часть промпта (список зон) — prompt caching.
|
||||||
|
|
||||||
|
### 6.3. Fallback
|
||||||
|
|
||||||
|
Если запрос к LLM упал (таймаут / 5xx) — откатываемся на алгоритм §3. Логируем неудачу в `audit.json`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6A. Интеграция с системой «Симбейс» (приказы об отпусках)
|
||||||
|
|
||||||
|
### 6A.1. Цель
|
||||||
|
|
||||||
|
Автоматически синхронизировать статус сотрудников (`active` / `vacation`) и `vacationUntil` со внутренней HR-системой Симбейс. Чтобы руководитель не отмечал отпуска вручную.
|
||||||
|
|
||||||
|
### 6A.2. Архитектура
|
||||||
|
|
||||||
|
Pull-модель: cron-задача на том же прокси (см. §6.1) опрашивает Симбейс раз в сутки (`0 6 * * *`):
|
||||||
|
|
||||||
|
```
|
||||||
|
[Cron на proxy.telecom.quest]
|
||||||
|
│
|
||||||
|
├── GET https://simbase.<internal>/api/vacations?from=<today>
|
||||||
|
│ Authorization: Bearer <SIMBASE_API_TOKEN>
|
||||||
|
│
|
||||||
|
├── Для каждого приказа находим сотрудника в directory.json по simbaseId
|
||||||
|
│
|
||||||
|
├── Обновляем status, vacationUntil
|
||||||
|
│
|
||||||
|
├── Если нет delegateId → ставим флаг "needs_delegate"
|
||||||
|
│
|
||||||
|
└── PUSH в Gitea (commit + push в gitea-pages)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6A.3. Контракт API (предположения, требуют подтверждения у владельцев Симбейс)
|
||||||
|
|
||||||
|
Запрос:
|
||||||
|
```http
|
||||||
|
GET /api/vacations?from=2026-05-22&to=2026-12-31
|
||||||
|
```
|
||||||
|
Ожидаемый ответ:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"employeeId": "EMP-003",
|
||||||
|
"fullName": "Динара Иванова",
|
||||||
|
"vacationStart": "2026-05-25",
|
||||||
|
"vacationEnd": "2026-06-15",
|
||||||
|
"orderNumber": "О-2026-451",
|
||||||
|
"orderDate": "2026-05-10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Сопоставление: `directory.people[i].simbaseId == response[j].employeeId`. Если совпадения нет — приказ игнорируется и логируется в `audit.json` как `unmatched_vacation_order`.
|
||||||
|
|
||||||
|
### 6A.4. Реакция на отсутствие замещающего
|
||||||
|
|
||||||
|
Если в результате синка сотрудник переведён в `vacation`, но у него `delegateId == null` и нет активного `backup` в зоне — отправляется уведомление руководителю:
|
||||||
|
- e-mail с темой «Требуется замещающий для {{name}}»;
|
||||||
|
- в UI помощника при следующем входе — баннер «1 сотрудник требует замещающего».
|
||||||
|
|
||||||
|
Канал уведомления — конфиг (`config.notificationChannel`: `email` / `telegram` / `none`).
|
||||||
|
|
||||||
|
### 6A.5. Приоритет источников
|
||||||
|
|
||||||
|
Симбейс — источник истины для отпусков. При расхождении между ручной отметкой и Симбейс — побеждает Симбейс. Ручная отметка возможна только если сотрудник в Симбейс не заведён или приказа нет.
|
||||||
|
|
||||||
|
### 6A.6. Защита токена
|
||||||
|
|
||||||
|
`SIMBASE_API_TOKEN` — только в env прокси. Никогда не уходит в браузер. Прокси `task-router` сам инициирует синк, без участия фронта.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Деплой
|
||||||
|
|
||||||
|
### 7.1. Структура репозитория
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├─ index.html # вход
|
||||||
|
├─ app.js # вся логика
|
||||||
|
├─ styles.css
|
||||||
|
├─ directory.json # справочник зон и сотрудников
|
||||||
|
├─ history.json # общая история задач (v0.2+)
|
||||||
|
├─ audit.json # журнал изменений (v0.2+)
|
||||||
|
├─ i18n.json # строки RU/EN
|
||||||
|
├─ README.md
|
||||||
|
├─ PRD.md
|
||||||
|
├─ TRD.md
|
||||||
|
└─ (v0.3+) proxy.js, docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4. Запись истории/справочника обратно в репо
|
||||||
|
|
||||||
|
История и правки CRUD сохраняются в Gitea через Contents API:
|
||||||
|
```
|
||||||
|
PUT https://git.telecom.quest/api/v1/repos/kt-innovators/zhanakova/contents/history.json
|
||||||
|
Authorization: token <GITEA_PAT>
|
||||||
|
Body: { "branch": "gitea-pages", "content": "<base64 нового JSON>", "sha": "<текущий sha файла>", "message": "..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
`GITEA_PAT` хранится в прокси (см. §6). Фронт делает POST на прокси, прокси делает PUT в Gitea. Так PAT не попадает в браузер.
|
||||||
|
|
||||||
|
Альтернатива на v0.1 (до прокси): руководитель вручную делает «Экспорт JSON» → коммит через VS Code в браузере. История пишется в `localStorage` временно, до перехода на v0.2.
|
||||||
|
|
||||||
|
### 7.2. Ветка `gitea-pages`
|
||||||
|
|
||||||
|
Согласно [GITEA.md](GITEA.md):
|
||||||
|
|
||||||
|
1. Создать ветку `gitea-pages` в репо `kt-innovators/zhanakova`.
|
||||||
|
2. Залить `index.html`, `app.js`, `styles.css`, `directory.json` в корень ветки.
|
||||||
|
3. Добавить топик `gitea-pages-allowall` к репозиторию.
|
||||||
|
4. Открыть `https://zhanakova.telecom.quest/zhanakova/`.
|
||||||
|
|
||||||
|
Автодеплой: каждый `git push origin gitea-pages` обновляет сайт.
|
||||||
|
|
||||||
|
### 7.3. Локальная разработка
|
||||||
|
|
||||||
|
Достаточно `python3 -m http.server 8000` в корне → `http://localhost:8000`. Без билда, без npm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. План реализации (тех. разбивка по версиям PRD §11)
|
||||||
|
|
||||||
|
### v0.1 — статика + поиск по словам + i18n
|
||||||
|
|
||||||
|
- [ ] `index.html` + минимальный CSS (3 таба) + переключатель RU/EN.
|
||||||
|
- [ ] `i18n.json` со всеми строками (RU + EN).
|
||||||
|
- [ ] `directory.json` со стартовыми зонами и сотрудниками (PRD §5 таблица).
|
||||||
|
- [ ] `app.js`: загрузка `directory.json`, normalize, скоринг (§3), рендер карточек.
|
||||||
|
- [ ] Кнопка «Скопировать сообщение» (два шаблона по языкам).
|
||||||
|
- [ ] Деплой на Gitea Pages.
|
||||||
|
|
||||||
|
### v0.2 — CRUD справочника + общая история + edge cases
|
||||||
|
|
||||||
|
- [ ] Таб «Сотрудники» с CRUD (зоны + люди).
|
||||||
|
- [ ] Поле `delegateId`, обязательное при `status == "vacation"` для единственного primary.
|
||||||
|
- [ ] Пин-код, SHA-256 в `localStorage`.
|
||||||
|
- [ ] `audit.json` (через прокси + Gitea API).
|
||||||
|
- [ ] Экспорт/импорт `directory.json` (download/upload).
|
||||||
|
- [ ] Таб «История» — `history.json` в репо, кнопка «Скопировать историю задачи».
|
||||||
|
- [ ] Комментарии в задачах (manager / assignee / delegate).
|
||||||
|
- [ ] Кнопка «переопределить ответственного» с фиксацией `manualOverride`.
|
||||||
|
- [ ] Edge case: все в зоне в отпуске → модалка «назначить вручную / принять на себя».
|
||||||
|
- [ ] Прокси для записи в Gitea (PAT в env).
|
||||||
|
|
||||||
|
### v0.3 — LLM-классификатор
|
||||||
|
|
||||||
|
- [ ] Прокси `proxy.js` (Node http server → Anthropic API), общий с §7.4.
|
||||||
|
- [ ] `docker-compose.yml` + Traefik labels.
|
||||||
|
- [ ] Фронт: переключатель «Использовать LLM» в настройках.
|
||||||
|
- [ ] Fallback на алгоритм §3 при ошибке.
|
||||||
|
- [ ] Промпт-кэширование зон.
|
||||||
|
|
||||||
|
### v0.4 — Симбейс (отпуска)
|
||||||
|
|
||||||
|
- [ ] Поле `simbaseId` у сотрудников (миграция в `directory.json`).
|
||||||
|
- [ ] Cron на прокси: ежедневный pull `/api/vacations` (§6A).
|
||||||
|
- [ ] Запись `status` / `vacationUntil` обратно в `directory.json`.
|
||||||
|
- [ ] Уведомление руководителю при отсутствии замещающего (e-mail / Telegram).
|
||||||
|
- [ ] Баннер в UI «N сотрудников требуют замещающего».
|
||||||
|
|
||||||
|
### v0.5 — внешние каналы входа задачи
|
||||||
|
|
||||||
|
- [ ] Telegram-бот: пересылка сообщения боту → POST на прокси → классификация.
|
||||||
|
- [ ] E-mail-приёмник (IMAP) → новая задача в `history.json`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Тестирование
|
||||||
|
|
||||||
|
Юнит-тесты не критичны на MVP (Vanilla JS, мало логики), но стоит сделать:
|
||||||
|
|
||||||
|
- [ ] `test/classifier.test.js` — функция `score()` на 10-15 примерах из PRD §5 (Node + `node:test`).
|
||||||
|
- [ ] Ручной чек-лист сценариев:
|
||||||
|
1. Задача с одной явной категорией («сделать SMS-рассылку») → одна карточка.
|
||||||
|
2. Задача с двумя категориями («push в мобилке и SMS дублирующее») → две карточки на выбор.
|
||||||
|
3. Размытая задача («уведомить клиентов») → все зоны с низкими весами.
|
||||||
|
4. Сотрудник «в отпуске» → дублёр промоутится.
|
||||||
|
5. Пин-код неверный → раздел «Сотрудники» не открывается.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Риски и митигации
|
||||||
|
|
||||||
|
| Риск | Митигация |
|
||||||
|
|---|---|
|
||||||
|
| `directory.json` в публичном репо — данные сотрудников видны | Не класть телефоны/email в публичную ветку (поле `contact` маскировать или хранить отдельно через прокси с v0.2). |
|
||||||
|
| Пин-код легко обойти через devtools | Прямо сказано в UI. Защита от случайных правок, не от злоумышленника. |
|
||||||
|
| LLM-ответ невалидный JSON | Валидация по схеме, fallback на алгоритм §3. |
|
||||||
|
| Утечка `ANTHROPIC_API_KEY` / `GITEA_PAT` / `SIMBASE_API_TOKEN` | Только в env прокси, никогда в браузер. Ротация ключей раз в квартал. |
|
||||||
|
| Руководитель потерял пин-код | Кнопка «Сбросить» очищает `app.pinHash` (требует ручное подтверждение). |
|
||||||
|
| Конфликт ручной отметки и Симбейс | Симбейс — источник истины (§6A.5). Ручная отметка перезаписывается при синке. |
|
||||||
|
| Симбейс недоступен | Cron логирует ошибку в `audit.json`, статусы не меняет. Алёрт руководителю при 3 подряд неудачных синках. |
|
||||||
|
| Сотрудник в Симбейс есть, в `directory.json` нет | Приказ игнорируется, запись `unmatched_vacation_order` в `audit.json` для разбора руководителем. |
|
||||||
|
| Все в зоне в отпуске | Edge case §3.5: ручное назначение / эскалация руководителю. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Открытые технические вопросы
|
||||||
|
|
||||||
|
- **Симбейс API** — endpoint, метод аутентификации, формат полей. Нужны от владельцев системы. До получения — контракт §6A.3 черновой.
|
||||||
|
- **Соответствие сотрудников Симбейс ↔ помощник** — по `simbaseId` (табельный номер) или по ФИО? Решено: `simbaseId` обязателен с v0.4, при отсутствии — fallback на нормализованное ФИО с предупреждением.
|
||||||
|
- **Контакты в публичном `directory.json`** — допустимо ли по политике безопасности компании. Если нет → выносим `contact` в приватный слой через прокси уже в v0.2.
|
||||||
|
- **Канал уведомлений** руководителю о новых приказах — e-mail или Telegram-бот? Решается в v0.4, влияет на конфиг прокси.
|
||||||
Reference in New Issue
Block a user