Leadera: Ограничения секций в диаграмме Ганта

📁 ~/Projects/docs-a2v/content/leadera/leadera-section-constraints.md

Бизнес-требования

BR-1: Лимит одновременных задач (Concurrency Limit)

У каждой секции появляется параметр max_concurrent_tasks (целое число ≥ 0, по умолчанию 0).

Поведение:

  • Если max_concurrent_tasks = 3 — в любой данный день в этой секции не может быть больше 3 активных задач одновременно
  • Если max_concurrent_tasks = 0 — ограничение отключено, лимита нет
  • Пересечение считается по дате: задача занимает дни с start_date по end_date включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день
  • Ограничение работает только в рамках одной секции. Задачи из разных секций не влияют друг на друга

Срабатывание при:

  1. Создании задачи — если добавление нарушает лимит → Alert
  2. Перетаскивании задачи (drag) — если новая позиция нарушает лимит → Alert
  3. Растягивании задачи (resize) — если новый размер нарушает лимит → Alert
  4. Редактировании задачи (форма) — если изменённые даты нарушают лимит → Alert

Важно: Действие НЕ отменяется. Задача создаётся/перемещается в любом случае, но пользователь видит предупреждение.

Текст Alert: «В секции “[название секции]” на [дата/диапазон] превышен лимит одновременных задач (максимум: N)»


BR-2: Блоки секций (Section Blocks)

У каждой секции появляется набор блоков — особый вид записей, отображаемых на таймлайне как заштрихованные области.

Сущность GanttSectionBlock:

Поле Тип Описание
id UUID Идентификатор
section_id UUID Принадлежность к секции
title string Название (например: «Отпуск», «Командировка»)
start_date date Начало интервала (минимум 1 день)
end_date date Конец интервала
block_concurrency boolean Запрещать ли пересечение обычных задач с данным блоком

Валидация:

  • end_date не может быть раньше start_date
  • Все поля обязательны
  • Блок может быть в прошлом

Визуальное отображение на Ганте:

  • Заштрихованная область на всю высоту секции (включая все строки задач в секции)
  • Название блока отображается поверх штриховки с подложкой (для читаемости)

Поведение block_concurrency:

  • Если block_concurrency = true — при пересечении задачи с данным блоком показывается Alert (аналогично BR-1), но задача всё равно создаётся/перемещается
  • Если block_concurrency = false — блок отображается визуально, но не влияет на задачи

Срабатывание при:

  1. Создании задачи — пересекает блок с block_concurrency = true → Alert
  2. Перетаскивании задачи (drag) → Alert
  3. Растягивании задачи (resize) → Alert
  4. Редактировании задачи (форма) → Alert

Текст Alert: «В секции “[название секции]” задача пересекает блок “[название блока]” ([дата] — [дата])»


BR-3: Система Alert-ов

Все предупреждения (от BR-1 и BR-2) выводятся в единую область над диаграммой Ганта.

Поведение:

  • Alert-ы — Bootstrap Alert (warning), постоянные
  • Пользователь НЕ может закрыть Alert вручную
  • Alert исчезает автоматически, когда текущая дата прошла дату конфликта (конфликт больше не актуален)
  • Проверка конфликтов — только на фронтенде (на основе загруженных данных: задачи секции, блоки, max_concurrent_tasks)
  • Если Alert-ов много — область сворачиваемая с счётчиком в бейдже (например: «⚠️ 3 предупреждения»)
  • По умолчанию — развёрнуто
  • Сворачивание/разворачивание — клик по заголовку области

BR-4: Настройки секции

  • Настройки совмещены с существующим модальным окном редактирования секции (не отдельное окно)

Окно создания секции — добавляется:

  • Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита, по умолчанию 0)

Окно редактирования секции — добавляется:

  • Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита)
  • Список блоков (таблица/список с CRUD: добавить, редактировать, удалить)
    • Сортировка: по start_date по убыванию (будущее сверху)
    • Каждый блок — форма: title, start_date, end_date, block_concurrency (чекбокс: «Запретить пересечение задач»)
    • Валидация: end_date >= start_date, все поля заполнены

BR-5: Наследование

  • При появлении функционала копирования/дублирования диаграммы — настройки секций (лимит + блоки) наследуются

