Система согласований
Telegram-бот
Техническая документация: Telegram-бот системы согласований
Оглавление
- Техническое задание
- Структура репозитория (package map)
- Полная модель данных
- Пакет
approvalflow— дерево согласования и реестр тем - Пакет
state— состояние диалога в Redis Handler— инициализация и главное меню- Прохождение дерева —
HandleCallback,HandleTextInput,HandleBackCallback - Цепочка дополнительных шагов —
continueFlowAfterInput - Выбор согласующего (ручной и по роли)
- Наблюдатели
- Делегирование обязанностей
submitApproval— создание заявки, построчно- Решение согласующего —
handleDecision processNextStepOrComplete- Дополнительные согласующие (
add_approver.go) - Задачи исполнителей (
tasks.go) - Отзыв заявок
- Уведомления (
Notifier) - Устойчивость бота (прокси, ретраи)
scheduler— ежедневная сводка- Каталог
callback_data - Каталог ключей Redis
- Журнал багов с кодом до/после
0. Техническое задание: дерево тем согласования
Источник: карта согласований, содержащая полный перечень категорий и тем согласования с указанием инициаторов, согласующих и наблюдателей на момент постановки задачи.
Обозначения: Инициаторы — кто может подавать заявку по данной ветке; Согласующий N — кто и в каком порядке принимает решение (порядковый номер соответствует шагу в последовательной цепочке согласования); Наблюдатели — кто получает уведомление о заявке без права решения.
Итоговое дерево включает 26 категорий верхнего уровня:
- Согласование отпусков\отгулов
- Внебюджетные расходы
- Внебюджетные расходы ПЭТ Лайн
- Возвраты
- Изменение клиентов в ОКБ и 1С
- Заказ товаров
- Отгрузки на СОХ
- Паспорт сделки
- Ставки логистика
- Поступления по приходным документам
- Доставка клиентам\на склад
- В. Отгрузки
- Менеджерские расходы
- Отгрузки
- Нестандартные условия договора поставки
- Вакансии
- Ставки доставки АП
- Утверждение фрилансеров
- Выход в нерабочий день
- Долгосрочные условия ОТК
- Остаток на кассах
- Продажи по клиенту
- Проведенные расходы по менеджеру
- Согласование удаленной работы
- Смена условий автоматического согласования сделок
- Утверждение оплат безнал
Полное дерево категорий и тем
- Согласование отпусков\отгулов
- Отгул
- На один день
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- Диапазон дат
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- Несколько дней
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- На один день
- Отпуск
- На один день
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- Диапазон дат
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- Несколько дней
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- На один день
- Больничный. Меняем на "официальный больничный". Следующим пунктом просим ввести дату открытия больничного листа.
- На один день
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Только оф. больничный
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- Диапазон дат
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Официальный больничный
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- Несколько дней
- Весь день
- В счёт отпуска
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- За свой счёт
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Официальный больничный
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- В счёт отпуска
- Частично
- Инициаторы:
- Все
- Согласующий 1:
- Непосредственный руководитель
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- Весь день
- На один день
- Отгул
- Внебюджетные расходы
- Ф1
- ТМЦ (далее "Нужна ПП?")
- Да
- Инициаторы:
- Турчина Варвара
- Горбачёва Оксана
- Демченко Елена
- Жуков Дмитрий
- Шведов Никита
- Романов Александр
- Свирень Максим
- Танашевич Анифе
- Щасливая Екатерина
- Шепко Лия
- Аблякимова Айше
- Халилов Дамур
- Анисимов Виталий
- Храпач Анастасия
- прикрепить файл
- Согласующий 1:
- Шепко Лия
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Исполнитель:
- Рудь Анфиса
- задача 1:
- Оплатить счет
- задача 2:
- Отправить ПП сотруднику
- прикрепить файл
- Инициаторы:
- Нет
- Инициаторы:
- Турчина Варвара
- Горбачёва Оксана
- Демченко Елена
- Жуков Дмитрий
- Шведов Никита
- Романов Александр
- Свирень Максим
- Танашевич Анифе
- Щасливая Екатерина
- Шепко Лия
- Аблякимова Айше
- Халилов Дамур
- Анисимов Виталий
- Храпач Анастасия
- прикрепить файл
- Согласующий 1:
- Шепко Лия
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Исполнитель:
- Рудь Анфиса
- задача 1:
- Оплатить счет
- Инициаторы:
- Да
- Расход (далее "Нужна ПП?")
- Да
- Инициаторы:
- Турчина Варвара
- Горбачёва Оксана
- Демченко Елена
- Жуков Дмитрий
- Шведов Никита
- Романов Александр
- Свирень Максим
- Танашевич Анифе
- Щасливая Екатерина
- Шепко Лия
- Аблякимова Айше
- Халилов Дамур
- Анисимов Виталий
- Храпач Анастасия
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Шепко Лия
- Исполнитель:
- Рудь Анфиса
- задача 1:
- Оплатить счет
- задача 2:
- Отправить ПП сотруднику
- прикрепить файл
- Инициаторы:
- Нет
- Инициаторы:
- Турчина Варвара
- Горбачёва Оксана
- Демченко Елена
- Жуков Дмитрий
- Шведов Никита
- Романов Александр
- Свирень Максим
- Танашевич Анифе
- Щасливая Екатерина
- Шепко Лия
- Аблякимова Айше
- Халилов Дамур
- Анисимов Виталий
- Храпач Анастасия
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Шепко Лия
- Исполнитель:
- Рудь Анфиса
- задача 1:
- Оплатить счет
- Инициаторы:
- Да
- ТМЦ (далее "Нужна ПП?")
- Ф2
- ТМЦ
- Инициаторы:
- Турчина Варвара
- Горбачёва Оксана
- Демченко Елена
- Жуков Дмитрий
- Шведов Никита
- Романов Александр
- Свирень Максим
- Танашевич Анифе
- Щасливая Екатерина
- Шепко Лия
- Аблякимова Айше
- Халилов Дамур
- Анисимов Виталий
- Храпач Анастасия
- Согласующий 1:
- Шепко Лия
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Инициаторы:
- Расход
- Инициаторы:
- Турчина Варвара
- Горбачёва Оксана
- Демченко Елена
- Жуков Дмитрий
- Шведов Никита
- Романов Александр
- Свирень Максим
- Танашевич Анифе
- Щасливая Екатерина
- Шепко Лия
- Аблякимова Айше
- Халилов Дамур
- Анисимов Виталий
- Храпач Анастасия
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Шепко Лия
- Инициаторы:
- ТМЦ
- Ф1
- Внебюджетные расходы ПЭТ Лайн
- Ф1
- ТМЦ
- Инициаторы:
- Пирожников Павел
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- Расход
- Инициаторы:
- Пирожников Павел
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- ТМЦ
- Ф2
- ТМЦ
- Инициаторы:
- Пирожников Павел
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- Расход
- Инициаторы:
- Пирожников Павел
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- ТМЦ
- Ф1
- Возвраты
- Возврат поставщику
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- Согласующий 1:
- Щасливая Екатерина
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Великожон Татьяна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- Краснодар
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- Согласующий 1:
- Щасливая Екатерина
- Согласующий 2:
- Макаренко Александр
- Согласующий 3:
- Тищенко Владимир
- Наблюдатели:
- Великожон Татьяна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- Крым
- Продажа другим менеджером
- Крым
- выбор из списка менеджеров Крыма:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Петрунин Кирилл
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- Согласующий 1:
- выбранный в прошлом шаге менеджер
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Великожон Татьяна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Демченко Елена
- Вялкова Анна
- выбор из списка менеджеров Крыма:
- Краснодар
- выбор из списка менеджеров Краснодара:
- Макаренко Александр
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Сыпков Станислав
- Федосенко Иван
- Пирожников Павел
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- Согласующий 1:
- выбранный в прошлом шаге менеджер
- Согласующий 2:
- Макаренко Александр
- Согласующий 3:
- Тищенко Владимир
- Наблюдатели:
- Великожон Татьяна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Демченко Елена
- Вялкова Анна
- выбор из списка менеджеров Краснодара:
- Крым
- Самостоятельная продажа
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Великожон Татьяна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- Краснодар
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Великожон Татьяна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- Крым
- Возврат поставщику
- Изменение клиентов в ОКБ и 1С
- Поменять название
- Крым
- ОКБ
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести изменение в название клиента
- Инициаторы:
- Битрикс
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести изменение в название клиента
- Инициаторы:
- 1С
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Вялкова Анна
- задача 1:
- Внести изменение в название клиента
- Инициаторы:
- Подтвердить выбор
- при выборе нескольких, ставить совокупность всех задач по выбранным кнопкам
- ОКБ
- Краснодар
- ОКБ
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести изменение в название клиента
- Инициаторы:
- Битрикс
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести изменение в название клиента
- Инициаторы:
- 1С
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Вялкова Анна
- задача 1:
- Внести изменение в название клиента
- Инициаторы:
- Подтвердить выбор
- при выборе нескольких, ставить совокупность всех задач по выбранным кнопкам
- ОКБ
- Крым
- Сменить менеджера
- Крым
- ОКБ
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Сменить менеджера клиента
- Инициаторы:
- Битрикс
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Сменить менеджера клиента
- Инициаторы:
- 1С
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Вялкова Анна
- задача 1:
- Сменить менеджера клиента
- Инициаторы:
- Подтвердить выбор
- при выборе нескольких, ставить совокупность всех задач по выбранным кнопкам
- ОКБ
- Краснодар
- ОКБ
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Сменить менеджера клиента
- Инициаторы:
- Битрикс
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Сменить менеджера клиента
- Инициаторы:
- 1С
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Вялкова Анна
- задача 1:
- Сменить менеджера клиента
- Инициаторы:
- Подтвердить выбор
- при выборе нескольких, ставить совокупность всех задач по выбранным кнопкам
- ОКБ
- Крым
- Внести нового клиента
- Крым
- ОКБ
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести нового клиента
- Инициаторы:
- Битрикс
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести нового клиента
- Инициаторы:
- 1С
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файл
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Вялкова Анна
- задача 1:
- Внести нового клиента
- Инициаторы:
- Подтвердить выбор
- при выборе нескольких, ставить совокупность всех задач по выбранным кнопкам
- ОКБ
- Краснодар
- ОКБ
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести нового клиента
- Инициаторы:
- Битрикс
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Руднева Надежда
- задача 1:
- Внести нового клиента
- Инициаторы:
- 1С
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файл
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Татьяна Савойская
- Степанов Сергей
- Селифанова Эльза
- Исполнитель:
- Вялкова Анна
- задача 1:
- Внести нового клиента
- Инициаторы:
- Подтвердить выбор
- при выборе нескольких, ставить совокупность всех задач по выбранным кнопкам
- ОКБ
- Крым
- Поменять название
- Заказ товаров
- На месяц
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- прикрепить файлы
- Согласующий 1:
- Великожон Татьяна
- внести текст
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Щасливая Екатерина
- Степанов Сергей
- Исполнитель:
- Великожон Татьяна
- задача 1:
- Внести заказ в таблицу
- отправить менеджеру сообщение НЕ "исполнитель завершил задачу", а "Заказ принят, следи за статусом в гугл-таблице"
- Инициаторы:
- Краснодар
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файлы
- Согласующий 1:
- Великожон Татьяна
- внести текст
- Согласующий 2:
- Макаренко Александр
- Наблюдатели:
- Щасливая Екатерина
- Степанов Сергей
- Исполнитель:
- Великожон Татьяна
- задача 1:
- Внести заказ в таблицу
- отправить менеджеру сообщение НЕ "исполнитель завершил задачу", а "Заказ принят, следи за статусом в гугл-таблице"
- Инициаторы:
- Крым
- Дополнительный заказ
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- прикрепить скриншоты
- Согласующий 1:
- Великожон Татьяна
- внести текст
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Щасливая Екатерина
- Степанов Сергей
- Исполнитель:
- Великожон Татьяна
- задача 1:
- Внести заказ в таблицу
- отправить менеджеру сообщение НЕ "исполнитель завершил задачу", а "Заказ принят, следи за статусом в гугл-таблице"
- Инициаторы:
- Краснодар
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить скриншоты
- Согласующий 1:
- Великожон Татьяна
- внести текст
- Согласующий 2:
- Макаренко Александр
- Наблюдатели:
- Щасливая Екатерина
- Степанов Сергей
- Исполнитель:
- Великожон Татьяна
- задача 1:
- Внести заказ в таблицу
- отправить менеджеру сообщение НЕ "исполнитель завершил задачу", а "Заказ принят, следи за статусом в гугл-таблице"
- Инициаторы:
- Крым
- На месяц
- Отгрузки на СОХ
- Реальная
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Согласующий 1:
- Шепко Лия
- внести текст
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Рудь Анфиса
- Романов Александр
- Рудник Алина
- Шепко Лия
- Исполнитель 1:
- Сергатова Светлана
- задача 1:
- Составить договор СОХ и акт приема-передачи с клиентом
- задача 2:
- Подписать договор СОХ и акт приема-передачи с клиентом и прикрепить этот файл далее
- прикрепить файл
- Исполнитель 2:
- Шепко Лия
- задача 1:
- Сверить договор и внести данные о лимите в таблицу
- Исполнитель 3:
- Романов Александр
- задача 1:
- Убедиться, что отгрузка на СОХ согласована, проведена и не задублирована в 1С
- Исполнитель 2:
- Инициаторы:
- Краснодар
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- Согласующий 1:
- Шепко Лия
- внести текст
- Согласующий 2:
- Макаренко Александр
- Согласующий 3:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Рудь Анфиса
- Романов Александр
- Рудник Алина
- Шепко Лия
- Исполнитель 1:
- Сергатова Светлана
- задача 1:
- Составить договор СОХ и акт приема-передачи с клиентом
- задача 2:
- Подписать договор СОХ и акт приема-передачи с клиентом и прикрепить этот файл далее
- прикрепить файл
- Исполнитель 2:
- Шепко Лия
- задача 1:
- Сверить договор и внести данные о лимите в таблицу
- Исполнитель 3:
- Романов Александр
- задача 1:
- Убедиться, что отгрузка на СОХ согласована, проведена и не задублирована в 1С
- Исполнитель 2:
- Инициаторы:
- Крым
- Для лимита
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Согласующий 1:
- Шепко Лия
- внести текст
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Шепко Лия
- Исполнитель 1:
- Сергатова Светлана
- задача 1:
- Составить договор СОХ и акт приема-передачи с клиентом
- задача 2:
- Подписать договор СОХ и акт приема-передачи с клиентом и прикрепить этот файл далее
- прикрепить файл
- Исполнитель 2:
- Шепко Лия
- задача 1:
- Сверить договор и внести данные о лимите в таблицу
- Исполнитель 2:
- Инициаторы:
- Краснодар
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- Согласующий 1:
- Шепко Лия
- внести текст
- Согласующий 2:
- Макаренко Александр
- Согласующий 3:
- Тищенко Владимир
- Наблюдатели:
- Шепко Лия
- Исполнитель 1:
- Сергатова Светлана
- задача 1:
- Составить договор СОХ и акт приема-передачи с клиентом
- задача 2:
- Подписать договор СОХ и акт приема-передачи с клиентом и прикрепить этот файл далее
- прикрепить файл
- Исполнитель 2:
- Шепко Лия
- задача 1:
- Сверить договор и внести данные о лимите в таблицу
- Исполнитель 2:
- Инициаторы:
- Крым
- Реальная
- Паспорт сделки
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- прикрепить файлы
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Коршунова Александра
- Шепко Лия
- Танашевич Анифе
- Инициаторы:
- Краснодар
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Пирожников Павел
- прикрепить файлы
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Коршунова Александра
- Шепко Лия
- Танашевич Анифе
- Инициаторы:
- Беларусь
- Инициаторы:
- Пирожников Павел
- Гацолаева Наталья
- прикрепить файлы
- Согласующий 1:
- Пирожников Павел
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Коршунова Александра
- Шепко Лия
- Танашевич Анифе
- Инициаторы:
- Крым
- Ставки логистика
- Разово
- Инициаторы:
- Танашевич Анифе
- Минаджиева Залина
- Чалышева Ление
- прикрепить файлы (не обяз.)
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Танашевич Анифе
- Минаджиева Залина
- Чалышева Ление
- Храпач Анастасия
- Шепко Лия
- Инициаторы:
- Надолго
- Инициаторы:
- Танашевич Анифе
- Минаджиева Залина
- Чалышева Ление
- прикрепить файлы (не обяз.)
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Танашевич Анифе
- Минаджиева Залина
- Чалышева Ление
- Храпач Анастасия
- Шепко Лия
- Исполнитель 1:
- Коршунова Александра
- задача 1:
- Внести изменения в гугл-таблицу Ставки логистики (с обязательным указанием, с какого срока начинает действовать ставка)
- Инициаторы:
- Разово
- Поступления по приходным документам
- Ф1
- Инициаторы:
- Щасливая Екатерина
- Великожон Татьяна
- Сергатова Светлана
- Широков Михаил
- прикрепить файлы (обяз.)
- Согласующий 1:
- Щасливая Екатерина
- внести текст, который будет виден и исполнителю тоже
- Наблюдатели:
- Щасливая Екатерина
- Великожон Татьяна
- Демченко Елена
- Вялкова Анна
- Шведов Никита
- Исполнитель 1:
- Сергатова Светлана
- задача 1:
- Проверить и подтвердить, что приход проведен по 1С:Бухгалтерия
- Исполнитель 2:
- Романов Александр
- задача 1:
- Проверить и подтвердить, что приход проведен по 1С:УТ
- Инициаторы:
- Ф2
- Инициаторы:
- Щасливая Екатерина
- Великожон Татьяна
- Широков Михаил
- прикрепить файлы (обяз.)
- Согласующий 1:
- Щасливая Екатерина
- внести текст, который будет виден и исполнителю тоже
- Наблюдатели:
- Щасливая Екатерина
- Великожон Татьяна
- Демченко Елена
- Вялкова Анна
- Шведов Никита
- Исполнитель 1:
- Романов Александр
- задача 1:
- Проверить и подтвердить, что приход проведен по 1С:УТ
- Инициаторы:
- Ф1
- Доставка клиентам\на склад
- выбор менеджера/зав склада из списка согласующих:
- все менеджеры, помощники и зав складов
- Инициаторы:
- Танашевич Анифе
- Чалышева Ление
- Минаджиева Залина
- прикрепить файлы
- Согласующий 1 (на выбор):
- Макаренко Александр
- Петрунин Кирилл
- Стуков Павел
- Федосенко Иван
- Сыпков Станислав
- Бондаренко Михаил
- Михайлова Татьяна
- Широков Михаил
- Чекальский Александр
- Шевченко Виталий
- Халилов Дамур
- Наблюдатели:
- Танашевич Анифе
- Щасливая Екатерина
- Великожон Татьяна
- Рудник Алина
- Жуков Дмитрий
- Сергатова Светлана
- Исполнитель 1:
- Романов Александр
- задача 1:
- Клиент забрал товар, менеджер подтвердил. Проверить и подтвердить, что отгрузка записана И ПРОВЕДЕНА в 1С
- выбор менеджера/зав склада из списка согласующих:
- В. Отгрузки
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Сыпков Станислав
- Федосенко Иван
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Шепко Лия
- Исполнитель 1:
- Сергатова Светлана
- задача 1:
- Создать реализацию и отправить клиенту на подпись
- задача 2:
- Убедиться, что клиент подписал реализацию
- задача 3:
- Перепроверить, что клиент подписал реализацию и поступила оплата. Если подписано И оплата поступила, нажмите, чтобы подтвердить (менеджеру придет уведомление о том, что документ подписан и деньги получены)
- Инициаторы:
- Менеджерские расходы
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Романов Александр
- Рудник Алина
- Шепко Лия
- Инициаторы:
- Краснодар
- Инициаторы:
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Романов Александр
- Рудник Алина
- Шепко Лия
- Инициаторы:
- Крым
- Отгрузки
- МНОЖЕСТВЕННЫЙ выбор менеджера из списка наблюдателей:
- все менеджеры, помощники
- Инициаторы:
- Романов Александр
- Рудник Алина
- прикрепить файлы (обяз.)
- Согласующий 1:
- Коршунова Александра
- сделать текстовую приписку для этого согласующего:
- Если не входит в рамки согласованного лимита, добавь руководителя соответствующего филиала к согласованию через кнопку "Добавить согласующего"
- Наблюдатели (всегда логисты и Лия + на выбор из кнопки менеджер):
- Танашевич Анифе
- Чалышева Ление
- Минаджиева Залина
- Шепко Лия
- на выбор из предыдущего шага:
- Макаренко Александр
- Петрунин Кирилл
- Стуков Павел
- Федосенко Иван
- Сыпков Станислав
- Бондаренко Михаил
- Михайлова Татьяна
- Гацолаева Наталья
- Чекальский Александр
- Шевченко Виталий
- МНОЖЕСТВЕННЫЙ выбор менеджера из списка наблюдателей:
- Нестандартные условия договора поставки
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- прикрепить файлы
- Согласующий 1:
- Аблякимова Айше
- внести текст
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Жуков Дмитрий
- Сергатова Светлана
- Шепко Лия
- Инициаторы:
- Краснодар
- Инициаторы:
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- прикрепить файлы
- Согласующий 1:
- Аблякимова Айше
- внести текст
- Согласующий 2:
- Макаренко Александр
- Согласующий 3:
- Тищенко Владимир
- Наблюдатели:
- Жуков Дмитрий
- Сергатова Светлана
- Михайлова Татьяна
- Шепко Лия
- Инициаторы:
- Беларусь
- Инициаторы:
- Пирожников Павел
- Гацолаева Наталья
- прикрепить файлы
- Согласующий 1:
- Аблякимова Айше
- внести текст
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Жуков Дмитрий
- Сергатова Светлана
- Михайлова Татьяна
- Шепко Лия
- Инициаторы:
- Крым
- Вакансии
- выбор рук отдела из списка согласующих
- Инициаторы:
- Турчина Варвара
- прикрепить файлы
- Согласующий 1 (на выбор):
- Щасливая Екатерина
- Танашевич Анифе
- Халилов Дамур
- Ковтун Игорь
- Шепко Лия
- Свирень Максим
- Макаренко Александр
- Жуков Дмитрий
- Демченко Елена
- Савойская Татьяна
- Родюшкин Артем
- Тищенко Владимир
- Наблюдатели:
- Горбачёва Оксана
- Стрижак Виолетта
- Демченко Елена
- Вялкова Анна
- Шепко Лия
- Инициаторы:
- выбор рук отдела из списка согласующих
- Ставки доставки АП
- Инициаторы:
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Согласующий 1:
- Танашевич Анифе
- Наблюдатели:
- Танашевич Анифе
- Чалышева Ление
- Минаджиева Залина
- Инициаторы:
- Утверждение фрилансеров
- Инициаторы:
- Халилов Дамур
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Танашевич Анифе
- Вялкова Анна
- Храпач Анастасия
- Инициаторы:
- Выход в нерабочий день
- Инициаторы:
- Танашевич Анифе
- Романов Александр
- Шведов Никита
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Халилов Дамур
- Танашевич Анифе
- Вялкова Анна
- Храпач Анастасия
- Инициаторы:
- Долгосрочные условия ОТК
- Крым
- Инициаторы:
- Чекальский Александр
- Шевченко Виталий
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Гацолаева Наталья
- Федосенко Вероника
- Федосенко Иван
- Свирень Максим
- Бояджан Сергей
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Шепко Лия
- Исполнитель 1:
- Ковтун Игорь
- задача 1:
- Зафиксировать согласованные условия в файле ОТК
- Инициаторы:
- Краснодар
- Инициаторы:
- Макаренко Александр
- Михайлова Татьяна
- Петрунин Кирилл
- Стуков Павел
- Бондаренко Михаил
- Гацолаева Наталья
- Сыпков Станислав
- Федосенко Вероника
- Федосенко Иван
- Согласующий 1:
- Макаренко Александр
- Согласующий 2:
- Тищенко Владимир
- Наблюдатели:
- Шепко Лия
- Исполнитель 1:
- Ковтун Игорь
- задача 1:
- Зафиксировать согласованные условия в файле ОТК
- Инициаторы:
- Беларусь
- Инициаторы:
- Пирожников Павел
- Гацолаева Наталья
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Шепко Лия
- Исполнитель 1:
- Ковтун Игорь
- задача 1:
- Зафиксировать согласованные условия в файле ОТК
- Инициаторы:
- Крым
- Остаток на кассах
- выбор сотрудника из списка согласующих
- Инициаторы:
- Романов Александр
- Рудник Алина
- прикрепить файлы
- Согласующий 1 (на выбор):
- все сотрудники
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- выбор сотрудника из списка согласующих
- Продажи по клиенту
- выбор менеджера из списка согласующих
- Инициаторы:
- Романов Александр
- Рудник Алина
- Вялкова Анна
- прикрепить файлы
- Согласующий 1 (на выбор):
- Макаренко Александр
- Петрунин Кирилл
- Стуков Павел
- Федосенко Иван
- Сыпков Станислав
- Бондаренко Михаил
- Чекальский Александр
- Шевченко Виталий
- Свирень Максим
- Бояджан Сергей
- Пирожников Павел
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- выбор менеджера из списка согласующих
- Проведенные расходы по менеджеру
- выбор менеджера из списка согласующих
- Инициаторы:
- Вялкова Анна
- прикрепить файлы
- Согласующий 1 (на выбор):
- Макаренко Александр
- Петрунин Кирилл
- Стуков Павел
- Федосенко Иван
- Сыпков Станислав
- Бондаренко Михаил
- Чекальский Александр
- Шевченко Виталий
- Свирень Максим
- Бояджан Сергей
- Пирожников Павел
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Инициаторы:
- выбор менеджера из списка согласующих
- Согласование удаленной работы
- Смена условий автоматического согласования сделок
- повысить лимит дебеторской задолженности от Владимира лично
- Инициаторы:
- Макаренко Александр
- Петрунин Кирилл
- Стуков Павел
- Федосенко Иван
- Сыпков Станислав
- Бондаренко Михаил
- Чекальский Александр
- Шевченко Виталий
- Свирень Максим
- Бояджан Сергей
- Пирожников Павел
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- нет
- Исполнитель:
- Шепко Лия
- задача 1:
- Внести изменения в таблицу "Условия автоматического согласования"
- Инициаторы:
- увеличить кол-во возможных долгов по документам, сумм этих документов, кол-во возможных дней просрочки оплаты
- Инициаторы:
- Макаренко Александр
- Петрунин Кирилл
- Стуков Павел
- Федосенко Иван
- Сыпков Станислав
- Бондаренко Михаил
- Чекальский Александр
- Шевченко Виталий
- Свирень Максим
- Бояджан Сергей
- Пирожников Павел
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- нет
- Исполнитель:
- Ковтун Игорь
- задача 1:
- Внести изменения в гугл-таблицу
- Инициаторы:
- повысить лимит дебеторской задолженности от Владимира лично
- Утверждение оплат безнал
- Нужна ПП?
- Да
- Инициаторы:
- Жуков Дмитрий
- Свирень Максим
- Танашевич Анифе
- Шепко Лия
- прикрепить файл (обяз)
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Шепко Лия
- Исполнитель:
- Рудь Анфиса
- задача 1:
- Оплатить счет
- задача 2:
- Отправить ПП сотруднику
- прикрепить файл
- Инициаторы:
- Нет
- Инициаторы:
- Жуков Дмитрий
- Свирень Максим
- Танашевич Анифе
- Шепко Лия
- прикрепить файл (обяз)
- Согласующий 1:
- Тищенко Владимир
- Наблюдатели:
- Демченко Елена
- Вялкова Анна
- Жуков Дмитрий
- Сергатова Светлана
- Шведов Никита
- Романов Александр
- Рудник Алина
- Шепко Лия
- Исполнитель:
- Рудь Анфиса
- задача 1:
- Оплатить счет
- Инициаторы:
- Да
- Нужна ПП?
1. Структура репозитория (package map)
telegram_service/
├── approvalflow/
│ ├── FlowNode / BuildTree / FindNode — модель дерева
│ ├── DynamicTopic.go — тема категории, шаблоны, подписи
│ └── registry.go — Registry, LoadFromDB, алиасы
├── state/
│ └── state.go — ApprovalFlowState, StateManager (Redis)
├── keyboard/
│ ├── *.go (BuildApprovalMenu, BuildApprovalStep, BuildMultiSelectStep, ...)
│ └── delegation_keyboard.go — 🆕 клавиатуры делегирования
├── handlers/approval/
│ ├── handler.go — Handler, NewHandler, HandleMenu*, HandleCallback, HandleTextInput, HandleBackCallback, continueFlowAfterInput
│ ├── decision.go — HandleDecision, handleDecision, processNextStepOrComplete
│ ├── add_approver.go — доп. согласующие: добавление, список категорий/заявок
│ ├── tasks.go — задачи исполнителей, отзыв связанных задач
│ ├── delegation.go — 🆕 делегирование обязанностей
│ ├── helpers.go
│ └── revoke.go
├── lib/internally/redis/ — обёртка над Redis-клиентом
└── ...
core/lib/external/tgkit/
└── client.go — HTTP-клиент бота, прокси, ретраи
proxy/
└── main.go — внешний прокси-сервис к Telegram Bot API
scheduler/
└── daily_reminder.go — ежедневная сводка (9:00 МСК, будни)
2. Полная модель данных
2.1. ApprovalTopicCategory
type ApprovalTopicCategory struct {
ID *uint64
Name *string
Code *string
Description *string
IsActive *bool
CreatedAt *time.Time
UpdatedAt *time.Time
}
Примеры реальных категорий, встретившихся в данных: sickleave («Больничный», ID=3), vacation («Отпуск», ID=2), timeoff («Отгул», ID=1). Алиас absence в Registry объединяет все три для целей проверки прав доступа в главном меню.
2.2. ApprovalTopic
type ApprovalTopic struct {
ID *uint64
CategoryID *uint64
Name *string
Code *string
ApprovalMode *string // "sequential" | "any_of"
DurationType *string // single_day | multi_day | date_range | sl_single_day | sl_date_range
TimeType *string // full_day | partial
PaymentType *string // own_expense | vacation_account | official
Template *string // текст-подсказка формата ввода, напр. "22.02.2022"
FirstApproverRoleCode *string
AllowsFile *bool
AllowApproverSelection *bool
AllowObserverSelection *bool
AllowDelegationSelection *bool
RequireApproverComment *bool
RequiresFile *bool
RequireObserverSelection *bool
IsActive *bool
CreatedAt *time.Time
UpdatedAt *time.Time
Instructions *string
Category *ApprovalTopicCategory // populated-связь
}
Реальный пример из БД (тема id=19, категория «Больничный»):
{
"id": 19, "category_id": 3,
"name": "Больничный — диапазон официальный",
"code": "sickleave_date_range_full_day_official",
"ApprovalMode": "sequential",
"duration_type": "date_range", "time_type": "full_day",
"payment_type": "official",
"template": "22.02.2022 - 25.02.2022",
"allows_file": true, "requires_file": false,
"allow_approver_selection": false, "allow_observer_selection": false,
"require_approver_comment": false, "require_observer_selection": false,
"is_active": true
}
2.3. ApprovalFlowNode (дерево согласования)
type ApprovalFlowNode struct {
ID *uint64
ParentID *uint64 // nil = корневой узел
CategoryID *uint64
Key string // технический ключ, участвует в callback_data
Label string // видимый текст кнопки / источник {{.key_label}}
DataKey string // "" если не кнопка с данными
DataValue string
IsInput bool
InputKey string // обычно "content"
InputHint string
IsMultiSelect bool
MultiSelectKey string
MultiSelectValue string
TopicCode string // опционально: ограничение видимости узла по правам
MessageTemplate *string // только на корневом узле
CodeTemplate *string // только на корневом узле
Children []*ApprovalFlowNode // заполняется BuildTree()
}
Инвариант: Key корневого узла (ParentID == nil) обязан совпадать с ApprovalTopicCategory.Code — иначе HandleCallback не найдёт начало дерева при нажатии кнопки раздела в главном меню (симптом в логах: node not found).
2.4. Approval
type Approval struct {
ID *uint64
InitiatorID *uint64
TopicID *uint64
DepartmentID *uint64
Content *string // свободный текст, финальный ввод сотрудника
Status *string // draft|pending|approved|rejected|revoked|revision
Metadata *string // JSON: {"extra": {...}}
RevisionCount int
RevokedByID *uint64
RevokedAt *time.Time
CreatedAt *time.Time
UpdatedAt *time.Time
// populated-связи (не всегда присутствуют, зависит от запроса)
Initiator *User
Topic *ApprovalTopic
Department *Department
Steps []*ApprovalStep
Files []*ApprovalFile
RevokedBy *User
}
Metadata — JSON вида {"extra": {"time_type": "full_day", "absence_type": "sickleave", "payment_type": "official", "duration_type": "sl_date_range", "open_date": "11.07.2026"}}. Обратите внимание: open_date присутствует не всегда и не является надёжным источником полного диапазона дат — единственный надёжный источник дат — свободнотекстовое поле Content (см. раздел про разбор дат в scheduler, §19).
2.5. ApprovalStep
type ApprovalStep struct {
ID *uint64
ApprovalID *uint64
ApproverID *uint64
StepOrder *int
Status *string // waiting|pending|approved|rejected|cancelled_any_of|cancelled
ApproverNameSnapshot *string // ФИО на момент назначения — не меняется при делегации
ActedAt *time.Time
Comment *string
CreatedAt *time.Time
UpdatedAt *time.Time
}
ApproverNameSnapshot заполняется реальным исполнившим действие пользователем (actualExecutor.FullName) в момент решения — то есть после одобрения делегатом в снапшоте окажется имя делегата, а не исходного согласующего.
2.6. ApprovalAdditionalApprover
type ApprovalAdditionalApprover struct {
ID *uint64
ApprovalID *uint64
ApproverID *uint64
AddedByID *uint64
Status *string // pending|approved|rejected
Comment *string
ActedAt *time.Time
}
Независим от ApprovalStep — у доп. согласующего нет собственной строки в ApprovalStep (источник одного из ключевых багов, §22).
2.7. ApprovalTask и ApprovalTopicExecutor
type ApprovalTask struct {
ID *uint64
ApprovalID *uint64
ExecutorID *uint64
TaskText *string
Status *string // pending|waiting|completed|cancelled
StepOrder *int
RequireFile *bool
TopicExecutorID *uint64
CreatedAt *time.Time
CompletedAt *time.Time
}
type ApprovalTopicExecutor struct {
ID *uint64
TopicID *uint64
UserID *uint64
TargetSystem *string // "all" | "okb" | "1c" — фильтр по выбранной сотрудником системе
TaskText *string
RequireFile *bool
}
createTasksForApproval создаёт по одной задаче на каждую (система × исполнитель) комбинацию, где TargetSystem совпадает ("all" matчит любую систему). Первая созданная задача сразу pending, остальные waiting — активируются последовательно по мере выполнения предыдущей (activateNextTask).
2.8. UserHierarchy
type UserHierarchy struct {
ID *uint64
SubordinateID *uint64
ApproverID *uint64
DepartmentID *uint64
TopicCategoryID *uint64 // взаимоисключающе с TopicID
TopicID *uint64
Priority int
}
Область действия правила определяется по заполненным полям: TopicID задан → правило только для этой темы; иначе TopicCategoryID задан → для всей категории; оба nil → глобальное правило («для всех тем согласований»).
2.9. ApprovalDelegation (реальная структура, прислана пользователем)
type ApprovalDelegation struct {
ID *uint64
DelegatorID *uint64 // not null
DelegateID *uint64 // not null
CategoryID *uint64 // nullable
TopicID *uint64 // nullable
ApprovalID *uint64 // 🆕 nullable — привязка к заявке-отсутствию
ValidUntil *time.Time // not null
IsActive *bool // not null, default true
NotifiedDay *bool // not null, default false
Notified2h *bool // not null, default false
CreatedAt *time.Time
UpdatedAt *time.Time
Delegator *User
Delegate *User
Category *ApprovalTopicCategory
Topic *ApprovalTopic
}
2.10. Прочие сущности (кратко, по факту использования)
ApprovalComment { ApprovalID, UserID, Comment }ApprovalObserver/ApprovalAdditionalObserver { ApprovalID, UserID, AddedByID }ApprovalFile { ApprovalID, TaskID(nullable), FileBase64, FileName, AttachedByID }User { ID, TelegramID, Username, FullName, LanguageCode, DepartmentID, IsBlocked, IsFired }Department { ID, Name, Code, Description, IsActive }
3. Пакет approvalflow
3.1. FlowNode.BuildTree() / FindNode(tree, key)
Registry хранит плоский список узлов на категорию; BuildTree() превращает его в дерево по ParentID. FindNode — рекурсивный поиск по Key внутри уже построенного дерева (используется и в HandleCallback, и в HandleBackCallback, и в HandleTextInput при переходе к следующему уровню после текстового ввода).
3.2. DynamicTopic
type DynamicTopic struct {
category *typescore.ApprovalTopicCategory
nodes []*typescore.ApprovalFlowNode
codeTemplate string
messageTemplate string
// 🆕 dataValueLabels: "data_key\x00data_value" -> Label узла с такими
// DataKey/DataValue (или MultiSelectKey/MultiSelectValue).
dataValueLabels map[string]string
}
NewDynamicTopic(category, nodes) — единственный проход по всем узлам: если узел корневой — забирает CodeTemplate/MessageTemplate; независимо от этого (без break, в отличие от исходной версии до правки) — если у узла есть Label и DataKey+DataValue (или MultiSelectKey+MultiSelectValue) — кладёт подпись в dataValueLabels под составным ключом data_key + "\x00" + data_value.
applyMessageTemplate(tmpl, initiatorName, data):
- Копирует
data(собранная картаdata_key → data_value) вtemplateData. - Добавляет
initiator_name,topic_name. addLabelMappings(templateData, data)— старый захардкоженный список соответствий (region: crimea→«Крым» и т.п., branch: f1→«Ф1» и т.п. — полный список хардкода не воспроизведён дословно в этой документации, см. исходный файлDynamicTopic.go).addTreeLabelMappings(templateData, data)— 🆕 перекрывает значения из п.3, если для конкретного(key, value)есть узел дерева сLabel. Для мультивыбора (значение вида"a,b") при отсутствии точного совпадения разбивает по запятой и подписывает каждую часть отдельно, объединяя через", ".- Выполняет Go-шаблон (
text/template), при ошибке парсинга/выполнения —defaultMessage(initiatorName, data)(запасной вариант).
TopicCodeFromCollected(collected) — если code_template непустой, выполняет его как Go-шаблон над collected; иначе — запасной вариант: сортирует ключи collected алфавитно и склеивает значения через _.
3.3. Registry
type Registry struct {
topics map[string]Topic // код темы → DynamicTopic
order []string
topicCodeToCategory map[string]string // алиасы
}
NewRegistry(ipc) — единственное место, где заполняются алиасы:
r.topicCodeToCategory["timeoff"] = "absence"
r.topicCodeToCategory["vacation"] = "absence"
r.topicCodeToCategory["sickleave"] = "absence"
LoadFromDB(ctx) — пересобирает topics/order с нуля, не трогает topicCodeToCategory — алиасы, заданные в NewRegistry, переживают любое количество перезагрузок автоматически.
watchRegistryReload() (горутина, запускается в NewHandler) — опрашивает Redis-ключ approval_flow:version каждые 5 секунд; при изменении значения вызывает LoadFromDB. Админ-панель обязана вызывать bumpFlowVersion() после любой мутации дерева/тем/категорий — иначе бот продолжит работать со старой версией дерева до перезапуска.
4. Пакет state
4.1. ApprovalFlowState (реальная структура, прислана пользователем)
type ApprovalFlowState struct {
TopicCode string
NodeKey string
Collected map[string]string
AwaitInput bool
InputKey string
History []string
AwaitFileInput bool
Files []FileAttachment
FileStatusMsgID int
FilesStepCompleted bool
MultiSelected map[string][]string
AwaitMultiSelectConfirm bool
MultiSelectNodeKey string
AwaitFirstApproverSelection bool
SelectedFirstApproverID uint64
AwaitApproverSelection bool
SelectedApproverID uint64
AwaitObserverSelection bool
ObserverSelectionDone bool
SelectedObserverIDs []uint64
// Делегирование
AwaitDelegationChoice bool
AwaitDelegateSelection bool
DelegationSelectionDone bool
SelectedDelegateID uint64
}
type FileAttachment struct {
Base64 string
FileName string
MessageID int
}
4.2. StateManager
- Redis-ключ:
approval_flow:{telegramID}. - TTL: 30 минут (
stateTTL) — если пользователь бросил диалог дольше этого времени, состояние теряется, при попытке продолжить бот просит начать заново («⚠️ Состояние утеряно. Начните заново.»). Getтрактует отсутствие ключа как штатную ситуацию (nil, nil), не как ошибку.- Помимо
ApprovalFlowState,StateManagerуправляет и другими, отдельными видами состояний, встретившимися по коду:AddApproverFlowState(флоу добавления доп. согласующего, ключиSetAddApprover/GetAddApprover/ClearAddApprover),AttachmentSession(сессия прикрепления файла к задаче, поляApprovalID,TaskID,Files,Stage), состояния комментариев (GetCommentFlow/GetCommentReply,Stage: "await_text")
5. Handler — инициализация
type Handler struct {
ipc *types.InternalProviderControl
Registry *Registry
States *state.StateManager
}
func NewHandler(ipc *types.InternalProviderControl) *Handler {
h := &Handler{
ipc: ipc,
Registry: NewRegistry(ipc),
States: state.NewStateManager(ipc.RedisClient),
}
ctx, cancel := context.WithTimeout(context.Background(), variables.ContextTimeoutLong)
if err := h.Registry.LoadFromDB(ctx); err != nil {
logrus.Errorf("Failed to load topics from DB: %v", err)
}
cancel()
go h.watchRegistryReload() // см. §3.3
return h
}
Обратите внимание: ошибка первичной загрузки дерева только логируется, не приводит к остановке бота — при полностью пустом/ неудачно загруженном реестре бот запустится, но не сможет провести ни одного согласования до успешной перезагрузки через watchRegistryReload.
5.1. Меню и подменю
5.2. getAllowedTopicCodes(ctx, user)
Возвращает map[string]bool. Логика:
- Тянет разрешения (
Permission,action="create") пользователя. - Если разрешений нет вообще — возвращает пустую карту (доступа нет никуда).
- Для каждой темы из реестра ищет категорию по коду; если это код
"absence"(агрегирующий алиас) — отдельно проверяет три дочерние категории (timeoff/vacation/sickleave) — по прямому разрешению на категорию или по разрешению на конкретную тему внутри неё. - Для остальных (не-
absence) тем — обычная проверка: разрешение на категорию целиком, либо на конкретную тему.
6. Прохождение дерева
6.1. HandleCallback(bot, cq, topicCode, nodeKey)
Формат входящего callback_data: appr:{topicCode}:{nodeKey}.
1. Отвечает на callback, удаляет предыдущее сообщение с кнопками.
2. topic := Registry.Get(topicCode) — если не найдено, warn-лог и return.
3. flowState := States.Get(telegramID)
isNew := flowState == nil
если isNew — создаёт новый ApprovalFlowState{TopicCode, NodeKey, Collected: {}}
4. tree := topic.BuildTree(); node := FindNode(tree, nodeKey)
если nil — warn-лог "node not found", return (см. инвариант §2.3 про Key корня)
5. если НЕ isNew — History = append(History, flowState.NodeKey) (пуш текущего перед переходом)
6. если node.DataKey != "" — Collected[node.DataKey] = node.DataValue
7. если у node нет детей И это не IsInput — warn "leaf node without IsInput", return
(защита от некорректно построенного дерева)
8. Особый случай: у узла РОВНО один ребёнок, и это IsInput —
пропускает промежуточный экран, сразу переходит в режим ожидания
текста: AwaitInput=true, InputKey=inputNode.InputKey, NodeKey=nodeKey (!),
показывает InputHint, клавиатура BuildApprovalInputPrompt(topicCode).
(NodeKey указывает на РОДИТЕЛЯ input-узла, не на сам input — важно
для последующей логики в HandleTextInput, см. §6.2)
9. Иначе: NodeKey = nodeKey, State.Set(...)
visibleChildren := filterNodesByAccess(ctx, telegramID, node.Children)
если пусто после фильтрации — "⚠️ У вас нет доступа к подкатегориям
этого раздела.", States.Clear (!) — весь диалог сбрасывается.
10. Детект мультивыбора: если у первого видимого ребёнка IsMultiSelect —
переходит в режим AwaitMultiSelectConfirm, MultiSelectNodeKey=nodeKey,
клавиатура BuildMultiSelectStep(..., nil, nil).
11. Иначе — обычная клавиатура BuildApprovalStep(topicCode, visibleChildren).
6.2. HandleTextInput(bot, msg) bool
Вызывается на любое текстовое сообщение от пользователя (не callback), возвращает bool — обработано ли сообщение этим хендлером (если false — сообщение не относится к диалогу согласования, дальше по цепочке обработчиков может пойти что-то другое).
1. Проверка Redis-ключа appr_approve_comment_pending:{telegramID} —
если есть, это ввод ОБЯЗАТЕЛЬНОГО комментария к решению (см. §12.4),
не к созданию заявки. Удаляет ключ, вызывает HandleDecisionWithChat
с approved=true и введённым текстом. return true.
2. Проверка состояния комментариев к заявке (GetCommentFlow) —
делегирует в HandleCommentTextInput, если Stage == "await_text".
3. Проверка состояния ответа на комментарий (GetCommentReply) —
аналогично, HandleCommentReplyInput.
4. flowState := States.Get(telegramID) — если nil или !AwaitInput —
return false (не наш случай).
5. Collected[flowState.InputKey] = msg.Text; AwaitInput = false
6. Особый случай "текст → сразу следующий уровень кнопок, минуя лишний
Set+показ":
topic := Registry.Get(flowState.TopicCode)
parentNode := FindNode(tree, flowState.NodeKey) ← это тот самый
родитель input-узла, сохранённый в п.8 §6.1
если у parentNode ровно один ребёнок IsInput и у ЭТОГО inputNode
ЕСТЬ дети — сразу показывает их (с фильтрацией по правам,
NodeKey переключается на inputNode.Key), return true.
7. Иначе (input — реальный лист дерева, дальше веток нет):
States.Set(...); continueFlowAfterInput(...) — переход к
дополнительным шагам (§7). return true.
6.3. HandleBackCallback(bot, cq, topicCode)
- Если
flowState.AwaitFileInput— сбрасывает режим файлового ввода, удаляет статусное сообщение (FileStatusMsgID), если было. - Если
flowState.AwaitInput— сбрасывает режим текстового ввода. - Если
Historyпуста — полный сброс состояния,HandleMenu(возврат к выбору темы). - Иначе —
pop()изHistory,FindNodeпо этому ключу, показывает его детей (с фильтрацией по правам). Важно: при возврате назад фильтр мультивыбора/input повторно не применяется — всегда показывает обычную клавиатуру кнопок, даже если по факту это узел с одним input-ребёнком.
7. continueFlowAfterInput — цепочка дополнительных шагов
Единая точка, куда сходятся: конец ввода текста (§6.2), любой из дополнительных шагов ниже (каждый в конце сам вызывает эту функцию снова, чтобы проверить следующий шаг в цепочке).
func (h *Handler) continueFlowAfterInput(bot, ctx, chatID, telegramID, flowState) {
topic := Registry.Get(flowState.TopicCode)
если topic == nil { States.Clear(); return }
topicCode, err := topic.TopicCodeFromCollected(flowState.Collected)
если err != nil { warn-лог; States.Clear(); return }
approvalTopic := GetApprovalTopicByCodeDB(ctx, topicCode)
// approvalTopic может остаться nil при ошибке — часть проверок ниже
// построена как `approvalTopic != nil && approvalTopic.X != nil && *approvalTopic.X`,
// то есть при ошибке получения темы бот тихо пропускает ВСЕ
// дополнительные шаги и уходит сразу к финализации.
// --- Шаг 1: файл ---
если approvalTopic != nil && !flowState.FilesStepCompleted:
requiresFile := RequiresFile
allowsFile := AllowsFile
isOfficial := Collected["payment_type"] == "official"
needFileStep := requiresFile || allowsFile || isOfficial
если needFileStep && !AwaitFileInput:
AwaitFileInput = true; FilesStepCompleted = false
States.Set(...)
showFileStatus(bot, chatID, telegramID, flowState, required=requiresFile||isOfficial)
return
// --- Шаг 2: первый согласующий по роли ---
если approvalTopic != nil && FirstApproverRoleCode не пусто
&& SelectedFirstApproverID == 0 && !AwaitFirstApproverSelection:
AwaitFirstApproverSelection = true; States.Set(...)
показать список пользователей с этой ролью (showFirstApproverSelection)
return
// --- Шаг 3: ручной выбор согласующего ---
если approvalTopic != nil && AllowApproverSelection
&& SelectedApproverID == 0 && !AwaitApproverSelection:
AwaitApproverSelection = true; States.Set(...)
showApproverSelection(...) // §8
return
// --- Шаг 4: наблюдатели ---
если approvalTopic != nil && AllowObserverSelection && !ObserverSelectionDone:
ObserverSelectionDone = true; AwaitObserverSelection = true; States.Set(...)
showObserverSelection(...) // §9
return
// --- Шаг 5 (🆕): делегирование ---
если approvalTopic != nil && AllowDelegationSelection && !DelegationSelectionDone:
AwaitDelegationChoice = true; States.Set(...)
showDelegationChoice(...) // §10
return
// --- Финализация ---
States.Clear(ctx, telegramID)
finalizeAndSubmit(bot, ctx, chatID, telegramID, flowState)
}
Порядок фиксирован и важен: файл → первый согласующий по роли → ручной выбор согласующего → наблюдатели → делегирование → отправка. Каждый шаг проверяет соответствующий флаг ApprovalTopic независимо — можно включить любую комбинацию на конкретной теме, они не исключают друг друга.
8. Выбор согласующего
8.1. Ручной выбор (showApproverSelection)
getAvailableApproversForTopic(ctx, userID, departmentID, approvalTopic):
hierarchies := GetUserHierarchyListDB(Filtering: {SubordinateID: userID, DepartmentID: departmentID})
approverIDs := {}
для каждой hier:
если hier.TopicID == approvalTopic.ID → approverIDs[hier.ApproverID] = true; continue
если hier.TopicCategoryID == approvalTopic.CategoryID → approverIDs[hier.ApproverID] = true
подгружает User по каждому approverID
⚠️ Обратите внимание: здесь фильтрация по иерархии идёт только по TopicID/TopicCategoryID — глобальные правила иерархии (оба поля nil в UserHierarchy) не учитываются этой функцией. Это согласуется с общей логикой (глобальное правило — это правило «по умолчанию для всех тем», а не «выбор из списка вариантов»), но стоит иметь в виду при диагностике «почему в списке выбора согласующего никого нет», если для сотрудника настроено только глобальное правило.
Если список пуст — «⚠️ Нет доступных согласующих для этой темы.», States.Clear.
HandleApproverSelectionCallback(bot, cq, approverID) — сохраняет SelectedApproverID, AwaitApproverSelection=false, показывает подтверждение с ФИО, вызывает continueFlowAfterInput.
HandleApproverSelectionPageCallback — пагинация того же списка, перестраивает GetApprovalTopicByCodeDB заново по актуальному TopicCodeFromCollected (не кэширует).
9. Наблюдатели
9.1. showObserverSelection
observers := getAvailableObservers(ctx) // все пользователи с ролью "observer"
isRequired := RequireObserverSelection == true
если observers пуст:
если isRequired:
"⚠️ Для этой темы обязательно выбрать наблюдателя, но в системе
нет пользователей с ролью наблюдателя. Обратитесь к администратору."
AwaitObserverSelection=false; ObserverSelectionDone=false (!) — специально
НЕ помечается выполненным, чтобы не позволить создать заявку в
нарушение обязательного требования; States.Set; return (диалог
застревает здесь до вмешательства администратора)
иначе:
AwaitObserverSelection=false; ObserverSelectionDone=true
States.Clear(); finalizeAndSubmit(...) — пропускает шаг, сразу отправляет
return
иначе показывает мультивыбор (BuildObserverSelectionMenu(observers,
SelectedObserverIDs, page=0, isRequired)), текст различается в
зависимости от isRequired (для обязательного — с уточнением
«Выберите менеджера, чья это отгрузка», это доменная формулировка,
специфичная для конкретных тем с обязательными наблюдателями).
9.2. Остальные хендлеры наблюдателей
HandleObserverToggleCallback— toggle одного ID вSelectedObserverIDs(добавить, если не было; убрать, если было), перерисовывает клавиатуру черезEditMessageReplyMarkup(не новое сообщение).HandleObserverPageCallback— пагинация, та же логикаisRequired, пересчитанная заново по текущемуflowState.HandleObserverConfirmCallback— еслиisRequiredи выбор пуст — показывает alert черезcallback(«⚠️ Выберите хотя бы одного наблюдателя»), не закрывает экран выбора. Иначе —States.Clear,finalizeAndSubmit.getAvailableObservers—GetUsersByRoleCodeDB(ctx, "observer").saveSelectedObservers(ctx, approvalID, addedByID, observerIDs)— вызывается изsubmitApprovalпосле создания заявки, создаёт по одной строкеApprovalAdditionalObserverна каждого выбранного.getAdditionalObserversForApproval(ctx, approvalID)— используется админкой/карточкой заявки для отображения списка наблюдателей.
10. Делегирование обязанностей (детально)
10.1. showDelegationChoice
Простой экран Да/Нет (keyboard.BuildDelegationChoiceMenu(), callback_data appr_deleg_yes/appr_deleg_no), с пояснением, что делегирование активируется только после согласования этой заявки и отменяется при её отзыве.
10.2. HandleDelegationChoiceCallback(bot, cq, wantsDelegate)
wantsDelegate == false:DelegationSelectionDone=true, сообщение «обязанности не делегируются»,continueFlowAfterInput.wantsDelegate == true:getAvailableDelegateCandidates— все активные сотрудники (не заблокированы, не уволены, естьTelegramID), кроме самого пользователя.- Если кандидатов нет — сообщение «Нет доступных сотрудников для делегирования»,
DelegationSelectionDone=true, идёт дальше без делегирования. - Иначе —
AwaitDelegateSelection=true, показываетBuildDelegateSelectionMenu(candidates, page=0).
10.3. HandleDelegateSelectionCallback(bot, cq, delegateID)
Важно: здесь запись в БД НЕ создаётся. Только сохраняет выбор (SelectedDelegateID), показывает предварительный расчёт delegationValidUntil для UX («Ориентировочно до: ...») и вызывает continueFlowAfterInput. Реальная запись появляется позже, вместе с самой заявкой (§11.4) — потому что flowState в Redis живёт всего 30 минут (§4.2), а решение по заявке может занять дни, поэтому долгосрочные данные (делегирование) не могут храниться только в flowState — они переносятся в постоянное хранилище (БД) при первой возможности, то есть в момент отправки заявки.
10.4. delegationValidUntil(collected map[string]string) (time.Time, bool)
Проходит по всем значениям flowState.Collected (не только по ключу "content" — на случай нестандартной настройки дерева, где дата могла осесть под другим ключом), ищет через regexp (\d{1,2})[.,](\d{1,2})\.(\d{4}) (запятая вместо точки после дня учтена специально — реальная опечатка встречалась в проде). Берёт максимальную найденную дату, время 23:59:59. Если дат не найдено — возвращает now + 30 дней и false (вызывающий код логирует warning).
10.5. createPendingDelegation / activatePendingDelegation / deactivatePendingDelegation
См. документ 1 (версия 1.0), раздел 5 — логика не изменилась, здесь фиксируется дополнительно точная последовательность вызовов:
| Функция | Вызывается из | Момент |
|---|---|---|
createPendingDelegation |
submitApproval |
сразу после создания Approval и сохранения файлов, до создания ApprovalStep |
activatePendingDelegation |
processNextStepOrComplete |
в ветке финального approved (НЕ в ветке передачи следующему шагу) |
deactivatePendingDelegation |
performActualRevoke |
между UpdateApprovalDB (статус → revoked) и handleRevokeCancellationTask |
Каждая из активации/деактивации сначала делает GetApprovalDelegationsListDB(Filtering: {ApprovalID: approval.ID}) — если пусто, тихо ничего не делает (делегирование не настраивалось при подаче этой заявки).
Техническая документация: Telegram-бот (детальная версия) — часть 3
Продолжение частей 1–2. Разделы 11–23.
11. submitApproval построчно
func (h *Handler) submitApproval(bot, ctx, chatID, telegramID, flowState) {
-
Пользователь и тема.
initiator := GetUserByTelegramIDDB;topic := Registry.Get(flowState.TopicCode);topicCode := topic.TopicCodeFromCollected(Collected);approvalTopic := GetApprovalTopicByCodeDB(ctx, topicCode). ЕслиapprovalTopic == nil— сообщение об ошибке пользователю,States.Clear, заявка не создаётся. -
Формирование
ContentиMetadata. ИзflowState.CollectedвApproval.Contentуходит только значение под ключомcontent(если есть). Все остальные парыkey: valueизCollectedуходят вmetadata.extraкак есть (сериализуется в JSON). -
Создание
Approval— статус сразуpending(неdraft),InitiatorID,TopicID = approvalTopic.ID,DepartmentID = initiator.DepartmentID,CreatedAt = now. -
Сохранение файлов (
flowState.Files, если были) —ApprovalFileна каждый,AttachedByID = initiator.ID,TaskID = nil(файлы уровня заявки, не задачи — в отличие от файлов, прикрепляемых исполнителем к конкретной задаче, см. §15). Подгружает файлы обратно вcreated.Filesдля последующих уведомлений (чтобы не делать отдельный запрос). -
createPendingDelegation(ctx, initiator, flowState, created)— см. §10.5. Вызывается безусловно; сама функция не делает ничего, еслиflowState.SelectedDelegateID == 0. -
Определение списка согласующих (
var approvers []*User):- Если
flowState.SelectedApproverID != 0(ручной выбор, §8.1) — единственный согласующий, весь дальнейший блок иерархии пропускается. - Иначе:
GetUserHierarchyListDB(Filtering: {SubordinateID: initiator.ID, DepartmentID: initiator.DepartmentID}), из них отбираются подходящие под тему записи (по тому же принципуTopicID→TopicCategoryID→ глобальное — в этой ветке уже включая глобальные правила, в отличие от §8.1, где глобальные не участвуют). - Если
flowState.SelectedFirstApproverID != 0(§8.2) — вставляется первым элементом списка, перед определёнными через иерархию (дедупликация по ID, если случайно совпал).
- Если
-
Группировка по приоритету (
priorityGroups) — сортировка найденныхUserHierarchyпоPriorityпо возрастанию, элементы с одинаковымPriorityпопадают в одну группу.ApprovalMode == "any_of": все согласующие (из всех групп) помещаются в группу 0 — любой один может решить за всех.- Иначе (
"sequential", по умолчанию): группы обрабатываются по порядку индекса — сначала все из группы 0 параллельно, затем группа 1, и т.д. (в текущей реализации внутри одной группы больше одного согласующего на практике не наблюдалось, но код группировки это допускает).
-
Создание
ApprovalStep— по одному на каждого согласующего. Первая группа (индекс 0) сразу получаетStatus = "pending", остальные группы —"waiting".ApproverNameSnapshotзаполняется сразу по имени назначенного (не делегата — см. §2.5, снапшот переписывается только в момент решения). -
Наблюдатели —
saveSelectedObservers(ctx, created.ID, initiator.ID, flowState.SelectedObserverIDs)(§9.2), если список непуст. -
Уведомление контролёров о создании новой заявки (см. §17.5).
-
Уведомление согласующих первой группы — для каждого: сначала проверка активного делегирования по теме/категории (
GetActiveDelegationForUser-подобная логика, тот же приоритет TopicID → CategoryID, что и в поиске согласующих) — если есть, сообщение о новой заявке уходит делегату, а не исходному согласующему; в остальных случаях — обычному согласующему. -
Сообщение инициатору — подтверждение отправки, с итоговым текстом (через
topic.FormatApproverMessage/аналог), список назначенных согласующих первой группы. -
States.Clear— на этом этапе уже вызван изcontinueFlowAfterInputперед вызовомfinalizeAndSubmit(см. §7), повторного вызова здесь не требуется.
12. handleDecision (решение согласующего)
12.1. Определение контекста вызова
func (h *Handler) handleDecision(bot, ctx, chatID, telegramID, approvalID, approved bool, comment string) {
approval := GetApprovalByIDDB(ctx, approvalID)
currentUser := GetUserByTelegramIDDB(ctx, telegramID)
// Проверка: это доп. согласующий?
additionalRecord := найти pending ApprovalAdditionalApprover для
(ApprovalID: approvalID, ApproverID: currentUser.ID)
isAdditionalApprover := additionalRecord != nil
mainStep := GetActiveStepDB(ctx, approvalID) // текущий активный ОСНОВНОЙ шаг, вне зависимости от того, кто сейчас решает
12.2. Ветка «дополнительный согласующий»
если isAdditionalApprover:
если approved:
additionalRecord.Status = "approved"; UpdateApprovalAdditionalApproverDB
actualExecutorID := currentUser.ID // 🩹 см. §22, п.3 — раньше сюда
// ошибочно попадал не тот ID
если allAdditionalApproved(ctx, approvalID) И mainStep.Status == "approved":
processNextStepOrComplete(ctx, bot, approval, mainStep, actualExecutorID)
// 🩹 §22 п.1: раньше вместо mainStep сюда передавался результат
// findStepForCurrentApprover(currentUserID) — всегда nil для
// доп. согласующего (у него нет строки в ApprovalStep, есть
// только в ApprovalAdditionalApprover) → заявка «зависала»
иначе:
// ждём либо основного согласующего, либо остальных доп.
уведомление доп. согласующему "Ваше решение принято, заявка
продолжит движение после решения остальных участников"
иначе (rejected):
additionalRecord.Status = "rejected"; UpdateApprovalAdditionalApproverDB
approval.Status = "rejected"; UpdateApprovalDB
// отклонение ЛЮБОГО доп. согласующего — блокирующее для всей заявки,
// в отличие от основной последовательности, где при any_of
// достаточно одного одобрения
уведомления инициатору, остальным согласующим (заявка закрыта)
return
12.3. Ветка «основной согласующий»
иначе (не доп. согласующий):
если approved:
если RequireApproverComment == true И mainStep.Comment пуст И comment == "":
// запрашиваем комментарий отдельным сообщением, СОХРАНЯЕМ
// решение "в подвешенном" виде через Redis-ключ
// appr_approve_comment_pending:{telegramID} = approvalID
// (см. §6.2, п.1 — HandleTextInput ловит следующий текстовый
// ввод и вызывает HandleDecisionWithChat(approved=true, text))
return
mainStep.Status = "approved"; mainStep.Comment = comment;
mainStep.ActedAt = now; mainStep.ApproverNameSnapshot = currentUser.FullName
UpdateApprovalStepDB
actualExecutorID := currentUser.ID
если allAdditionalApproved(ctx, approvalID):
processNextStepOrComplete(ctx, bot, approval, mainStep, actualExecutorID)
иначе:
// основной шаг одобрен, но есть неотвеченные доп. согласующие —
// заявка НЕ двигается дальше, ждём их
иначе (rejected):
mainStep.Status = "rejected"; ...; UpdateApprovalStepDB
если approval ApprovalMode == "any_of":
// отменяет ОСТАЛЬНЫЕ шаги той же группы приоритета
для каждого другого step в той же priority-группе со
статусом pending/waiting:
step.Status = "cancelled_any_of"; UpdateApprovalStepDB
уведомление этому согласующему "участие больше не требуется"
approval.Status = "rejected"; UpdateApprovalDB
уведомления инициатору, наблюдателям, директорам
12.4. Обязательный комментарий (детали флоу)
Redis-ключ appr_approve_comment_pending:{telegramID} хранит approvalID (строкой). При следующем текстовом сообщении от этого пользователя (независимо от того, есть ли активный ApprovalFlowState — проверка в HandleTextInput идёт первой, до проверки flowState, см. §6.2 п.1) — ключ удаляется, вызывается HandleDecisionWithChat(bot, chatID, telegramID, approvalID, approved=true, comment=msg.Text), которая и приводит к обычной ветке §12.3 уже с непустым comment.
13. processNextStepOrComplete
func (h *Handler) processNextStepOrComplete(ctx, bot, approval, currentStep, actualExecutorID uint64) {
nextStep := следующий ApprovalStep этой заявки с наименьшим
StepOrder среди тех, что ещё Status == "waiting"
(то есть первый шаг следующей приоритетной группы)
если nextStep != nil:
// --- Заявка ПЕРЕДАНА следующему согласующему, ещё НЕ одобрена целиком ---
nextStep.Status = "pending"; UpdateApprovalStepDB
// проверка делегирования следующего согласующего — та же логика,
// что при первичной отправке (§11 п.11)
уведомление следующему согласующему (или его делегату)
return // ⚠️ activatePendingDelegation НЕ вызывается здесь —
// делегирование активируется только при полном
// одобрении, см. ниже
// --- nextStep == nil: заявка ПОЛНОСТЬЮ одобрена ---
approvedStatus := "approved"
approval.Status = &approvedStatus
UpdateApprovalDB(ctx, nil, approval)
// 🆕 Активация делегирования — см. §10.5
activatePendingDelegation(ctx, approval)
lastStepIsDelegate := currentStep.ApproverID != nil &&
*currentStep.ApproverID != actualExecutorID
// ^ используется для формулировки уведомлений: "решение принято
// делегатом Х вместо назначенного Y" против обычного "одобрено Y"
createTasksForApproval(ctx, bot, approval) // §14, ApprovalTopicExecutor → ApprovalTask
уведомление инициатору (одобрено)
уведомления директорам, подписанным на категорию
уведомления наблюдателям
}
14. Дополнительные согласующие (add_approver.go)
14.1. HandleAddApproverFromApproval(bot, chatID, telegramID, approvalID)
- Проверяет права добавляющего (должен сам быть согласующим этой заявки — основным или дополнительным).
- Показывает список кандидатов, исключая уже назначенных (и основных, и дополнительных).
14.2. getApproverCategoriesWithPendingApprovals
Было: проверяло только topic-scoped иерархию (пропускало правила, заданные на уровне категории или глобально), и если у пользователя была хоть одна pending-запись где угодно — ошибочно помечало сразу все категории как имеющие ожидающие заявки (некорректная агрегация).
Стало: переписано по образцу соседней рабочей функции getPendingApprovalsByCategory — категории определяются напрямую из фактических pending-шагов пользователя (ApprovalStep + ApprovalAdditionalApprover со статусом pending, у которых сама заявка ещё pending), без обращения к иерархии вообще — иерархия для этой задачи была лишним, ошибочным уровнем косвенности.
15. Задачи исполнителей (tasks.go)
Полный список хендлеров и их назначение:
| Функция | Назначение |
|---|---|
HandleMyTasks |
Список pending-задач текущего исполнителя (проверка роли через IsExecutorDB) |
sendTaskCard |
Карточка одной задачи: инициатор, тема, текст заявки, дата, текст задачи, прикреплённые именно к этой задаче файлы (TaskID совпадает), секция комментариев |
HandleCompleteTask |
Проверка RequireFile (если файл обязателен и не прикреплён — блокирует завершение, просит прикрепить), Status → "completed", CompletedAt = now, activateNextTask, уведомление инициатору (включая текст самой задачи — 🩹 см. §22 последняя строка) |
activateNextTask |
Следующая задача этой же заявки с StepOrder = завершённая.StepOrder + 1 и статусом waiting → переводит в pending, уведомляет её исполнителя |
handleRevokeCancellationTask |
Вызывается при отзыве заявки: отменяет pending/waiting ApprovalStep/ApprovalAdditionalApprover; по каждой задаче — если completed → createReversalTask, если pending → askExecutorAboutChanges, если waiting → просто cancelled с уведомлением |
createReversalTask |
Новая задача «Обратить изменения по заявке #N», тот же исполнитель, Status = pending сразу |
askExecutorAboutChanges |
Вопрос исполнителю Да/Нет «вносили ли уже изменения» через BuildRevokeTaskConfirmation |
HandleRevokeTaskNoChanges / HandleRevokeTaskHasChanges |
Обработка ответа: первое — просто cancelled; второе — создаёт reversal-задачу (§ выше) |
HandleExecutorAttachStart |
Начало сессии прикрепления файла к задаче (state.AttachmentSession) |
16. Отзыв заявок
HandleRevokeWithComment— точка входа с комментарием причины отзыва.- Правила, кто может отозвать:
pending/revision— инициатор или директор.approved, отзывает не-директор-инициатор —startApprovalRevocationRequest— отдельный флоу запроса согласия у всех, кто участвовал в согласованииapproved, отзывает директор — сразу, без запроса согласия.
performActualRevoke— единственное место, гдеApproval.Statusреально становится"revoked":approval.Status = "revoked"; RevokedByID = revoker.ID; RevokedAt = nowUpdateApprovalDBdeactivatePendingDelegation(ctx, approval) // 🆕 §10.5handleRevokeCancellationTask(bot, ctx, approval) // §15уведомления инициатору, директорам, согласующим, наблюдателям
17. Уведомления
Список типов уведомлений:
| # | Кому | Когда | Откуда |
|---|---|---|---|
| 1 | Согласующему первой группы (или его делегату) | Заявка создана | submitApproval |
| 2 | Инициатору | Заявка отправлена (подтверждение) | submitApproval |
| 3 | Контролёрам категории | Заявка создана | submitApproval |
| 4 | Следующему согласующему (или делегату) | Предыдущий шаг одобрен, есть следующий | processNextStepOrComplete |
| 5 | Инициатору | Заявка полностью одобрена | processNextStepOrComplete |
| 6 | Директорам, подписанным на категорию | Заявка полностью одобрена | processNextStepOrComplete |
| 7 | Наблюдателям | Заявка полностью одобрена | processNextStepOrComplete |
| 8 | Инициатору, наблюдателям, директорам | Заявка отклонена | handleDecision (rejected-ветки) |
| 9 | Остальным согласующим той же any_of-группы | Их участие больше не требуется | handleDecision |
| 10 | Исполнителю следующей задачи | Предыдущая задача выполнена | activateNextTask |
| 11 | Инициатору | Исполнитель завершил задачу (включая текст задачи) | HandleCompleteTask |
| 12 | Исполнителю | Заявка отозвана — просьба обратить/подтвердить изменения | handleRevokeCancellationTask |
| 13 | Инициатору, директорам, согласующим, наблюдателям | Заявка отозвана | performActualRevoke |
18. Устойчивость бота
buildHTTPClient— сборкаhttp.Clientдля Telegram Bot API, подключаетCustomAPIProxyTransport, если заданproxyCfg(🩹 ранее конфиг прокси передавался, но фактически не использовался при сборке транспорта — исправлено).- Ретраи на старте — при недоступности Telegram API во время запуска бот повторяет попытку подключения вместо немедленного падения.
- Отдельный
proxy/main.go— вынесенный прокси-сервис перед реальным Telegram Bot API, с собственной логикой ретраев на запросы (doWithRetry— упоминается как существующий, детали реализации не входили в присланные файлы).
19. scheduler/daily_reminder.go — построчно
19.1. Start()
func (dr *DailyReminder) Start() {
go dr.sendReminders() // 🧪 см. журнал — временная строка для
// тестирования, должна быть удалена после
// проверки в проде (немедленный запуск при
// старте сервиса, а не ожидание 9:00 МСК)
go dr.scheduleLoop()
}
19.2. scheduleLoop()
Бесконечный цикл: вычисляет ближайшие 9:00 по Europe/Moscow (fallback — фиксированная зона UTC+3, если LoadLocation не удался), переносит на понедельник, если выпадает на субботу/воскресенье (skipWeekends), спит до этого момента, вызывает sendReminders(), повторяет.
19.3. sendReminders()
- Получает всех пользователей (
GetUsersListDB, без фильтра — все, в т.ч. потенциально заблокированные/уволенные — фильтрация по пригодности отправки происходит ниже, по наличиюTelegramID). - Один раз на всю рассылку —
getAbsentEmployeesToday(§19.4). - Для каждого пользователя с непустым
TelegramID—sendReminderToUser. - 🧪 Временный фильтр по конкретному
telegramID— использовался для тестирования на одном аккаунте без риска разослать всем; должен быть удалён из продовой версии.
19.4. getAbsentEmployeesToday(ctx, mskLocation) []AbsentEmployeeInfo
type AbsentEmployeeInfo struct {
Name string
TimeDescription string
Contact string // делегат, если есть активное делегирование, иначе сам сотрудник
}
GetApprovalsListDB(Filtering: {Status: "approved"})— все одобренные заявки компании одним запросом (не по пользователю — так один раз на всю рассылку, а не N запросов).- Для каждой заявки:
- Пропуск, если
Metadata/Contentпусты, илиmetadata.extra.absence_typeпуст (не заявка на отсутствие — поле есть только у отгула/отпуска/больничного), или инициатор уже встречен ранее (дедупликация — сотрудник с несколькими пересекающимися заявками попадёт в сводку один раз). parseAbsenceDates(content)— все совпадения regex(\d{1,2})[.,](\d{1,2})\.(\d{4}). Если ни одной даты не найдено — warning в лог с ID заявки и содержимым, пропуск.absenceCoversDate(dates, content, today)— если дат ровно 2 и в тексте есть дефис — трактует как диапазон (с меньшей по большую дату, независимо от порядка написания); иначе — сверяет точное совпадение с любой из распознанных дат (список отдельных дней).- Формулировка
TimeDescriptionпоabsence_type:timeoff+time_type == "partial"→ парсит время через(\d{1,2}):(\d{2}).*?(\d{1,2}):(\d{2})(строго через двоеточие, не точку — специально, чтобы не путать с датами), формат"отгул с HH:MM до HH:MM"; при неудаче парсинга времени —"отгул весь день".timeoff(не partial) →"отгул весь день".vacation→"отпуск до " + maxDate(dates).sickleave→"больничный до " + maxDate(dates).
Contact— поиск активныхApprovalDelegationпоDelegatorID = InitiatorID,IsActive = true, с дополнительной проверкойValidUntil(подстраховка на случай, если фоновая задача деактивации по времени ещё не отработала) — первая подходящая даёт имя делегата, иначе — имя самого сотрудника.
- Пропуск, если
19.5. sendReminderToUser
Считает 4 персональных счётчика (доработки/задачи/согласования, плюс неявный «есть ли вообще что показать»), формирует текст, добавляет блок «Сотрудники в отгуле/больничном/отпуске» если absentToday непуст. Отправляет, если есть хоть один личный пункт или список отсутствующих непуст (осознанное решение — раньше отправляли только при личных пунктах).
20. Каталог callback_data
| Префикс/формат | Обработчик | Назначение |
|---|---|---|
appr:{topicCode}:{nodeKey} |
HandleCallback |
Переход по дереву |
appr_back:{topicCode} |
HandleBackCallback |
Кнопка «Назад» |
appr_decision:{approvalID}:1|0 |
handleDecision (через диспетчер) |
Одобрить/отклонить |
appr_approver_sel:{userID} |
HandleApproverSelectionCallback |
Выбор согласующего вручную |
appr_approver_pg:{page} |
HandleApproverSelectionPageCallback |
Пагинация списка согласующих |
appr_obs_toggle:{userID} |
HandleObserverToggleCallback |
Toggle наблюдателя |
appr_obs_pg:{page} |
HandleObserverPageCallback |
Пагинация наблюдателей |
appr_obs_confirm |
HandleObserverConfirmCallback |
Подтверждение выбора наблюдателей |
appr_deleg_yes / appr_deleg_no |
HandleDelegationChoiceCallback |
🆕 Да/Нет делегирования |
appr_deleg_sel:{userID} |
HandleDelegateSelectionCallback |
🆕 Выбор делегата |
appr_deleg_pg:{page} |
HandleDelegateSelectionPageCallback |
🆕 Пагинация делегатов |
add_appr_to_approval:{approvalID} |
HandleAddApproverFromApproval |
Добавление доп. согласующего (кнопка сейчас закомментирована) |
revoke_task_no_changes:{taskID}:{approvalID} |
HandleRevokeTaskNoChanges |
Подтверждение отсутствия изменений при отзыве |
revoke_task_has_changes:{taskID}:{approvalID} |
HandleRevokeTaskHasChanges |
Подтверждение наличия изменений |
⚠️ Все ключи держатся в пределах 64 байт вручную (лимит Telegram) — особенно важно для Key узлов дерева (§2.3), которые входят в appr:{topicCode}:{nodeKey}.
21. Каталог ключей Redis
| Ключ | Формат/TTL | Назначение |
|---|---|---|
approval_flow:{telegramID} |
JSON ApprovalFlowState, TTL 30 мин |
Состояние диалога создания заявки |
approval_flow:version |
Произвольное значение, без TTL | Триггер перезагрузки Registry (опрос раз в 5 сек) |
appr_approve_comment_pending:{telegramID} |
approvalID строкой |
Ожидание обязательного комментария к одобрению |
(без точного имени) — AddApproverFlowState, AttachmentSession, comment-flow состояния |
JSON, управляются через StateManager |
Прочие второстепенные диалоговые состояния (§4.2) |
22. Полный журнал багов с кодом до/после
22.1. handleDecision: тема не найдена для timeoff/vacation/sickleave
Симптом: при попытке согласующего принять решение по заявке категории «Отгул»/«Отпуск»/«Больничный» — ошибка «unknown topic code».
Причина: topicCodeToCategory в Registry инициализировалась пустой картой, алиасы никогда не заполнялись.
Исправление — в NewRegistry:
r := &Registry{
ipc: ipc, topics: make(...), order: nil,
topicCodeToCategory: map[string]string{
"timeoff": "absence",
"vacation": "absence",
"sickleave": "absence",
},
}
22.2. 64-байтный лимит callback_data — падение конструктора/бота
Причина: r.Route("/api/approvals/{id}", ...) в chi перекрывал уже существующий GET-хендлер базового пути /api/approvals — новые под-маршруты (/comments, /additional-approvers и т.п.) логически переносили туда весь роутинг, ломая существующий список заявок.
Исправление: заменено на r.Group(...) с полными абсолютными путями вместо вложенного r.Route.
22.3. Заявка «зависает» после одобрения доп. согласующим
Причина: ветка доп. согласующего в handleDecision искала «свой» шаг через findStepForCurrentApprover(ctx, approvalID, currentUserID) — эта функция ищет запись в ApprovalStep, а у доп. согласующего такой записи никогда не было (он есть только в ApprovalAdditionalApprover) → всегда nil → заявка молча переставала двигаться дальше после его одобрения, при этом никакой ошибки не логировалось.
Было:
step := findStepForCurrentApprover(ctx, approvalID, currentUserID)
processNextStepOrComplete(ctx, bot, approval, step, currentUserID)
// step == nil здесь всегда для доп. согласующего
Стало: переиспользование уже корректно полученного mainStep (результат GetActiveStepDB, взятого в начале функции, см. §12.1):
processNextStepOrComplete(ctx, bot, approval, mainStep, currentUserID)
Заодно исправлен передаваемый actualExecutorID — раньше туда уходил ID самого доп. согласующего, из-за чего в processNextStepOrComplete (lastStepIsDelegate проверка, §13) он ошибочно трактовался как «делегат основного согласующего», хотя это два независимых, не связанных друг с другом человека.
22.4. Прокси бота фактически не использовался
Причина: buildHTTPClient принимал proxyCfg аргументом, но не подключал его к транспорту http.Client.
Исправление: подключён CustomAPIProxyTransport, добавлены ретраи на подключение при старте.
22.5. getApproverCategoriesWithPendingApprovals — неверные категории
См. §14.2 — полностью переписана функция.
22.6. Уведомление о выполненной задаче не содержало текст задачи
Было: сообщение инициатору формировалось только из approval.Content (текст всей заявки).
Стало: добавлена отдельная строка 🎯 Задача: {task.TaskText} — конкретно то, что исполнитель фактически делал.
22.7. DynamicTopic — подписи {{.key_label}} только для захардкоженных ключей
Причина: addLabelMappings содержала фиксированный список соответствий «ключ+значение → подпись», не расширяемый без правки кода.
Исправление: добавлен dataValueLabels, заполняемый при NewDynamicTopic из Label любого узла дерева с соответствующими DataKey/DataValue, и метод addTreeLabelMappings, вызываемый после addLabelMappings (приоритет — у дерева). Теперь переименование узла в конструкторе автоматически меняет и текст в шаблоне сообщения, без правки Go-кода.
Панель администратора
Техническая документация: веб-админка системы согласований
Оглавление
- Структура репозитория (package map)
- Точка входа и инициализация сервиса
- Слой авторизации (AccessCodeVerifier, JWTVerifier)
- Универсальная обёртка хендлеров (register.go)
- Каталог REST-эндпоинтов по пакетам
1. Структура репозитория (package map)
rest_admin_service/
├── cmd/
│ └── app.go — точка входа, инициализация роутера и сервера
├── docs/
│ ├── docs.go, swagger.json, swagger.yaml — сгенерированная Swagger-документация
├── handlers/
│ ├── admin/ — CRUD администраторов
│ ├── admin_actions/ — журнал действий администраторов (аудит-лог)
│ ├── approval_steps/ — шаги согласования заявок
│ ├── approval_tasks/ — задачи исполнителей по заявкам
│ ├── approvalextras/ — вложенные сущности заявки (доп. согласующие, комментарии, наблюдатели, задачи, запрос отзыва)
│ ├── approvals/
│ │ ├── handlers.go — CRUD заявок
│ │ ├── files.go — файлы заявок
│ │ ├── reports.go — выгрузка отчётов
│ ├── auth/ — аутентификация администратора
│ ├── dashboard/ — сводная статистика
│ ├── departments/ — CRUD отделов
│ ├── flow_nodes/
│ │ ├── handler.go — CRUD узлов дерева согласования
│ │ ├── preview.go — предпросмотр дерева
│ │ ├── router.go — регистрация маршрутов
│ │ ├── utils.go — bumpFlowVersion и вспомогательные функции
│ ├── middleware.go — JWTVerifier, AccessCodeVerifier
│ ├── notification/ — рассылка уведомлений
│ ├── permission/ — CRUD прав доступа
│ ├── profile/ — профиль текущего администратора
│ ├── register.go — обёртка хендлеров, единый формат ответа
│ ├── roles/ — CRUD ролей
│ ├── system_monitoring/ — статус systemd-юнитов, БД, Redis
│ ├── topic_categories/ — CRUD категорий тем
│ ├── topic_executors/ — CRUD исполнителей по темам
│ ├── topics/ — CRUD тем согласования
│ ├── user_hierarchy/ — CRUD иерархии согласующих
│ ├── user_roles/ — назначение ролей пользователям
│ └── users/ — список и карточка пользователей
├── types/
│ ├── module.go — InternalProviderControl (Config, Redis, Database, TGBotClient)
│ └── req.go — LimitRequest, Response
├── go.mod, go.sum
└── version
Все пакеты handlers/* следуют единому паттерну: структура <Name>Reg с полем ipc *typesm.InternalProviderControl, метод Register<Name>Routes(r chi.Router, ipc *typesm.InternalProviderControl) error, набор Handler-методов с сигнатурой func(w http.ResponseWriter, r *http.Request) (interface{}, error).
2. Точка входа и инициализация сервиса
cmd/app.go — единственная точка запуска сервиса.
Последовательность инициализации (initService):
getClientsExternal()— загружает конфигурацию черезconfigcore.ConfigLoadOptionsс флагамиRedis,Database,Telegram,Secrets.AuthJWT.Admin,Secrets.AdminAccessCode,Server.RESTAdminService.initInternalProvider()— собираетtypesm.InternalProviderControl(Config, RedisClient, Database, TGBotClient) из внешних клиентов.initBaseApiRouter()— создаётchi.Mux, подключает middleware, регистрирует маршруты черезregisterRoutes().startRestApiServer()— поднимаетhttp.Serverна порту изConfig.Server.RESTAdminService.PortRest.
Таймауты сервера: ReadTimeout, WriteTimeout, IdleTimeout — по 15 минут каждый. Лимит запросов: 15 запросов в секунду на IP (httprate.LimitByIP).
Порядок глобальных middleware в initBaseApiRouter:
cors.Handler— allowed origins из конфига, методы GET/POST/PUT/DELETE/PATCH/OPTIONS, заголовкиAccept, Authorization, Content-Type, ApiKey, x-device-id.middleware.RealIPmiddleware.Recovererhttprate.LimitByIP(15, 1s)
Перед регистрацией остальных маршрутов проверяется ipc.Database != nil — при отсутствии подключения к БД роутер не создаётся, сервис не запускается.
Swagger UI подключён отдельно от общей цепочки middleware по пути /swagger/* (без авторизации).
3. Слой авторизации
Каждый защищённый маршрут проходит две независимые проверки, применяемые как middleware группы r.Route(...): сначала AccessCodeVerifier, затем JWTVerifier. Порядок фиксирован во всех пакетах.
3.1. AccessCodeVerifier — проверка клиента (handlers.AccessCodeVerifier)
Назначение — верифицировать, что запрос пришёл от легитимного фронтенда (HMAC-подпись), независимо от того, авторизован ли конкретный пользователь.
Требуемые заголовки:
ApiKey— HMAC-подпись.X-Timestamp— unix-время запроса.x-device-id— идентификатор устройства (используется в этом middleware только для логирования, не для проверки).
Шаги проверки:
- Конфигурация должна содержать
Secrets.ApiPublicAdminиSecrets.ApiPrivateAdmin— иначе 500. - Оба заголовка (
ApiKey,X-Timestamp) обязательны — иначе 401. X-Timestampпарсится как Unix-время.- Время округляется до минуты (и текущее, и полученное).
- Разрешённое окно — не более 3 минут в прошлом от округлённого текущего момента, и не в будущем — иначе 401 (
Timestamp expired/Timestamp in future). - Ожидаемая подпись вычисляется как
GenerateHMACSignature(ApiPublicAdmin, timestampStr, ApiPrivateAdmin)и сравнивается сApiKeyчерезHmacEquals(защита от timing-атак) — при несовпадении 401Unauthorized client. - При успехе публичный ключ кладётся в контекст (
securecore.AccessContextKey).
3.2. JWTVerifier — проверка администратора (handlers.JWTVerifier)
Назначение — определить, какой именно администратор выполняет запрос, и подтвердить, что его сессия и устройство актуальны.
Требуемые заголовки:
Шаги проверки:
Secrets.AuthJWT.AdminSecretдолжен быть задан — иначе 500.- Заголовок
Authorizationобязателен, формат строгоBearer <token>— иначе 401. x-device-idобязателен — иначе 401Unauthorized device.- Redis-клиент должен быть инициализирован — иначе 500.
- Токен верифицируется (
securecore.VerifyToken), из claims извлекаетсяuser_id. - Администратор ищется в БД по ID с фильтром
IsBlocked = false; отсутствие записи или блокировка — 401. - В Redis по ключу
{RedisPathUserAuthSessions}:admin:{userID}ищется JSON-сессия (typescore.UsersSessions) — отсутствие ключа означает, что сессия истекла или отозвана — 401User session not found. Please authorize again. - Сессия должна содержать переданный
x-device-idв спискеAuthorizedDevices— иначе 401Unauthorized device(устройство было деавторизовано, например, при смене пароля или ручном отзыве сессий). - При успехе
user_idкладётся в контекст (securecore.IDContextKey) и используется далее хендлерами черезhandlers.GetIDFromContext(ctx)— в частности, для записи автора действия в аудит-лог (admin_actions).
Инвариант: сессия администратора хранится в Redis, а не только подписывается в JWT — это позволяет принудительно отзывать доступ (блокировка, смена пароля, logout всех устройств) без ожидания истечения токена.
4. Универсальная обёртка хендлеров (register.go)
Все хендлеры имеют единую сигнатуру func(w http.ResponseWriter, r *http.Request) (interface{}, error) и регистрируются через handlers.RegisterRoute(r, method, path, handlerFunc), которая оборачивает их в WrapHandlerF.
4.1. WrapHandlerF
- Оборачивает
http.ResponseWriterвresponseWriterWrapper, предотвращающий повторный вызовWriteHeader(защита от паники при двойной записи статуса). - Вызывает хендлер, получает
(payload, err). - Если
payloadимеет тип*ExcelFileResponseиerr == nil— отдаёт бинарный файл черезserveExcelFile(используется для выгрузки отчётов и файлов заявок), минуя JSON-сериализацию. - Иначе — сериализует ответ в JSON через
respondWithJSON.
4.2. Формат JSON-ответа
Единая структура types.Response:
{
"total_count": 0,
"count": 0,
"error": "текст ошибки",
"data": {}
}
При ошибке хендлера ответ всегда 500 Internal Server Error с телом ErrorResponse{ErrorCode, ErrorDescription} — коды ошибок доменного уровня (400/403/404) в текущей реализации respondWithJSON не различаются; хендлеры возвращают errors.New(...), транслируемый в 500 независимо от смысловой причины отказа. Это единообразное, но не RESTful поведение стоит учитывать при интеграции фронтенда — различать причины ошибок можно только по тексту error_description.
4.3. ExcelFileResponse
Специальный тип для потоковой отдачи бинарных файлов (выгрузка отчётов по заявкам, скачивание вложений):
type ExcelFileResponse struct {
Data []byte
FileName string
}
serveExcelFile выставляет заголовки Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, Content-Disposition: attachment; filename="...", Content-Length, Cache-Control: no-cache, no-store, must-revalidate.
5. Каталог REST-пакетов
Все пакеты используют единый паттерн: <Name>Reg{ipc}, Register<Name>Routes, middleware-цепочка AccessCodeVerifier → JWTVerifier, CRUD-хендлеры через handlers.RegisterRoute. Ниже — назначение каждого пакета и один характерный маршрут.
| Пакет | Назначение | Пример маршрута |
|---|---|---|
admin |
CRUD администраторов панели | GET/POST/PUT/DELETE /api/admin |
admin_actions |
Журнал действий администраторов (аудит-лог), пишется другими пакетами через LogAdminActionWithMetadata |
GET /api/admin-logs, DELETE /api/admin-logs/{id} |
approval_steps |
Шаги согласования конкретной заявки | GET /api/approval-steps/by-approval/{approval_id} |
approval_tasks |
Задачи исполнителей, включая файлы, прикреплённые к задаче | GET /api/approval-tasks/{id}/files/{file_id}/download (скачивание) |
approvalextras |
Вложенные сущности заявки: доп. согласующие, комментарии, наблюдатели, запрос на отзыв, задачи — все смонтированы на /api/approvals/{id}/... через r.Group, а не вложенный r.Route (см. §22.2 бот-документации — та же проблема 64-байтного лимита здесь неактуальна, но паттерн монтирования идентичен) |
GET /api/approvals/{id}/additional-approvers |
approvals |
CRUD заявок, файлы заявки, отчёты (просмотр и скачивание Excel) | GET /api/approvals/report/download |
auth |
Авторизация администратора (логин/пароль, обмен токена), публичный /api/time для синхронизации часов клиента |
POST /api/auth/login |
dashboard |
Агрегированная статистика: счётчики, разбивка по категориям, узкие места по согласующим, отсутствующие сегодня сотрудники, предупреждения о некорректной настройке | GET /api/dashboard/stats |
departments |
CRUD отделов | GET /api/departments |
flow_nodes |
CRUD узлов дерева согласования, сохранение дерева целиком, перемещение/дублирование узла, предпросмотр, ручная перезагрузка бота | POST /api/flow-nodes/category/{category_id}/tree |
notification |
Массовая рассылка сообщений через Telegram-бота по фильтрам получателей | POST /api/notifications |
permission |
CRUD прав доступа (роль × категория/тема × действие) | GET /api/permissions/by-role/{role_id} |
profile |
Просмотр/изменение профиля текущего администратора | GET/PUT /api/admin/profile |
roles |
CRUD ролей | GET /api/roles |
system_monitoring |
Статус systemd-юнитов, БД, Redis | GET /api/monitoring |
topic_categories |
CRUD категорий тем | GET /api/categories |
topic_executors |
CRUD исполнителей по темам | GET /api/topic-executors/by-topic/{topic_id} |
topics |
CRUD тем согласования | GET /api/topics |
user_hierarchy |
CRUD иерархии согласующих | GET /api/hierarchies |
user_roles |
Назначение/снятие ролей пользователю | — |
users |
Список и карточка пользователей Telegram-бота (не администраторов) | GET /api/users/{id} |
Все списковые (GetXListHandler) эндпоинты возвращают types.Response{data, count, total_count} и, как правило, поддерживают фильтрацию и пагинацию через typescore.ListDbOptions. Мутирующие операции (создание/изменение/удаление значимых сущностей — администраторы, отделы, роли, права, темы, категории, пользователи) фиксируются в аудит-логе через admin_actions.LogAdminActionWithMetadata с указанием исполнителя, типа действия, целевой сущности и снапшотом изменённых полей.
6. Веб-панель
6.1. Стек и библиотеки
| Категория | Библиотека | Назначение |
|---|---|---|
| Фреймворк | React 18 + TypeScript, сборка Vite | SPA |
| Роутинг | react-router-dom (BrowserRouter, Routes) |
клиентская маршрутизация, защищённые роуты |
| UI-кит | @mantine/core, @mantine/hooks, @mantine/form, @mantine/notifications, @mantine/modals, @mantine/dates |
компоненты интерфейса, формы, тосты, модалки, выбор дат |
| Работа с сервером | @tanstack/react-query |
кэш, инвалидация, useQuery/useMutation во всех features/*/hooks |
| HTTP-клиент | axios (обёрнут в src/api/client.ts) |
единая точка вызовов REST API |
| Подпись запросов | crypto-js (HmacSHA256) |
генерация ApiKey-заголовка на клиенте, зеркалит AccessCodeVerifier бэкенда |
| Даты | dayjs (+ плагины utc, timezone) |
форматирование и расчёт дат |
| Графики | recharts |
графики на дашборде |
| Drag-and-drop | @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities |
сортировка узлов дерева согласования (SortableFlowTree) |
| Прочее | crypto.randomUUID() (браузерный API) |
генерация device_id |
6.2. Структура репозитория
src/
├── App.tsx — дерево маршрутов приложения
├── main.tsx — точка входа: провайдеры (Router, QueryClient, MantineProvider, ModalsProvider, Notifications, ErrorBoundary)
├── theme.ts — тема Mantine
├── api/ — по одному файлу на REST-ресурс, тонкие обёртки над apiClient
│ ├── client.ts — настройка axios: HMAC-подпись, JWT, device_id, синхронизация времени
│ ├── auth.ts, time.ts, dashboard.ts, users.ts, departments.ts, roles.ts,
│ │ permissions.ts, admins.ts, adminLogs.ts, categories.ts, topics.ts,
│ │ topicExecutors.ts, hierarchies.ts, flowNodes.ts, approvals.ts,
│ │ approvalTasks.ts, notifications.ts, userRoles.ts
├── features/ — бизнес-логика по доменам, отделена от страниц
│ └── <domain>/
│ ├── hooks/ — useQuery/useMutation-хуки конкретного домена
│ ├── components/ — доменные компоненты, переиспользуемые между страницами (опционально)
│ └── utils.ts — доменные хелперы (опционально)
│ Домены: adminLogs, admins, approvalTasks, approvals, categories,
│ departments, flowNodes, hierarchies, notifications, permissions,
│ roles, topicExecutors, topics, users
├── pages/ — страницы, привязанные к маршрутам
│ └── <domain>/
│ ├── <Domain>Page.tsx
│ └── components/ — модалки и виджеты, специфичные для конкретной страницы
├── components/ — общие сквозные компоненты
│ ├── Layout/ (Header, MainLayout, Sidebar)
│ ├── DiagnosticErrorBoundary.tsx
│ ├── CopyRolesModal.tsx, TopicGapsModal.tsx, UserEditModal.tsx
├── hooks/
│ └── useAuth.ts — состояние аутентификации на уровне приложения
├── types/
│ └── api.ts — полная модель данных (TypeScript-интерфейсы)
├── utils/
│ └── date.ts
└── vite-env.d.ts
6.3. Страницы (маршруты)
Маршрутизация определена в App.tsx. Все страницы, кроме /login, обёрнуты в ProtectedRoute (редирект на /login, если useAuth().isAuthenticated === false) и общий MainLayout (шапка + боковое меню).
| Маршрут | Страница | Назначение |
|---|---|---|
/login |
Login |
форма входа по логину/паролю |
/ |
Placeholder |
заглушка главной страницы («в разработке») |
/notifications |
NotificationsPage |
отправка и история Telegram-рассылок |
/approval-dash |
Dashboard |
сводная статистика по заявкам, графики, узкие места, предупреждения о настройке |
/nodes |
FlowTreePage |
редактор дерева согласования по категориям |
/guide/flow-trees |
FlowTreeGuidePage |
справочная страница по работе с деревом согласования |
/users |
UsersPage |
список сотрудников Telegram-бота |
/departments |
DepartmentsPage |
CRUD отделов |
/roles |
RolesPage |
CRUD ролей |
/approvals |
ApprovalsPage |
список и карточка заявок, просмотр файлов |
/permissions |
PermissionsPage |
CRUD прав доступа роль/категория/тема |
/categories |
CategoriesPage |
CRUD категорий тем |
/topics |
TopicsPage |
CRUD тем согласования |
/executors |
TopicExecutorsPage |
назначение исполнителей задач по темам |
/hierarchies |
HierarchiesPage |
иерархия согласующих, визуализация и дублирование цепочек |
/settings |
SettingsPage |
настройки |
/admins |
AdminsPage |
CRUD администраторов панели |
/logs |
AdminLogsPage |
просмотр аудит-лога действий администраторов |
/approval-tasks |
ApprovalTasksPage |
задачи исполнителей по заявкам |
6.4. Полная модель данных (src/types/api.ts)
TypeScript-интерфейсы зеркалят Go-структуры бэкенда (см. §2 бот-документации), в snake_case, соответствующем JSON-ответам API.
User
interface User {
id?: number; telegram_id?: number; username?: string; full_name: string
language_code?: string; department_id?: number
is_blocked?: boolean; is_fired?: boolean; blocked_at?: string
last_login?: string; created_at?: string; updated_at?: string
department?: Department; user_roles?: UserRole[]
}
Department
interface Department {
id?: number; name: string; code: string; description?: string
is_active?: boolean; created_at?: string; updated_at?: string
}
Role
interface Role {
id?: number; name: string; code: string
is_default?: boolean; is_director?: boolean; is_approver?: boolean
is_controller?: boolean; is_observer?: boolean; is_executor?: boolean
description?: string; created_at?: string; updated_at?: string
}
Флаги is_director/is_approver/is_controller/is_observer/is_executor определяют функциональную роль в процессе согласования — используются, в частности, при подборе получателей уведомлений (TargetFilters.role_type_flags) и первого согласующего по роли (first_approver_role_code в ApprovalTopic).
UserRole — связка пользователь↔роль: { id?, user_id, role_id, created_at?, updated_at?, role?: Role }.
Admin — учётная запись администратора панели:
interface Admin {
id?: number; login: string; name?: string
is_root?: boolean; is_observer?: boolean
telegram_id?: number; telegram_user?: User
created_at?: string; last_login?: string; last_ip_login?: string
is_blocked?: boolean; password?: string
}
is_root — суперадминистратор; telegram_id/telegram_user — привязка учётки администратора к пользователю Telegram-бота (для уведомлений и связи ролей).
ApprovalTopicCategory — категория тем: { id?, name, code, description?, is_active?, created_at?, updated_at?, topics?: ApprovalTopic[] }.
ApprovalTopic — тема согласования (полное соответствие Go-структуре ApprovalTopic из документации бота, §2.2):
interface ApprovalTopic {
id?: number; category_id?: number; name: string; code: string
approval_mode?: string // sequential | any_of
duration_type?: string; time_type?: string; payment_type?: string
template?: string; first_approver_role_code?: string
allows_file?: boolean; allow_approver_selection?: boolean
allow_observer_selection?: boolean; allow_delegation_selection?: boolean
require_approver_comment?: boolean; requires_file?: boolean
require_observer_selection?: boolean; is_active?: boolean
instructions?: string; created_at?: string; updated_at?: string
category?: ApprovalTopicCategory
}
Approval — заявка:
interface Approval {
id?: number; initiator_id?: number; topic_id?: number; department_id?: number
content?: string; status?: string; metadata?: string
revoked_by_id?: number; revoked_at?: string
revision_comment?: string; revision_by_id?: number; revision_at?: string; revision_count?: number
created_at?: string; updated_at?: string
files?: ApprovalFile[]; steps?: ApprovalStep[]
initiator?: User; topic?: ApprovalTopic; department?: Department; revoked_by?: User
}
ApprovalStep — шаг согласования: { id?, approval_id?, approver_id?, step_order?, status?, comment?, approver_name_snapshot?, approver_department_snapshot?, acted_at?, returned_at?, created_at?, updated_at?, approver?: User }.
ApprovalFile — файл заявки или задачи: { id?, approval_id?, task_id?, file_base64?, file_name?, attached_by_id?, created_at? }.
ApprovalTask — задача исполнителя: { id?, approval_id?, executor_id?, task_text?, status?, step_order?, require_file?, topic_executor_id?, completed_at?, created_at?, updated_at?, approval?: Approval, executor?: User, files?: ApprovalFile[] }.
ApprovalTopicExecutor — правило назначения исполнителя по теме: { id?, topic_id?, user_id?, step_order?, require_file?, task_text?, is_active?, target_system?, created_at?, updated_at?, topic?: ApprovalTopic, user?: User }.
ApprovalFlowNode — узел дерева согласования (полное соответствие ApprovalFlowNode из документации бота, §2.3):
interface ApprovalFlowNode {
id?: number; topic_id?: number; parent_id?: number | null
key: string; category_id: number
message_template?: string | null; code_template?: string | null
label: string
data_key?: string | null; data_value?: string | null
is_input?: boolean; input_key?: string | null; input_hint?: string | null
is_multi_select?: boolean; multi_select_key?: string | null; multi_select_value?: string | null
topic_code?: string | null; sort_order?: number
created_at?: string; updated_at?: string
children?: ApprovalFlowNode[]
}
Permission — право доступа: { id?, role_id?, topic_category_id?: number | null, topic_id?: number | null, action?, created_at?, updated_at?, role?: Role, topic_category?: ApprovalTopicCategory, topic?: ApprovalTopic }. Взаимоисключающая область действия (категория / тема / глобально) идентична паттерну UserHierarchy бэкенда.
UserHierarchy — правило иерархии согласующих: { id?, subordinate_id?, approver_id?, department_id?, topic_category_id?: number | null, topic_id?: number | null, priority?, target_system?: string | null, created_at?, updated_at?, subordinate?: User, approver?: User, department?: Department }.
Notification / NotificationRecipient / TargetFilters — рассылка уведомлений:
interface TargetFilters {
user_ids?: number[]; role_ids?: number[]
role_type_flags?: string[] // director | approver | observer | executor | controller
department_ids?: number[]
}
interface Notification {
id?: number; title: string; message_html: string
photo_base64?: string; photo_file_name?: string
target_filters?: string // сериализованный JSON TargetFilters
status?: string // pending | sending | completed | failed
total_count?: number; sent_count?: number; failed_count?: number
created_by_id?: number; created_at?: string; updated_at?: string; sent_at?: string
recipients?: NotificationRecipient[]
}
interface NotificationRecipient {
id?: number; notification_id?: number; user_id?: number
status?: string // pending | sent | failed
error_message?: string; telegram_message_id?: number
sent_at?: string; created_at?: string; user?: User
}
Служебные типы: ApiResponse<T> = { data: T; total_count?: number }, LoginRequest = { login, password }, LoginResponse = { token }.
6.5. Управление состоянием (State Management)
Отдельного глобального стора (Redux/Zustand/MobX) в проекте нет. Состояние разделено по назначению:
- Серверное состояние (данные API) — полностью на
@tanstack/react-query. Каждый домен вfeatures/<domain>/hooks/use<Domain>.tsинкапсулируетuseQuery(чтение списков/карточек) иuseMutation(создание/изменение/удаление) с автоматической инвалидацией кэша (queryClient.invalidateQueries) после успешной мутации и уведомлением через@mantine/notifications. ГлобальныйQueryClientсоздаётся один раз вmain.tsxсrefetchOnWindowFocus: falseиretry: 1. - Состояние аутентификации — локальный хук
useAuth()(src/hooks/useAuth.ts) наuseState/useEffect, без контекста верхнего уровня: при каждом вызове хука состояние (user,loading) создаётся заново на уровне компонента, использующегоuseAuth. Источник истины для факта авторизации — JWT вlocalStorage.token; профиль подгружается черезauthApi.getProfile()при монтировании, если токен присутствует. - Постоянное хранение на клиенте —
localStorage:token(JWT),device_id(UUID устройства, генерируется один раз черезcrypto.randomUUID()и переиспользуется),user(очищается при logout). - Локальное UI-состояние — стандартный
useState/useForm(@mantine/form) внутри страниц и модалок: значения форм, открытость модалок, выбранные строки таблиц и т. п. — не выносится выше компонента, которому принадлежит. - Кросс-компонентные уведомления и модалки — управляются через провайдеры Mantine (
ModalsProvider,Notifications), подключённые глобально вmain.tsx; вызываются императивно (notifications.show(...),modals.open(...)) без промежуточного стора.
6.6. API-клиент (src/api/client.ts)
Единый axios-инстанс с baseURL: '/api'. Request-интерцептор на каждый запрос:
Response-интерцептор:
- при
401с телом"Timestamp expired\n"— единожды (_retry) синхронизирует время с сервером, пересчитывает подпись и повторяет запрос; - при любом другом
401— очищаетtokenизlocalStorageи делаетwindow.location.href = '/login'(жёсткий редирект, минуя роутер).
Каждый файл в src/api/*.ts — тонкая типизированная обёртка над этим клиентом для конкретного ресурса (один файл на REST-пакет бэкенда).