Весь контент перенесён в content/yasha/ (7 подразделов). Создан content/semen/ с подразделами: changelog, kb, diary. Навигация — dropdown в навбаре, двухуровневое меню в hugo.toml. Шаблоны, сайдбар, поиск обновлены под вложенные секции.
330 lines
18 KiB
Markdown
330 lines
18 KiB
Markdown
---
|
||
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 |
|