Технические требования

База данных

Новая таблица: gantt_section_blocks

CREATE TABLE gantt_section_blocks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    section_id UUID NOT NULL REFERENCES gantt_sections(id) ON DELETE CASCADE,
    title VARCHAR(255) NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    block_concurrency BOOLEAN NOT NULL DEFAULT false,
    created_by UUID NOT NULL REFERENCES users(id),
    updated_by UUID NOT NULL REFERENCES users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE,
    CONSTRAINT chk_block_dates CHECK (end_date >= start_date)
);

CREATE INDEX idx_gantt_section_blocks_section ON gantt_section_blocks(section_id);
CREATE INDEX idx_gantt_section_blocks_dates ON gantt_section_blocks(start_date, end_date);

Изменение таблицы: gantt_sections

ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0;

Backend (Go API)

Этап 1: Миграция + Домен

Промт для агента:

Добавь в проект новую таблицу gantt_section_blocks и колонку max_concurrent_tasks в gantt_sections.

  1. Создай миграцию в cmd/migrate/:
    • Новая таблица gantt_section_blocks с полями: id (UUID PK), section_id (UUID FK → gantt_sections, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), start_date (DATE NOT NULL), end_date (DATE NOT NULL), block_concurrency (BOOLEAN DEFAULT false), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. CHECK constraint: end_date >= start_date. Индексы по section_id и (start_date, end_date).
    • ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0.
  2. Создай доменную модель GanttSectionBlock в internal/domain/gantt.go со всеми полями таблицы.
  3. Добавь поле MaxConcurrentTasks int в структуру GanttSection.
  4. Обнови маппинг в relevant repository файлах, чтобы новое поле max_concurrent_tasks читалось/писалось.

Этап 2: CRUD для GanttSectionBlocks

Промт для агента:

Реализуй полный CRUD для сущности GanttSectionBlock по аналогии с существующими handlers/services/repositories в проекте.

Роуты (внутри группы секции):

  • GET /api/gantt-charts/:chartId/sections/:sectionId/blocks — список блоков секции
  • POST /api/gantt-charts/:chartId/sections/:sectionId/blocks — создать блок
  • PUT /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId — обновить блок
  • DELETE /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId — удалить блок (soft delete)

Repository: internal/repository/gantt_section_block_repository.go

  • GetBySectionID(ctx, sectionID) ([]domain.GanttSectionBlock, error)
  • GetByID(ctx, id) (*domain.GanttSectionBlock, error)
  • Create(ctx, block) error
  • Update(ctx, block) error
  • Delete(ctx, id) error — soft delete (set deleted_at)
  • Все запросы — raw SQL через pgx, как в остальных репозиториях проекта

Service: internal/service/gantt_section_block_service.go

  • Бизнес-логика: валидация dates (end >= start), проверка прав доступа к секции
  • Inject repository

Handler: internal/handler/gantt_section_block_handler.go

  • Swagger-аннотации для всех эндпоинтов
  • Валидация входных данных
  • Ошибки в формате проекта (GanttErrorResponse)

Модели запросов:

  • Create: title (required), start_date (required), end_date (required), block_concurrency (bool, default false)
  • Update: title, start_date, end_date, block_concurrency — все опциональны

Этап 3: Обновление настроек секции

Промт для агента:

Обнови CRUD секций (gantt_sections) для поддержки нового поля max_concurrent_tasks.

  1. Добавь max_concurrent_tasks в GanttSectionCreateRequest (опциональное, int ≥ 0, default 0)
  2. Добавь max_concurrent_tasks в GanttSectionUpdateRequest (опциональное, int ≥ 0)
  3. Обнови SQL-запросы в repository: INSERT и UPDATE должны включать max_concurrent_tasks
  4. При GET секции (или списке секций) — возвращать max_concurrent_tasks в ответе
  5. Валидация: значение должно быть ≥ 0

Этап 4: Включение блоков в ответ секций

Промт для агента:

