Туториал. Проектное управление (ПУ) в 1Ф¶
Документ описывает функционал проектного управления на фронте 1Ф: архитектуру, пользовательские сценарии, механику планирования (Auto/Manual), версионирование (v2.268+) и критический путь.
1. Концепция¶
В 1Ф проект — это задача в категории с флагом IsForProjects. Проектные задачи живут в «проектной» категории (флаг IsForProjectTasks) либо во временной подкатегории планирования (ProjectTasksPlanningSubcat).
Визуализация делается через Bryntum Gantt 4.0.3 (vendor, spa/libs/bry-components/). 1Ф готовит данные, маршрутизирует экшены, добавляет кастомные колонки, тулбар и бизнес-логику поверх Bryntum.
Коробочный контур «Проектное управление» добавляет готовые категории (Проекты, Стадии, Backlog SLA, Backlog Agile, Статус-отчёты, Риски, Проблемы, Протоколы, Договорённости), автоматизации (расчёт паспорта, НЗП, бонус РП) и аналитические порталы.
Два контекста Ганта¶
| Контекст | Angular-компонент | Где открывается | Что можно делать |
|---|---|---|---|
| Проектный Гант (manage panel) | GanttComponent (vh-gantt), BryProjectComponent (новая версия, v2.268+) |
Из карточки задачи-проекта по кнопке «Проект», либо по прямой ссылке | Полное управление: создание/редактирование задач и связей, базовые планы, запуск в реализацию, версии, импорт из реальных задач |
| Гант подкатегории / подзадач | SubcategoryGanttComponent (vh-subcategory-gantt) |
Переключение вида в реестре категории на «Гант»; вкладка «Подзадачи» карточки | Просмотр + inline-редактирование (исполнитель, шаг, даты drag&resize, название), экспорт PDF/Excel |
Ключевое различие: проектный Гант оперирует виртуальным планом (задачи плана могут ещё не иметь соответствия в реальной категории), а Гант подкатегории показывает реальные задачи 1Ф.
2. Точки входа в ПУ¶
2.1. Из карточки задачи-проекта — кнопка «Проект»¶
В панели инструментов задачи (категория с типом «Для проектов») появляется иконка «Проект». Клик открывает проектный Гант во всю страницу.
2.2. По ДП-таблице проекта — кнопка «Проектный гант»¶
Если у категории настроен сквозной ДП-параметр, ссылающийся на ДП-таблицу проекта (ProjectReferenceTableExtParamId), и у задачи есть значение этого параметра — появляется доп. кнопка «Проектный гант». Права доступа — двухуровневые:
- доступ к задаче проекта есть → Гант полноценный
- доступа к задаче нет, но есть права на ДП-таблицу → Гант доступен для просмотра (read) или редактирования (write)
2.3. Прямая ссылка (readonly)¶
/spa/noframe/project-readonly/{taskId}?new=2 — зарегистрирован в pages-routes.prod.ts. Из карточки задачи тулбар-экшен useNewBryntumGanttReadonly открывает эту ссылку в модалке-iframe.
Что даёт readonly: визуально «заблокированные» поля, нельзя сохранять изменения. Используется, когда пользователь имеет право видеть проект, но не редактировать его (например, внешний РП клиента).
3. Интерфейс проектного Ганта¶
Левая панель — табличная часть (СДР)¶
Колонки (по умолчанию выделены жирным):
Наименование задачи · Маркеры (просрочка) · Дата начала · Дата окончания · Длительность · Календарных дней · % завершения · Категория · # Задача (связь с реальной задачей) · Трудозатраты · Назначенные ресурсы · Исполнители · Веха · Ручное планирование · Предшествующие/Последующие
Опционально доступны: Статус, СДР, Календарь, Тип ограничения, Дата ограничения, Крайний срок, Раннее/Позднее начало/окончание, Неактивна, Направление планирования, Режим трудозатрат, Показать на шкале, Общий временной резерв, Примечание, Сведение.
Правая панель — таймлайн¶
- Полосы задач: начало = дата старта, конец = дата окончания
- Белый фон — рабочие часы; серый — выходные/праздники из производственного календаря
- Красная вертикальная линия — «сегодня» (Bryntum feature
timeRanges) - Связи — стрелки между полосами (FS/SS/FF/SF)
- Baseline-полосы (если выбран базовый план) — дополнительные бары рядом с фактическими
Разделитель и персистентность¶
Ширина левой и правой области регулируется мышью. Порядок/видимость/ширина колонок, масштаб, положение разделителя между таблицей и таймлайном сохраняются per-пользователь per-проект.
Тулбар¶
| Элемент | Тип | Действие / метод |
|---|---|---|
| Создать задачу / подзадачу | button | добавление новой строки |
| Критический путь | toggle | Bryntum criticalPaths feature |
| Завершённые | toggle | показать/скрыть закрытые задачи |
| Тип планирования | combo | Auto (0) / Manual (1) — IPlanningType |
| Базовый план | combo | toggleBasePlan() → save settings + reload |
| Создать базовый план | button | GanttBasePlanModalComponent → addBasePlan() → POST /api/gantt/add-base-plan |
| Zoom + / Zoom − | button | ganttInstance.zoomIn/Out() |
| Запуск в реализацию | button | moveProjectTasks() |
| Сохранить | button | saveGanttData() / save() — маппит eventStore/dependencyStore |
| ⋮ (Ещё) | menu | импорт MS Project .mpp, экспорт PDF/Excel/XML, ссылка, настройки проекта |
| Копировать из проектных задач | menu | импорт данных из реальных задач (v2.268+) |
| Версии | toggle | Открывает панель VersionGrid (v2.268+) |
4. Пользовательские сценарии¶
4.1. Создание нового проекта¶
- Открыть категорию типа «Для проектов» (например,
Проектыиз коробки PM) - Создать задачу кнопкой «+», заполнить базовые ДП: тип проекта, компания, договор, плановые даты
- Сохранить карточку
- Нажать «Заняться» (переход в статус «Выполняется», с подписью)
- В тулбаре карточки нажать иконку «Проект» → открывается проектный Гант
Поведение: - Гант открывается пустым (задач нет) — только с маркером «сегодня» - Кнопка «Сохранить» серая, пока нет изменений - Тип планирования по умолчанию — Auto - Масштаб по умолчанию — «по масштабу проекта» (scroll к текущей дате)
4.2. Декомпозиция работ (СДР)¶
- Кнопка «Создать задачу» → новая строка в конец или после выбранной
- Ввести название в колонке «Наименование задачи»
- Выбрать строку → «Создать подзадачу» → появляется вложенный уровень
- Перетаскиванием строк менять иерархию/порядок
- Через контекстное меню: «Сделать подзадачей» / «Поднять над задачей»
Поведение:
- Родительская задача автоматически получает признак is-project (суммарная, жирная полоса)
- % завершения родителя рассчитывается только из статусов дочерних задач — вручную не редактируется
- Для конечной задачи без детей: завершена → 100%, отклонена → 0% (принудительно), не завершена → пусто
- Лимит: 10 000 задач в одном проекте
4.3. Установка связей между задачами¶
- В колонке «Предшествующие» у задачи-преемника указать номер строки предшественника
- Или на таймлайне — потянуть от края полосы одной задачи к краю другой
- Для изменения типа: двойной клик на стрелку связи → выбрать тип
Типы связей:
| Тип | Код Task2TaskLink | Поведение |
|---|---|---|
| Конец–Начало (FS) | 2 | B стартует после завершения A (по умолчанию) |
| Начало–Начало (SS) | 0 | Одновременный старт |
| Конец–Конец (FF) | 3 | Одновременное завершение |
| Начало–Конец (SF) | 1 | B завершается при старте A |
У каждой связи можно задать lag — задержку в днях, может быть отрицательной (FS с lag=−2 даёт overlap на 2 дня).
Поведение: - В Auto-планировании сдвиг предшественника каскадно двигает всех последователей - В Manual — связи только визуальные, даты не пересчитываются - «Старт проекта» (отдельное свойство) — опорная дата; первые задачи привязываются к ней - Наведение на стрелку — подсказка с типом связи
4.4. Назначение ресурсов и трудозатрат¶
Модель: - Ресурс — запись из справочника «Справочник ресурсов» (категория 1Ф) - Ресурс привязывается к задаче через ДП «Назначенные ресурсы» с процентом загрузки - Исполнитель задачи выбирается отдельно (колонка «Исполнители»)
Шаги: 1. В колонке «Назначенные ресурсы» — выпадающий список ресурсов 2. Задать процент загрузки (например, 50% = половина рабочего дня) 3. Колонка «Трудозатраты» рассчитается автоматически по календарю
Режимы трудозатрат (колонка «Режим»):
| Режим | Что фиксировано |
|---|---|
| Нормальный | — свободное пересогласование |
| Фиксированная длительность | length |
| Фиксированные единицы | % загрузки |
| Фиксированные трудозатраты | часы |
Нюанс: если задача на полный день (00:00–00:00 след. дня), при передаче ресурсов в реальные задачи трудозатраты пишутся только на дату начала, без растяжки на следующую дату.
4.5. Привязка к реальной задаче 1Ф (колонка «# Задача»)¶
Два способа: 1. Отправить в реальные задачи — иконка в тулбаре у выбранной строки → создаётся реальная задача в «Категории», установленной у строки; связь создаётся автоматически 2. Ввод номера вручную — в колонке «# Задача» указать существующий номер задачи 1Ф
Что происходит при установке связи (applyRealTaskDataToRecord):
- Из задачи подтягиваются: название (description → name), исполнители, статус, категория, фактические даты начала/окончания, срок
- Номер задачи превращается в гиперссылку
- Если задача отклонена (isRejected = true) — запись Ганта помечается inactive = true, percentDone = 0 и серым цветом, не учитывается в критическом пути
Валидация уникальности: в рамках одной версии проекта номер задачи не может быть привязан к двум разным строкам. Попытка дубля → строка сохраняется, но поле автоматически очищается.
Разрыв связи: очистить поле — реальная задача при этом не удаляется.
4.6. Базовые планы (baselines)¶
Создание:
1. Тулбар → «Создать базовый план»
2. В модалке GanttBasePlanModalComponent ввести название
3. Нажать «Сохранить»
Что происходит:
- SPA собирает текущие даты всех задач из Bryntum
- POST /api/gantt/add-base-plan с телом { projectId, name, tasks: [{taskId, start, end}] }
- Backend создаёт ProjectBasePlan + по записи ProjectBasePlanTask на каждую задачу
- Combobox «Базовый план» в тулбаре обновляется
Выбор / переключение: 1. Combo «Базовый план» → выбрать снимок 2. Bryntum отрисовывает дополнительные полосы рядом с фактическими — визуальное сравнение «план vs факт»
4.7. Запуск проекта в реализацию¶
Назначение: перевести виртуальные задачи из временной подкатегории планирования в целевую проектную категорию, где настроены маршруты, подписи, смарт-действия.
Шаги:
1. Проект спланирован, задачи имеют привязки «Категория» и назначенных исполнителей
2. Тулбар → «Запуск в реализацию»
3. moveProjectTasks() → POST /api/gantt/move-project-tasks → GanttService.MoveProjectTasks:
- Забирает задачи из подкатегории планирования
- Переносит в целевую subcat (ProjectTasksPlanningSubcat.ProjectTasksSubcatId)
- Вызывает ProjectDal.UpdatePlan
Поведение:
- Задачи появляются в обычных гридах целевой категории
- На них работают маршруты, уведомления, подписи
- На Ганте связь сохраняется через # Задача — виртуальная строка остаётся, но теперь ссылается на реальную задачу
4.8. Импорт данных из реальных задач (v2.268+)¶
Назначение: когда задачи проекта запущены в реализацию и живут своей жизнью (двигаются сроки, меняются исполнители) — синхронизировать Гант с текущим состоянием.
Шаги:
1. Тулбар → «Копировать из проектных задач» (realTasksMenuButton)
2. Выбрать: импортировать все или выборочно
Что обновляется на строке Ганта:
- Название (description → name)
- Подкатегория (id + name)
- Даты (taskStartTime → startDate, taskEndTime → endDate)
- Исполнители с аватарами
- Статус (taskState), taskClosed, isRejected
- Если isRejected = true → строка → inactive = true, percentDone = 0, серый цвет
4.9. Импорт из MS Project (.mpp)¶
Меню «Ещё» → «Импорт» → выбор .mpp-файла. Задачи и связи загружаются в Гант через Bryntum-feature mspImport. Обратный экспорт — в XML через mspExport.export({filename}).
4.10. Экспорт в Excel и PDF¶
Excel:
- Используется библиотека write-excel-file (метод exportToExcelFile)
- Экспортируются видимые колонки с текущими данными
PDF: - Доступен из проектного Ганта и вкладки «Подзадачи» - Диалог: выбор набора колонок, ориентация, параметры страницы - Требует настроенного сервиса PDF Export
4.11. Inline-редактирование в Ганте подкатегории¶
Где: реестр категории → переключить вид на «Гант» → SubcategoryGanttComponent.
Что можно редактировать прямо в таблице:
| Поле | Механизм | Условие |
|---|---|---|
| Исполнители | Combo с API-поиском, filterSelected: true |
Право редактирования задачи |
| Состояние/шаг | Combo доступных шагов → прямой вызов _server.task.executeStep |
Шаг доступен в маршруте |
| Название | Текстовый редактор | canChangeTaskText === true и задача не закрыта |
| Даты / длительность | Drag & resize полосы Ганта | Стандартное поведение Bryntum |
| % выполнения | Drag внутри полосы | Стандартное поведение Bryntum |
Особенности статуса: рядом с именем — цветная точка (StateColor из маршрута категории). Цвета кешируются; до загрузки кэша точка не показывается (виден только текст). Для задач/проектов, созданных до 2.267, цвет появится после операции «Импортировать» из тулбара.
Панель фильтров (v2.267, иконка воронки) — Bryntum Field Filters по нескольким полям. Фильтр применяется к конечным задачам; родительские остаются видимыми независимо от условий.
4.12. Работа с backlog (коробочный уровень)¶
Backlog SLA (для жёстких процессов) — маршрут из 10 шагов: Новая → Уточнение → Оценка трудозатрат (с подписью «Оценка предоставлена») → Ожидает → Бизнес-анализ → Выполняется → Готово к тесту → Сделано на тесте → Ожидание переноса на бой → Сделано на бою → Завершена.
Backlog Agile — облегчённый маршрут из 6 шагов: Новая → Бизнес-анализ → Выполняется → Внутреннее тест → Тест у Клиента → Ожидание переноса на бой → Завершена. Дополнительно: заморозка (Бизнес-анализ → «Отложить задачу» → Заморожено) и возврат на доработку с тестовых шагов.
5. Ручной и автоматический расчёт дат¶
Даты в проектном Ганте управляются двумя уровнями настроек и набором флагов Bryntum под капотом.
5.1. Два уровня настройки¶
| Уровень | Где настраивается | Что задаёт |
|---|---|---|
| Проект целиком | Тулбар → комбо «Тип планирования» (Auto / Manual) | Политика по умолчанию для новых задач |
| Отдельная задача | Колонка «Ручное планирование» (checkbox) | Переопределяет режим для этой строки |
В коде уровень проекта проецируется на каждую задачу через поле manuallyScheduled:
manuallyScheduled: planType == IPlanningType.manual
Глобальный выбор Auto/Manual — это массовая простановка manuallyScheduled на всех задачах. Флаг на задаче — индивидуальное исключение.
Источник индивидуального значения (project-mapper.ts):
t.manuallyScheduled =
typeof manualFromDefaults === 'boolean'
? manualFromDefaults
: i.typeDateSetup === 'manual';
5.2. Автоматический режим¶
При manuallyScheduled = false Bryntum при каждой транзакции пересчитывает даты.
Расчёт по зависимостям (push-forward):
| Тип зависимости | От чего отсчёт | Что устанавливается |
|---|---|---|
| Finish→Start (FS) | endDate предшественника + lag |
startDate преемника |
| Start→Start (SS) | startDate предшественника + lag |
startDate преемника |
| Finish→Finish (FF) | endDate предшественника + lag |
endDate преемника |
| Start→Finish (SF) | startDate предшественника + lag |
endDate преемника |
Сдвиг предшественника мгновенно двигает всех транзитивных последователей в рамках одной транзакции.
Расчёт по ограничениям (constraints):
| UI (русск.) | Bryntum ConstraintType | Поведение |
|---|---|---|
| Как можно скорее | assoonaspossible (ASAP) |
Дефолт для forward-планирования — старт как можно раньше |
| Как можно позже | aslateaspossible (ALAP) |
Старт как можно позже (при backward-планировании) |
| Фиксированное начало | muststarton (MSO) |
startDate ≡ constraintDate — жёстко |
| Фиксированное окончание | mustfinishon (MFO) |
endDate ≡ constraintDate |
| Начало не раньше | startnoearlier (SNE) |
startDate ≥ constraintDate |
| Начало не позднее | startnolater (SNL) |
startDate ≤ constraintDate |
| Окончание не раньше | finishnoearlier (FNE) |
endDate ≥ constraintDate |
| Окончание не позднее | finishnolater (FNL) |
endDate ≤ constraintDate |
Важно: manuallyScheduled=true → задача игнорирует свои constraints. Задать muststarton и одновременно manuallyScheduled=true — бессмысленно.
Rollup по детям:
Для родительской задачи: - Auto + есть дети → startDate = min(детей), endDate = max(детей) — родитель обтягивает период - Manual + есть дети → даты фиксированы вручную, дети могут быть за пределами родителя
Наследование constraints через иерархию:
manuallyScheduledParentBreaksConstraintInheritance (по умолчанию true): если родитель manuallyScheduled=true — он блокирует передачу своих интервалов ограничений детям.
В проектном Ганте есть отдельная ветка:
if (tasksStartConstraintProject) {
// Включен старт проекта — все задачи в Auto
this.ganttRef.tasks.forEach(t => { t.manuallyScheduled = false; });
this.ganttRef.project.startDate = startDate;
} else {
// Старт проекта выключен — все задачи в Manual
this.ganttRef.tasks.forEach(t => { t.manuallyScheduled = true; });
}
Опция «Старт проекта» — триггер массового переключения режима всей версии.
Авто-защита запущенных задач:
if (!manuallyScheduled) {
const startedTaskScheduling = project.$.startedTaskScheduling;
if (startedTaskScheduling === StartedTaskScheduling.Manual) {
manuallyScheduled = (yield this.$.percentDone) > 0;
}
}
Если у проекта startedTaskScheduling === Manual и у задачи percentDone > 0 — Bryntum считает её manually scheduled даже если флаг false. Это защита: запущенная работа не двигается автоматически при изменении плана.
5.3. Ручной режим¶
При manuallyScheduled = true:
- Не пересчитывает даты по зависимостям — связи остаются только визуальными стрелками
- Игнорирует constraint type + constraint date — хранятся, но не срабатывают
- Не делает rollup от детей — родитель хранит заданные вручную даты
- Частично игнорирует производственный календарь — поведение зависит от флагов проекта:
skipNonWorkingTimeWhenSchedulingManuallyskipNonWorkingTimeInDurationWhenSchedulingManually
По умолчанию в ручном режиме попадание на выходной не корректируется — задача может стартовать 1 января.
5.4. Календарь задачи¶
Параллельно режиму планирования у каждой задачи есть выбор календаря (колонка «Календарь»):
| Режим | Что считается рабочим временем | Как влияет на длительность |
|---|---|---|
| Производственный календарь | Рабочие часы из PK (обычно 8-часовой день, 5 дней в неделю, минус праздники) | Задача длительностью 16 часов = 2 рабочих дня |
| Полные дни + выходные/праздники | 24 часа в сутки, но выходные/праздники пустые | Задача длительностью 16 часов = 16 астрономических часов подряд |
PK загружается через CalendarService.getNonWorkingDays(startYear-5, endYear+5) и попадает в gantt.project.calendarManagerStore.
5.5. Направление планирования¶
Колонка «Направление планирования»: вперёд / назад.
| Direction | Алгоритм |
|---|---|
| Forward | От startDate → по зависимостям вычисляем endDate и даты последователей |
| Backward | От endDate (дедлайн) ← вычисляем, когда нужно стартовать |
В backward-режиме ASAP превращается в «как можно позже без срыва срока». Комбинация aslateaspossible + Backward — распространённая связка для дедлайн-планирования.
5.6. Режим трудозатрат¶
Колонка «Режим» определяет, что фиксируется при изменении одного из трёх параметров (длительность / загрузка ресурса / трудозатраты):
| Режим | Что фиксировано | Что пересчитывается |
|---|---|---|
| Нормальный | — | Свободная система |
| Фиксированная длительность | Длительность | При изменении загрузки — часы; при изменении часов — загрузка |
| Фиксированные единицы | % загрузки ресурсов | При изменении длительности — часы; при изменении часов — длительность |
| Фиксированные трудозатраты | Часы | При изменении загрузки — длительность; при изменении длительности — загрузка |
Режим определяет, как пересчитываются зависимые поля; флаг manuallyScheduled — пересчитывается ли дата начала/конца. Ортогональные механики.
5.7. Конфликты при автоматическом расчёте¶
Если в Auto возникает противоречие (задача с muststarton имеет предшественника FS, который толкает её позже) — Bryntum помечает это как ProjectConstraintResolution.Conflict.
Что видит пользователь:
- Полоса задачи подсвечивается как проблемная
- Доступен диалог разрешения конфликта с вариантами:
- Отбросить ограничение (constraintType = null)
- Перевести задачу в ручной режим (manuallyScheduled = true)
- Сдвинуть предшественника
5.8. Сводная матрица¶
| Механика | Auto | Manual |
|---|---|---|
| Пересчёт по зависимостям FS/SS/FF/SF | каскадно | связи только визуальные |
| Тип ограничения + дата ограничения | применяется | игнорируется |
| Rollup дат родителя по детям | min/max детей | даты фиксированы |
| Критический путь | рассчитывается | задача не участвует |
| Ранние/поздние даты (Early/Late start/finish) | вычисляются | не считаются |
| Общий временной резерв (Slack) | вычисляется | не вычисляется |
| Пропуск выходных в длительности | по PK | зависит от skipNonWorkingTimeInDurationWhenSchedulingManually |
| «Старт проекта» сдвигает первые задачи | действует | нет |
| Авто-защита запущенных задач (percentDone>0) | замораживаются | уже заморожены |
5.9. Практические рекомендации¶
- Для планирования с нуля — Auto. Выстраивай зависимости, даты сами подтянутся
- Для импорта готового плана (MS Project, Excel) — Manual на первом этапе, чтобы не перетасовать импортированные даты. Затем выборочно в Auto по мере уточнения связей
- Для фиксации ключевых дат (веха, дедлайн приёмки) —
muststarton/mustfinishon+ Auto на задаче - Для замороженных этапов (согласовано с заказчиком) — Manual. Пересчёт не затронет
- Для запущенных задач (percentDone > 0) — полагайся на авто-защиту, флаг специально не ставь
- Избегай комбинации Manual-родитель + Auto-дети — родитель не суммируется, иерархия визуально ломается. Лучше родителю
muststarton/mustfinishonв Auto - Проверяй конфликты — если в Auto задача окрасилась как проблемная, не игнорируй. Либо сними constraint, либо перестрой зависимости
6. Версионирование проекта (v2.268+)¶
Основная реализация — в BryProjectComponent (bry-project.component.ts) и VerProjectService (ver-project.service.ts).
6.1. Модель данных¶
Версия проекта (ProjectVersion) — полный снимок плана со всеми задачами, зависимостями, настройками. Несколько версий одного проекта существуют параллельно; пользователь переключается между ними через combo в тулбаре.
export type ProjectVersion = {
id?: number; // серверный id (положительный) или -1*nextVersionId (несохранённая)
name: string;
author: any;
created: Date;
modified: Date;
status: any; // статус согласования
accepted: any; // кто подписал
isMainVersion: boolean;
data: Partial<{
rows: ProjectTask[]; // задачи (иерархия через path)
plans: Record<any, Record<number, any>>; // ресурсные планы per-row
inlineData: any; // формат Bryntum (tasksData, dependenciesData, versionsData, changelogsData)
projectStartDate: any;
projectEndDate: any;
projectDateHandMode: boolean;
tasksStartConstraintProject: boolean;
tasksDependencies: any[];
calendar: string;
ganttTaskColorMode: 'markers'|'taskColor'|'none';
}>;
};
6.2. Runtime vs business версии¶
Важный момент архитектуры:
| Уровень | Идентификатор | Что означает |
|---|---|---|
| Business version | versionData.id (число) |
Логическая версия, видимая пользователю и хранящаяся в БД |
| Runtime session | currentSessionId = sess-{uuid} |
Одна сессия работы над версией (открыл → менял → сохранил/закрыл) |
| Runtime history version | currentSessionVersionId = vr-{sessionId} |
ID, к которому привязываются Bryntum-транзакции в этой сессии |
Механизм:
getRuntimeHistoryVersionId(sessionId) → `vr-${sessionId}`
getOrCreateActiveSession(versionId) → { sessionId: sess-..., sessionTitle: "Сессия dd.MM.yyyy HH:mm" }
activeSessionByVersionId: Map<string, {sessionId, sessionTitle}>
Одна бизнес-версия → одна активная runtime-сессия. Если пользователь переключился на другую версию и обратно — сессия сохранится. Если открыл вкладку заново завтра — новая сессия.
6.3. Появление сессионной версии при работе¶
При каждой транзакции Bryntum вызывается onGanttTransactionsChanged():
Bryntum сохранил транзакцию в features.versions.changeStore
↓
получили activeSession для текущей business-version
↓
currentSessionVersionId = vr-{sessionId}
↓
для каждой транзакции без versionId:
tx.versionId = currentSessionVersionId (vr-...)
tx.sourceVersionId = versionData.id (ссылка на business-версию)
tx.sessionId = sess-...
tx.sessionTitle = "Сессия 15.04.2026 14:30"
tx.description = "Изменения версии {name}" (если пусто)
tx.id = hist-{uuid} (если пусто)
↓
если в changeStore есть ≥1 транзакция с этим versionId:
проверяем versionStore.getById(vid)
если нет — добавляем runtime-версию:
versionStore.add({id: vr-..., title: "Сессия ...", sourceVersionId: business-id})
↓
forceRefreshVersionGrid()
Сессионная runtime-версия создаётся лениво, только при первой дата-мутации. Если просто походил по Ганту, поразворачивал группы, двинул зум — новая версия не появится.
6.4. Отделение UI-активности от содержательных изменений¶
UI-only (игнорируется):
- По полю транзакции: column, zoom, splitter, panel, sidebar, scroll, width, collapsed
- По описанию, паттерны: масштаб, колонк, разделител, панел, переключ, expand|collapse|zoom|splitter|column|panel|sidebar|timeline
Data-мутация (порождает запись в истории):
- По полю: startdate, enddate, name, duration, manuallyscheduled, calendar, dependency, parentid, predecessor, successor, constraint
- По store: task, event, dependency, assignment, calendar, resource
Bulk-операции распознаются по описанию: Выполнен импорт N задач, Обновлено N задач.
6.5. Bulk-операции и changelog¶
При импорте MS Project и массовом апдейте из реальных задач механизм транзакций Bryntum выключается:
versions.disabled = true;
stm.disable();
try {
await action();
await project.commitAsync();
versions.changeStore.add({
id: uuid,
description: "Выполнен импорт N задач",
occurredAt: new Date(),
versionId: currentSessionVersionId,
sourceVersionId: versionData.id,
sessionId, sessionTitle,
actions: []
});
} finally {
versions.disabled = prev;
stm.enable();
}
В истории появляется одна запись на весь bulk — иначе versionGrid заполнился бы сотнями мелких транзакций.
6.6. Создание новых версий¶
Четыре способа:
| Метод | Сценарий |
|---|---|
createNewVersionEmpty(name) |
Пустая версия — новый план с нуля |
createNewVersionFromCurrent(name) |
Клон текущей (deepClone), сбрасывает versionsData = [] у клона |
createNewVersionAndCopyFromProjectTasks(name, all) |
Клон из реальных задач проекта через API getVersionFor1FProjectTasks(projectId, all) — синхронизация с фактом |
createNewVersionFromSelected(name) |
Новая версия только из выбранных строк |
Новый id всегда отрицательный (-1 * nextVersionId()), чтобы не пересекаться с серверными. После save становится положительным.
6.7. Редактирование и удаление¶
Метаданные (onEditProject / changeProjectDates):
- Изменение имени: openPrompt с опцией «Удалить» (если версий > 1)
- Изменение дат и настроек: модалка StartEndDateModalComponent — startDate, endDate, handMode, tasksStartConstraintProject, calendar, accepted, status, isMainVersion, ganttTaskColorMode
Удаление (confirmAndDeleteCurrentVersion):
- openConfirm('Удалить проект?')
- saveProject(removeVersion, 'delete') → сервер
- После успешного удаления — переключение на соседнюю (предыдущую по списку) версию
6.8. Сохранение¶
Метод save():
1. Проверки: readonly, уже идёт save → return / очередь
2. project.suspendAutoSync()
3. commitAsync() — все транзакции применены
4. updateVersionDataFromProject(project):
- собирает inlineData (tasksData, dependenciesData, assignmentsData, resourcesData)
- кладёт versionsData (все runtime-версии)
- кладёт changelogsData (все транзакции, нормализованные)
5. pms.saveProject(versionData, id > 0 ? 'update' : 'create')
6. После ответа сервера:
- versionData.id = Math.abs(id)
- needSave = false
- clearDraftInIdb()
- updateSaveButtonState()
- если был needSaveAfterSave → рекурсивно save()
6.9. Черновик в IndexedDB¶
Между изменениями и save данные сохраняются локально:
key: `bryProjectDraft:${projectId}:${versionId || 'current'}`
value: { projectId, versionId, versionName, versionData, updatedAt }
storage: KeyValueIDBStorage (IndexedDB)
Если пользователь закрыл вкладку без сохранения, а потом вернулся — при загрузке проверяется getDraftFromIdb() и восстанавливаются данные. clearDraftInIdb() вызывается только после успешного save на сервере.
6.10. beforeUnload защита¶
private readonly beforeUnloadHandler = (event: BeforeUnloadEvent) => {
if (this.getDirtyCloseMessage()) {
event.preventDefault();
event.returnValue = 'Внесенные изменения не сохранены';
}
};
Браузер покажет стандартный диалог «Вы уверены, что хотите покинуть страницу?» при наличии hasMeaningfulPendingTransactions(). Это guard для подтверждения, не автосохранение.
6.11. Нормализация при загрузке¶
normalizeChangelogsVersionIds— при загрузке проходит поchangelogsDataи приводитversionIdтранзакций к типу изversionsData(число vs строка). Защита от сериализации JSON, где числа в ключах могут стать строкамиsanitizeVersionsData— дедупликация по id. Версиям без id выставляются синтетические idsynthetic-version-NbuildSessionTitleLookup/resolveSessionTitle— восстановление читаемых названий сессий изchangelogsData, если вversionsDataназвание потерялосьskipVersionStoreEnsureOnTransactionChange— флаг, отключающий автоматическое добавление runtime-версии в versionStore во время bulk-операций
6.12. Панель VersionGrid¶
Компонент — VerProjectManagerComponent (vh-ver-project-manager). Открывается по кнопке «Версии» в тулбаре.
Внутреннее состояние через VerProjectService с методами:
- nextVersionId() — вычисление следующего id (Math.max(|v.id|) + 1)
- selectVersion(version) — переключение активной
- versionsDropdown — computed для комбобокса
Колонки панели: - Имя версии - Автор - Создана/Изменена - Статус (согласования) - Подписи (accepted) - Участники - Признак «Основная» (isMainVersion)
Действия: выбрать, переименовать, удалить, копировать из текущей, пометить основной, изменить даты.
6.13. Связь с режимом планирования¶
Флаг tasksStartConstraintProject версии массово переключает manuallyScheduled всех задач:
if (tasksStartConstraintProject) {
this.ganttRef.tasks.forEach(t => {
if (t.startDate < startDate) { t.startDate = startDate; }
t.manuallyScheduled = false;
});
this.ganttRef.project.startDate = startDate;
} else {
this.ganttRef.tasks.forEach(t => {
t.manuallyScheduled = true;
});
}
Переключатель «Старт проекта» в модалке настроек — это глобальный переключатель Auto/Manual для всей версии. Каждая версия может иметь свой режим планирования независимо. После переключения запускается commitAsync() — Bryntum каскадно пересчитывает даты под новый режим.
Флаг projectDateHandMode — независимый, «ручной режим дат проекта».
6.14. Практическая семантика¶
| Действие | Что происходит |
|---|---|
| Открыл проект, не менял ничего | Сессия не создана, runtime-версий нет |
| Двинул одну задачу | Создана сессия + runtime-версия vr-sess-..., транзакция к ней привязана |
| Двинул ещё 5 задач | Все в ту же runtime-версию — она накапливает транзакции сессии |
| Переключил зум, развернул группы | В versionStore ничего нового — это UI-only |
| Нажал «Сохранить» | versionsData + changelogsData сливаются в versionData.data.inlineData и летят на сервер |
| Закрыл вкладку без save | beforeUnload предупредит; при согласии уйти — черновик остаётся в IDB |
| Вернулся на следующий день | Загружается сервер → проверяется IDB-черновик → если новее, предлагается восстановить |
| Создал новую версию из текущей | deepClone, id = −N, versionsData = [] (чистая история); после save станет +N |
| Удалил версию | saveProject('delete') → ушла с сервера, локально переключение на соседнюю |
| Пометил версию «основной» | isMainVersion = true, визуально выделяется в combo |
7. Критический путь¶
7.1. Кастомизация в SPA 1Ф¶
В SPA 1Ф — тумблер в тулбаре, всё остальное считает Bryntum.
Старый GanttComponent (gantt.component.ts):
toggleCriticalPath() {
const newValue = !this.ganttBaseComponent.ganttInstance.features.criticalPaths.disabled;
this.ganttBaseComponent.ganttInstance.features.criticalPaths.disabled = newValue;
}
Новый BryProjectComponent (bry-project.component.ts):
onToolbarToggleCriticalPaths() {
const gantt = this.ganttRef;
if (!gantt?.features?.criticalPaths) { return; }
const feature = gantt.features.criticalPaths;
feature.disabled = !feature.disabled;
this.criticalPathsEnabledSig.set(!feature.disabled);
}
Плюс инициализация при открытии:
const critical = this.ganttRef.features?.criticalPaths;
if (critical) {
this.criticalPathsEnabledSig.set(!critical.disabled);
}
Сигнал в тулбаре — criticalPathsEnabled input + toggleCriticalPaths output в bry-project-toolbar.component.ts.
7.2. Расчёт Bryntum¶
Bryntum рассчитывает расширенный набор полей через ConstrainedLateEventMixin:
| Поле | Смысл |
|---|---|
earlyStartDate |
Самая ранняя возможная дата старта (forward pass) |
earlyEndDate |
Самое раннее возможное окончание |
lateStartDate |
Самая поздняя дата старта без срыва проекта (backward pass) |
lateEndDate |
Самое позднее окончание |
totalSlack |
lateStartDate − earlyStartDate (в длительности) — резерв |
slackUnit |
Единица резерва (обычно дни) |
critical |
totalSlack <= 0 — задача на критическом пути |
Алгоритм — классический CPM (Critical Path Method) с forward/backward проходами по зависимостям, с учётом всех типов констрейнтов.
Критерий критичности:
@calculate('critical')
*calculateCritical(): CalculationIterator<boolean> {
const totalSlack = yield this.$.totalSlack;
return totalSlack <= 0;
}
Критические задачи = задачи с нулевым или отрицательным резервом. Когда features.criticalPaths включён, Bryntum визуально подсвечивает их красной обводкой полосы (CSS-класс b-critical).
7.3. Колонки для пользователя¶
Опциональные колонки (скрыты по умолчанию):
- «Раннее начало» — earlyStartDate
- «Раннее окончание» — earlyEndDate
- «Позднее начало» — lateStartDate
- «Позднее окончание» — lateEndDate
- «Общий временной резерв» — totalSlack
Включаются через правый клик по заголовку колонки → «Колонки» → галочки. Значения рассчитываются Bryntum автоматически.
7.4. Взаимодействие с режимами планирования¶
| Сценарий | Что с критическим путём |
|---|---|
Задача manuallyScheduled=true |
Исключена из критического пути. Slack для ручных задач не считается |
Задача inactive=true (отклонённая) |
Исключена из критического пути |
| Нет зависимостей у задачи | Критический путь не вычисляется — нечего рассчитывать |
percentDone > 0 и StartedTaskScheduling=Manual |
Задача автоматически manuallyScheduled → исключена |
| Все задачи в Manual-режиме | Критический путь пуст. Toggle работает, но подсвечивать нечего |
| Смешанный режим (часть Auto, часть Manual) | Критический путь строится только по Auto-подграфу |
Практическое следствие: если включить toggle и ничего не подсветилось — проверить режим планирования. Скорее всего проект в Manual, или стартовавшие задачи автоматически в Manual, или inactive задач слишком много.
8. Настройки табличного вида ПУ (администрирование категории)¶
- Route: /spa/administration/data-source-settings/projectControl/{subcatId}
- Условие доступности: категория с типом «Для проектов» + в custom-app-settings указан ключ ProjectTableJSONExtParamID
- Источник колонок: ДП-таблица проектного управления
Настраиваемые параметры:
| Параметр | Описание |
|---|---|
| Порядок по умолчанию | Позиция колонки (перетаскивание, кнопки сдвига) |
| Ширина | Ширина колонки в пикселях |
| Видимость по умолчанию | Отображение при первом открытии проекта. В шапке — счётчик 12/35 |
| Стиль отображения | Вариант визуализации. Для колонки «Исполнители»: Аватар / Аватар + ФИО / ФИО (displayName) |
Кнопка Сбросить на рекомендуемые возвращает значения платформы.
Механика наследования: настройки применяются при первом открытии проекта. После — состояние сохраняется индивидуально (БД + localStorage). Последующие изменения администратора не переопределяют сохранённые пользовательские настройки.
9. Диагностика типичных проблем¶
| Симптом | Что проверять |
|---|---|
| Гант открывается пустым у существующего проекта | Сетевую вкладку: отвечает ли GET /api/Gantt?projectId=. Если 404 — endpoint не обслуживается |
| Выбор baseline «слетает» после F5 | basePlanId может не персистится через update-timeline-general-settings |
| Drag задачи не двигает последователей | Тип планирования = Manual. Переключить в Auto |
| Цвет статуса не отображается | Кэш StateColor не загружен. Для проектов до 2.267 — нажать «Импортировать» |
| «# Задача» очистилась после ввода | Номер уже занят другой строкой той же версии проекта |
| Запуск в реализацию ничего не делает | Проверить ProjectTasksPlanningSubcat.ProjectTasksSubcatId у проекта |
| Readonly-Гант: недоступные поля вперемешку с редактируемыми | Двухуровневая проверка прав: задача vs ДП-таблица. Проверить ProjectReferenceTableExtParamId |
| Просрочка показывается, хотя реальная задача закрыта | Маркер берётся из плана Ганта, а не из связанной задачи — обновить план или нажать «Импортировать» на строке |
| Toggle критического пути не подсвечивает задачи | Проект в Manual-режиме или все стартовавшие задачи автоматически manually scheduled |
| Версия не создаётся при изменениях | Транзакция отфильтрована как UI-only (зум, колонки). Изменить дата-поле (startDate/endDate/name) |
| Не работает экспорт PDF | Не настроен сервис PDF Export |
10. Файловая структура фронта¶
spa/apps/spa/src/app/
├── common/components/gantt/ — старый проектный Гант
│ ├── bryntum-angular-shared/ — Angular-обёртка Bryntum (GanttComponentBase, vh-bry-gantt)
│ ├── gantt-base-plan-modal/ — модалка создания базового плана
│ ├── gantt-manage-panel/ — панель управления проектом
│ ├── gantt.component.ts — GanttComponent (vh-gantt)
│ ├── gantt.interfaces.ts — все интерфейсы
│ └── gantt.service.ts — API-вызовы
│
├── pages/project/ — новая страница проекта (v2.268+)
│ ├── project.component.ts — ProjectPageComponent, определяет isReadonlyRoute
│ ├── bry-project/
│ │ ├── bry-project.component.ts — BryProjectComponent, управление версиями и проектом
│ │ ├── bry-project-toolbar/ — тулбар новой версии
│ │ ├── project-class.ts — утилиты, applyRealTaskDataToRecord
│ │ ├── project-mapper.ts — маппинг данных проекта
│ │ ├── project-gantt.utils.ts — applyRejectedStateToGanttTask
│ │ └── project-import-merge.ts — импорт из MS Project
│ └── ver-project-manager/ — панель версий
│ ├── ver-project-manager.component.ts — VerProjectManagerComponent, VersionGrid
│ └── ver-project.service.ts — ProjectVersion, ProjectTask, VerProjectService
│
├── components/task/ui/subcategory/subcategory-gantt/
│ ├── subcategory-gantt.component.ts — SubcategoryGanttComponent (vh-subcategory-gantt)
│ └── subcategory-gantt.service.ts — SubcategoryGanttService
│
└── routing/pages-routes.prod.ts — маршрут project-readonly
spa/libs/bry-components/ — Bryntum Gantt 4.0.3 (vendor, не модифицировать)
├── ChronoGraph/ — scheduling engine
├── Engine/quark/model/
│ ├── gantt/ — ConstrainedByParentMixin, GanttProjectMixin, HasCriticalPathsMixin, ConstrainedLateEventMixin
│ └── scheduler_pro/ — HasDateConstraintMixin, ConstrainedByDependenciesEventScheduleMixin, HasPercentDoneMixin, SplitEventMixin
└── Gantt/, Grid/, Scheduler/, SchedulerPro/ — базовые компоненты
11. API-сервис фронта¶
GanttService (spa/apps/spa/src/app/common/components/gantt/gantt.service.ts):
| Метод | HTTP | URL | Назначение |
|---|---|---|---|
getGanttDataByProject |
GET | /api/Gantt?projectId= |
Данные проекта |
getBasePlans |
GET | /api/gantt/baseplans?projectId= |
Список базовых планов |
addBasePlan |
POST | /api/gantt/add-base-plan |
Создание базового плана |
saveProject |
POST | /api/gantt/save-project |
Сохранение проекта |
moveProjectTasks |
POST | /api/gantt/move-project-tasks |
Запуск в реализацию |
saveProjectSettings |
POST | /api/gantt/update-timeline-general-settings |
Настройки timeline |
VerProjectService (spa/apps/spa/src/app/pages/project/ver-project-manager/ver-project.service.ts) — абстрактный сервис для работы с версиями:
- getProject(params, fillEmptyPlan) — загрузка проекта со всеми версиями
- saveProject(version, mod: 'create'|'update'|'delete') — сохранение/удаление версии
- getVersionFor1FProjectTasks(taskId, all) — получение версии из реальных задач
- getProjectPlanTaskCategoryId() — id категории проектных задач
- setUseProjectReferenceTable(use) — переключение источника данных (ДП-таблица vs обычная категория)
12. Ключевые таблицы БД¶
| Таблица | Назначение |
|---|---|
Tasks |
Задачи — база всего |
Task2TaskLink |
Зависимости между задачами (type: 0=SS, 1=SF, 2=FS, 3=FF; Lag) |
ProjectTasksPlanningSubcat |
Связь проекта с подкатегорией планирования (ProjectTasksSubcatId) |
ProjectBasePlan |
Метаданные базового плана (Id, ProjectId, Name, Date) |
ProjectBasePlanTask |
Снимок дат (ProjectBasePlanId, TaskId, Start, End — составной PK) |
TimelineGeneralSettings |
Настройки timeline проекта (включая basePlanId) |
GantGridColumns |
Настройки колонок таблицы на проекте |
Subcategories |
«Категория» (шаблон процесса) |
Categories |
«Раздел» (папка для категорий) |