Перейти к содержанию

Туториал. Проектное управление (ПУ) в 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 GanttBasePlanModalComponentaddBasePlan()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. Создание нового проекта

  1. Открыть категорию типа «Для проектов» (например, Проекты из коробки PM)
  2. Создать задачу кнопкой «+», заполнить базовые ДП: тип проекта, компания, договор, плановые даты
  3. Сохранить карточку
  4. Нажать «Заняться» (переход в статус «Выполняется», с подписью)
  5. В тулбаре карточки нажать иконку «Проект» → открывается проектный Гант

Поведение: - Гант открывается пустым (задач нет) — только с маркером «сегодня» - Кнопка «Сохранить» серая, пока нет изменений - Тип планирования по умолчанию — Auto - Масштаб по умолчанию — «по масштабу проекта» (scroll к текущей дате)

4.2. Декомпозиция работ (СДР)

  1. Кнопка «Создать задачу» → новая строка в конец или после выбранной
  2. Ввести название в колонке «Наименование задачи»
  3. Выбрать строку → «Создать подзадачу» → появляется вложенный уровень
  4. Перетаскиванием строк менять иерархию/порядок
  5. Через контекстное меню: «Сделать подзадачей» / «Поднять над задачей»

Поведение: - Родительская задача автоматически получает признак is-project (суммарная, жирная полоса) - % завершения родителя рассчитывается только из статусов дочерних задач — вручную не редактируется - Для конечной задачи без детей: завершена → 100%, отклонена → 0% (принудительно), не завершена → пусто - Лимит: 10 000 задач в одном проекте

4.3. Установка связей между задачами

  1. В колонке «Предшествующие» у задачи-преемника указать номер строки предшественника
  2. Или на таймлайне — потянуть от края полосы одной задачи к краю другой
  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): - Из задачи подтягиваются: название (descriptionname), исполнители, статус, категория, фактические даты начала/окончания, срок - Номер задачи превращается в гиперссылку - Если задача отклонена (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-tasksGanttService.MoveProjectTasks: - Забирает задачи из подкатегории планирования - Переносит в целевую subcat (ProjectTasksPlanningSubcat.ProjectTasksSubcatId) - Вызывает ProjectDal.UpdatePlan

Поведение: - Задачи появляются в обычных гридах целевой категории - На них работают маршруты, уведомления, подписи - На Ганте связь сохраняется через # Задача — виртуальная строка остаётся, но теперь ссылается на реальную задачу

4.8. Импорт данных из реальных задач (v2.268+)

Назначение: когда задачи проекта запущены в реализацию и живут своей жизнью (двигаются сроки, меняются исполнители) — синхронизировать Гант с текущим состоянием.

Шаги: 1. Тулбар → «Копировать из проектных задач» (realTasksMenuButton) 2. Выбрать: импортировать все или выборочно

Что обновляется на строке Ганта: - Название (descriptionname) - Подкатегория (id + name) - Даты (taskStartTimestartDate, taskEndTimeendDate) - Исполнители с аватарами - Статус (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:

  1. Не пересчитывает даты по зависимостям — связи остаются только визуальными стрелками
  2. Игнорирует constraint type + constraint date — хранятся, но не срабатывают
  3. Не делает rollup от детей — родитель хранит заданные вручную даты
  4. Частично игнорирует производственный календарь — поведение зависит от флагов проекта:
  5. skipNonWorkingTimeWhenSchedulingManually
  6. skipNonWorkingTimeInDurationWhenSchedulingManually

По умолчанию в ручном режиме попадание на выходной не корректируется — задача может стартовать 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. Практические рекомендации

  1. Для планирования с нуля — Auto. Выстраивай зависимости, даты сами подтянутся
  2. Для импорта готового плана (MS Project, Excel) — Manual на первом этапе, чтобы не перетасовать импортированные даты. Затем выборочно в Auto по мере уточнения связей
  3. Для фиксации ключевых дат (веха, дедлайн приёмки)muststarton / mustfinishon + Auto на задаче
  4. Для замороженных этапов (согласовано с заказчиком) — Manual. Пересчёт не затронет
  5. Для запущенных задач (percentDone > 0) — полагайся на авто-защиту, флаг специально не ставь
  6. Избегай комбинации Manual-родитель + Auto-дети — родитель не суммируется, иерархия визуально ломается. Лучше родителю muststarton/mustfinishon в Auto
  7. Проверяй конфликты — если в 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 выставляются синтетические id synthetic-version-N
  • buildSessionTitleLookup / 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 «Раздел» (папка для категорий)