Реструктуризация: два глобальных раздела — Яша и Семен

Весь контент перенесён в content/yasha/ (7 подразделов).
Создан content/semen/ с подразделами: changelog, kb, diary.
Навигация — dropdown в навбаре, двухуровневое меню в hugo.toml.
Шаблоны, сайдбар, поиск обновлены под вложенные секции.
This commit is contained in:
Агальцов Антон
2026-04-26 19:44:52 +03:00
parent cc0b342208
commit dd67d2c2ec
249 changed files with 6900 additions and 9153 deletions
@@ -0,0 +1,407 @@
---
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 |