При запросе списка секций или одной секции включай связанные блоки в ответ.

  1. Обнови GET /api/gantt-charts/:chartId/sections/:sectionId — добавить поле blocks с массивом GanttSectionBlock
  2. Обнови GET /api/gantt-charts/:chartId/sections — добавить поле blocks для каждой секции
  3. Блоки с deleted_at IS NOT NULL не возвращаются
  4. Блоки сортируются по start_date по убыванию (будущее сверху)

Frontend (Angular)

Этап FE-1: Модели + Сервис

Промт для агента:

Добавь в проект поддержку ограничений секций Ганта.

  1. В pages/gantt/models/gantt-chart.model.ts добавь:
export interface GanttSectionBlock {
  id: string;
  section_id: string;
  title: string;
  start_date: string;
  end_date: string;
  block_concurrency: boolean;
  created_by_user: UserInfo;
  updated_by_user: UserInfo;
  created_at: string;
  updated_at: string;
}

export interface GanttSectionBlockCreateRequest {
  title: string;
  start_date: string;
  end_date: string;
  block_concurrency?: boolean;
}

export interface GanttSectionBlockUpdateRequest {
  title?: string;
  start_date?: string;
  end_date?: string;
  block_concurrency?: boolean;
}

export interface ConflictCheck {
  has_conflicts: boolean;
  conflicts: ConflictItem[];
}

export interface ConflictItem {
  type: 'concurrency' | 'block';
  section_id: string;
  section_title: string;
  block_title?: string;
  start_date?: string;
  end_date?: string;
  message: string;
  dates?: string[];
}
  1. Добавь max_concurrent_tasks: number в интерфейс GanttSection

  2. Добавь blocks: GanttSectionBlock[] в интерфейс GanttSection

  3. Добавь max_concurrent_tasks?: number в GanttSectionCreateRequest и GanttSectionUpdateRequest

  4. Создай сервис pages/gantt/services/gantt-section-block.service.ts:

  • Inject HttpClient
  • Методы: getBlocks(chartId, sectionId), createBlock(chartId, sectionId, data), updateBlock(chartId, sectionId, blockId, data), deleteBlock(chartId, sectionId, blockId)
  • Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)

Этап FE-2: Интеграция в модальные окна секции

Промт для агента:

Интегрируй настройки ограничений в существующие модальные окна создания и редактирования секции Ганта.

  1. Найди существующие компоненты модальных окон секции (создание/редактирование).

Окно создания секции — добавить:

  • Числовой ввод: «Максимум одновременных задач»
  • Подсказка: «0 = без ограничений»
  • Валидация: число ≥ 0
  • Значение по умолчанию: 0
  • Отправляется в GanttSectionCreateRequest как max_concurrent_tasks

Окно редактирования секции — добавить:

  • Числовой ввод: «Максимум одновременных задач» (аналогично созданию)
  • Секция «Блоки (запретные интервалы)»:
    • Таблица/список существующих блоков с колонками: Название, Начало, Конец, Блокировка (чекбокс/иконка), Действия (редактировать/удалить)
    • Сортировка: по start_date по убыванию (будущее сверху)
    • Кнопка «Добавить блок» — inline-форма:
      • title (текстовый ввод, обязательный)
      • start_date (datepicker, обязательный)
      • end_date (datepicker, обязательный)
      • block_concurrency (чекбокс: «Запретить пересечение задач»)
      • Валидация: end_date >= start_date, все поля заполнены
    • Редактирование — inline или через ту же форму
    • Удаление — с подтверждением
  1. При открытии окна редактирования — загрузить текущие настройки секции + блоки
  2. При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков
  3. Инжектировать GanttChartService и GanttSectionBlockService

Этап FE-3: Визуализация блоков на Ганте

Промт для агента:

Отобрази блоки секций (GanttSectionBlock) на canvas-диаграмме Ганта.

Проект использует Konva.js для рендеринга. Основной компонент: gantt-chart-canvas.

  1. При загрузке данных диаграммы — извлечь блоки из каждой секции
  2. В рендерере секций (canvas/timeline-renderer.ts или аналогичный) — для каждого блока:
    • Нарисовать заштрихованную область (diagonal hatch pattern) на всю высоту секции (все строки задач в этой секции)
    • Область ограничена по горизонтали: от start_date до end_date (в пикселях по шкале времени)
    • Цвет штриховки: полупрозрачный серый или configurable
    • Поверх штриховки — текст с названием блока (block.title) на подложке (прямоугольник с фоном, чтобы текст читался)
  3. Блоки рендерятся до задач (z-index ниже), чтобы задачи были поверх
  4. Блоки с block_concurrency = true и false визуально не отличаются (штриховка одинаковая)
  5. При скролле/зуме — блоки пересчитываются как обычные элементы таймлайна

