Files
docs.a2v.space/content/leadera/leadera-milestones.md
T

330 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Leadera: Вехи (Milestones) на диаграмме Ганта"
date: 2026-04-08
tags: ["leadera", "gantt", "angular", "go", "api", "ux"]
---
## Бизнес-требования
### BR-1: Сущность «Веха» (Milestone)
Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.
**Поля вехи:**
| Поле | Тип | Описание |
|------|-----|----------|
| id | UUID | Идентификатор |
| gantt_chart_id | UUID | Принадлежность к диаграмме (не к секции!) |
| title | string | Название вехи (обязательное, 1–255 символов) |
| date | date | Дата вехи (без времени, обязательная) |
| color | string | Цвет маркера (HEX, по умолчанию `#dc3545` — красный) |
| created_by | UUID | Кто создал (не отображается в UI, хранится для аудита) |
| updated_by | UUID | Кто обновил (не отображается в UI) |
| created_at | timestamp | Дата создания |
| updated_at | timestamp | Дата обновления |
| deleted_at | timestamp | Soft delete |
**Права доступа:** Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).
**Особенности:**
- Вех может быть несколько в проекте
- Несколько вех могут иметь одну и ту же дату
- Вехи сортируются строго по дате (по возрастанию)
- Вехи не привязаны к секциям — они отображаются **сквозь все секции**
---
### BR-2: Управление вехами (модальное окно)
**Вызов:** Кнопка «Вехи» рядом с кнопкой «Создать секцию» на странице диаграммы.
**Модальное окно содержит:**
- Заголовок: «Вехи проекта»
- Список всех вех текущей диаграммы (сортировка по дате по возрастанию)
- Каждая веха в списке показывает: цвет (индикатор), название, дату, кнопки «Редактировать» и «Удалить»
- Кнопка «Добавить веху»
**Форма добавления/редактирования вехи:**
- Название — текстовый ввод (обязательное, 1–255 символов)
- Дата — datepicker (обязательное)
- Цвет — выбор цвета (по аналогии с палитрой цветов в задачах)
- По умолчанию цвет = красный (`#dc3545`)
**Удаление:** с подтверждением (confirm dialog).
---
### BR-3: Отображение вех на таймлайне
**Визуальное представление:**
- **Маркер-флажок** (flag marker) на шкале таймлайна
- От маркера вниз идёт **пунктирная вертикальная линия** через все секции (на всю высоту диаграммы)
- Цвет линии и маркера = цвет вехи
**Позиционирование:**
- Веха ставится ровно на свою дату на шкале времени
- Если дата вехи выходит за рамки текущего диапазона задач — таймлайн **расширяется** (аналогично задачам)
- При изменении масштаба (days/weeks/months/quarters) позиция вехи пересчитывается
**Tooltip при наведении:**
- При наведении на маркер/линию — показывается tooltip с названием вехи
- Если на одну дату приходится несколько вех — в tooltip показываются **все** вехи этой даты с их названиями (каждая с индикацией своего цвета)
---
## Технические требования
### База данных
#### Новая таблица: gantt_milestones
```sql
CREATE TABLE gantt_milestones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
gantt_chart_id UUID NOT NULL REFERENCES gantt_charts(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
date DATE NOT NULL,
color VARCHAR(20) NOT NULL DEFAULT '#dc3545',
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
);
CREATE INDEX idx_gantt_milestones_chart ON gantt_milestones(gantt_chart_id);
CREATE INDEX idx_gantt_milestones_date ON gantt_milestones(date);
```
---
### Backend (Go API)
#### Этап 1: Миграция + Домен
**Промт для агента:**
> Добавь в проект новую таблицу `gantt_milestones`.
>
> 1. Создай миграцию в `cmd/migrate/`:
> - Новая таблица `gantt_milestones` с полями: id (UUID PK), gantt_chart_id (UUID FK → gantt_charts, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), date (DATE NOT NULL), color (VARCHAR 20 NOT NULL DEFAULT '#dc3545'), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. Индексы по gantt_chart_id и date.
>
> 2. Создай доменную модель `GanttMilestone` в `internal/domain/gantt.go`:
> ```go
> type GanttMilestone struct {
> ID UUID `json:"id"`
> GanttChartID UUID `json:"gantt_chart_id"`
> Title string `json:"title"`
> Date time.Time `json:"date"`
> Color string `json:"color"`
> CreatedBy UUID `json:"created_by"`
> UpdatedBy UUID `json:"updated_by"`
> CreatedAt time.Time `json:"created_at"`
> UpdatedAt time.Time `json:"updated_at"`
> DeletedAt *time.Time `json:"deleted_at,omitempty"`
> }
> ```
#### Этап 2: CRUD для GanttMilestones
**Промт для агента:**
> Реализуй полный CRUD для сущности `GanttMilestone` по аналогии с существующими handlers/services/repositories в проекте.
>
> **Роуты:**
> - `GET /api/gantt-charts/:chartId/milestones` — список вех диаграммы
> - `POST /api/gantt-charts/:chartId/milestones` — создать веху
> - `PUT /api/gantt-charts/:chartId/milestones/:milestoneId` — обновить веху
> - `DELETE /api/gantt-charts/:chartId/milestones/:milestoneId` — удалить веху (soft delete)
>
> **Repository:** `internal/repository/gantt_milestone_repository.go`
> - `GetByChartID(ctx, chartID) ([]domain.GanttMilestone, error)` — все неудалённые вехи диаграммы, отсортированные по date ASC
> - `GetByID(ctx, id) (*domain.GanttMilestone, error)`
> - `Create(ctx, milestone) error`
> - `Update(ctx, milestone) error`
> - `Delete(ctx, id) error` — soft delete (set deleted_at)
> - Все запросы — raw SQL через pgx, как в остальных репозиториях проекта
>
> **Service:** `internal/service/gantt_milestone_service.go`
> - Валидация: title (1–255 символов), date (обязательное), color (валидный HEX, default '#dc3545')
> - Проверка прав доступа: пользователь должен иметь права выше «только чтение» в пространстве (space) диаграммы
> - Проверка что chartId существует и не архивирован
>
> **Handler:** `internal/handler/gantt_milestone_handler.go`
> - Swagger-аннотации для всех эндпоинтов
> - Валидация входных данных
> - Ошибки в формате проекта
>
> **DTO:** `internal/dto/gantt/milestones_dto.go`
> ```go
> type CreateGanttMilestoneRequest struct {
> Title string `json:"title" binding:"required,min=1,max=255"`
> Date string `json:"date" binding:"required"`
> Color *string `json:"color" binding:"omitempty,max=20"`
> }
>
> type UpdateGanttMilestoneRequest struct {
> Title *string `json:"title" binding:"omitempty,min=1,max=255"`
> Date *string `json:"date" binding:"omitempty"`
> Color *string `json:"color" binding:"omitempty,max=20"`
> }
>
> type GanttMilestoneResponse struct {
> ID domain.UUID `json:"id"`
> GanttChartID domain.UUID `json:"gantt_chart_id"`
> Title string `json:"title"`
> Date string `json:"date"`
> Color string `json:"color"`
> CreatedByUser *CreatedByUpdatedUser `json:"created_by_user"`
> UpdatedByUser *CreatedByUpdatedUser `json:"updated_by_user"`
> CreatedAt time.Time `json:"created_at"`
> UpdatedAt time.Time `json:"updated_at"`
> }
>
> type GanttMilestonesListResponse struct {
> Milestones []GanttMilestoneResponse `json:"milestones"`
> }
> ```
>
> **Регистрация роутов:** Добавь группу milestone-роутов в роутер рядом с существующими роутами секций/задач. Проверка прав через существующий middleware.
#### Этап 3: Включение вех в ответ диаграммы
**Промт для агента:**
> При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.
>
> 1. Добавь эндпоинт или расширь существующий `GET /api/gantt-charts/:chartId` — включить поле `milestones` с массивом `GanttMilestoneResponse`
> 2. Вехи с `deleted_at IS NOT NULL` не возвращаются
> 3. Вехи сортируются по `date` ASC
> 4. Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта
---
### Frontend (Angular)
#### Этап FE-1: Модели + Сервис
**Промт для агента:**
> Добавь в проект поддержку вех (milestones) на диаграмме Ганта.
>
> 1. В `pages/gantt/models/` (или соответствующем файле моделей) добавь:
>
> ```typescript
> export interface GanttMilestone {
> id: string;
> gantt_chart_id: string;
> title: string;
> date: string;
> color: string;
> created_by_user?: { id: string; full_name: string };
> updated_by_user?: { id: string; full_name: string };
> created_at: string;
> updated_at: string;
> }
>
> export interface GanttMilestoneCreateRequest {
> title: string;
> date: string;
> color?: string;
> }
>
> export interface GanttMilestoneUpdateRequest {
> title?: string;
> date?: string;
> color?: string;
> }
> ```
>
> 2. Добавь `milestones: GanttMilestone[]` в интерфейс/модель полной загрузки данных диаграммы
>
> 3. Создай сервис `pages/gantt/services/gantt-milestone.service.ts`:
> - Inject HttpClient
> - Методы:
> - `getMilestones(chartId: string): Observable<GanttMilestone[]>`
> - `createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable<GanttMilestone>`
> - `updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable<GanttMilestone>`
> - `deleteMilestone(chartId: string, milestoneId: string): Observable<void>`
> - Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)
#### Этап FE-2: Модальное окно управления вехами
**Промт для агента:**
> Создай компонент модального окна для управления вехами диаграммы Ганта.
>
> 1. Создай компонент `pages/gantt/components/milestones-modal/`
>
> 2. **Кнопка вызова:** Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.
>
> 3. **Модальное окно** (ng-bootstrap modal):
> - Заголовок: «Вехи проекта»
> - Список вех (сортировка по date ASC):
> - Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)
> - Кнопка «Добавить веху» внизу списка
> - Если вех нет — сообщение «Вех пока нет. Добавьте первую!»
>
> 4. **Форма добавления/редактирования** (inline в модальном окне):
> - Название — текстовый ввод (обязательное)
> - Дата — ng-bootstrap datepicker (обязательное)
> - Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)
> - По умолчанию цвет = `#dc3545` (красный)
> - Кнопки «Сохранить» и «Отмена»
>
> 5. При сохранении — вызов соответствующего метода сервиса (create/update)
> 6. При удалении — confirm dialog → вызов deleteMilestone
> 7. После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)
>
> 8. Инжектировать GanttMilestoneService и NgbModal
#### Этап FE-3: Отображение вех на таймлайне (Konva canvas)
**Промт для агента:**
> Отобрази вехи (milestones) на canvas-диаграмме Ганта.
>
> Проект использует Konva.js для рендеринга.
>
> 1. При загрузке данных диаграммы — извлечь массив `milestones`
>
> 2. Для каждой вехи нарисовать на canvas:
> - **Маркер-флажок** (flag marker) на позиции даты вехи на шкале времени
> - Стиль: маленький треугольник или флаг на верхней границе таймлайна
> - Цвет = milestone.color
> - **Пунктирная вертикальная линия** от маркера вниз через все секции (на всю высоту области секций)
> - Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7
>
> 3. **Позиционирование:**
> - Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)
> - Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)
> - При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех
> - При скролле — вехи скроллятся вместе с задачами
>
> 4. **Tooltip при наведении:**
> - При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
> - Если веха одна на эту дату — показать название
> - Если несколько вех на одну дату — показать список: цветной кружок + название для каждой
>
> 5. **Z-index:**
> - Линии вех рисуются **поверх** фона секций, но **под** задачами
> - Маркеры-флажки — поверх всего (самый высокий z-index)
>
> 6. **Реактивность:**
> - При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)
> - Подписаться на изменения массива milestones через сервис/store
---
## Порядок реализации
| # | Этап | Сторона | Зависимости |
|---|------|---------|-------------|
| 1 | Миграция + Домен | Backend | — |
| 2 | CRUD вех | Backend | Этап 1 |
| 3 | Включение вех в ответ диаграммы | Backend | Этап 2 |
| 4 | Модели + Сервис | Frontend | — (параллельно с backend) |
| 5 | Модальное окно управления вехами | Frontend | Этапы 4 + backend 2 |
| 6 | Отображение вех на таймлайне | Frontend | Этапы 4 + backend 3 |