--- title: "Leadera: Ограничения секций в диаграмме Ганта" date: 2026-04-06 tags: ["leadera", "gantt", "angular", "go", "api", "ux"] --- ## Бизнес-требования ### 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 ```sql 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 ```sql 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` добавь: > > ```typescript > 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[]; > } > ``` > > 2. Добавь `max_concurrent_tasks: number` в интерфейс `GanttSection` > 3. Добавь `blocks: GanttSectionBlock[]` в интерфейс `GanttSection` > 4. Добавь `max_concurrent_tasks?: number` в `GanttSectionCreateRequest` и `GanttSectionUpdateRequest` > > 5. Создай сервис `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 или через ту же форму > - Удаление — с подтверждением > > 2. При открытии окна редактирования — загрузить текущие настройки секции + блоки > 3. При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков > 4. Инжектировать 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 |