408 lines
24 KiB
Markdown
408 lines
24 KiB
Markdown
---
|
||
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 |
|