Этап FE-4: Система Alert-ов

Промт для агента:

Создай компонент для отображения Alert-ов о конфликтах на диаграмме Ганта.

  1. Создай компонент pages/gantt/components/gantt-conflict-alerts/

  2. Компонент размещается над canvas-диаграммой Ганта

  3. Входные данные: массив ConflictItem[] (из модели)

  4. Визуал:

    • Bootstrap Alert (warning) для каждого конфликта
    • Каждый Alert показывает message из ConflictItem
    • Без кнопки закрытия — Alert нельзя закрыть вручную
    • Alert исчезает автоматически, когда текущая дата прошла дату конфликта
    • Область сворачиваемая: заголовок «⚠️ Предупреждения (N)» — клик сворачивает/разворачивает список
    • По умолчанию — развёрнуто
    • При 0 конфликтов — область скрыта
  5. Состояние хранится в сервисе или в компоненте диаграммы:

    • Добавление нового конфликта → area auto-expands
    • Alert удаляется из массива автоматически, когда текущая дата прошла дату конфликта
    • Проверка актуальности конфликтов — при каждой отрисовке / изменении данных
    • Не использовать toast — Alert-ы постоянные

Этап FE-5: Проверка конфликтов при операциях с задачами

Промт для агента:

Интегрируй проверку конфликтов во все операции с задачами на диаграмме Ганта.

Проект использует interaction-handler.ts для drag/resize и формы для создания/редактирования.

Важно: Проверка конфликтов — только на фронтенде, без вызова API. Используются загруженные данные секции (задачи, блоки, max_concurrent_tasks).

  1. Вынеси логику проверки конфликтов checkConflicts() в отдельный метод в gantt-canvas.service.ts:

    • Принимает: section (GanttSection — содержит max_concurrent_tasks и blocks), tasks (GanttTask[] — задачи секции), task_id (nullable — исключить текущую задачу), start_date, end_date
    • Логика: a. Если section.max_concurrent_tasks > 0: посчитать кол-во задач (исключая task_id), пересекающихся с [start_date, end_date] на каждый день. Если в любой день count > max_concurrent_tasks — добавить конфликт concurrency b. Для каждого блока секции с block_concurrency = true, пересекающегося с [start_date, end_date] — добавить конфликт block
    • Возвращает ConflictCheck
  2. После каждой успешной операции с задачей (создание, обновление, drag, resize):

    • Вызвать checkConflicts с параметрами задачи
    • Если has_conflicts = true — передать конфликты в GanttConflictAlertsComponent
    • Alert-ы показываются, действие НЕ отменяется
  3. Точки интеграции:

    • Создание задачи (форма): после успешного POST → checkConflicts → show alerts
    • Редактирование задачи (форма): после успешного PUT → checkConflicts → show alerts
    • Drag задачи (interaction-handler.ts): после успешного обновления позиции → checkConflicts → show alerts
    • Resize задачи (interaction-handler.ts): после успешного обновления размера → checkConflicts → show alerts
  4. При переключении диаграммы или секции — очищать Alert-ы

  5. Alert-ы автоматически удаляются, когда текущая дата прошла дату конфликта — область скрывается, когда конфликтов не осталось


Порядок реализации

# Этап Сторона Зависимости
1 Миграция + Домен Backend
2 CRUD блоков Backend Этап 1
3 Обновление настроек секции Backend Этап 1
4 Включение блоков в ответ секций Backend Этап 2
5 Модели + Сервис Frontend — (можно параллельно с backend)
6 Интеграция в модальные окна Frontend Этапы 5 + backend 2,3
7 Визуализация блоков на Ганте Frontend Этапы 5 + backend 4
8 Система Alert-ов Frontend Этап 5
9 Проверка конфликтов при операциях Frontend Этапы 5,7,8