Leadera: Вехи (Milestones) на диаграмме Ганта
~/Projects/docs-a2v/content/leadera/leadera-milestones.md
Бизнес-требования
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
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.
Создай миграцию в
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.Создай доменную модель
GanttMilestoneвinternal/domain/gantt.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 ASCGetByID(ctx, id) (*domain.GanttMilestone, error)Create(ctx, milestone) errorUpdate(ctx, milestone) errorDelete(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.gotype 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: Включение вех в ответ диаграммы
Промт для агента:
При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.
- Добавь эндпоинт или расширь существующий
GET /api/gantt-charts/:chartId— включить полеmilestonesс массивомGanttMilestoneResponse- Вехи с
deleted_at IS NOT NULLне возвращаются- Вехи сортируются по
dateASC- Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта
Frontend (Angular)
Этап FE-1: Модели + Сервис
Промт для агента:
Добавь в проект поддержку вех (milestones) на диаграмме Ганта.
- В
pages/gantt/models/(или соответствующем файле моделей) добавь: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; }
Добавь
milestones: GanttMilestone[]в интерфейс/модель полной загрузки данных диаграммыСоздай сервис
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: Модальное окно управления вехами
Промт для агента:
Создай компонент модального окна для управления вехами диаграммы Ганта.
Создай компонент
pages/gantt/components/milestones-modal/Кнопка вызова: Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.
Модальное окно (ng-bootstrap modal):
- Заголовок: «Вехи проекта»
- Список вех (сортировка по date ASC):
- Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)
- Кнопка «Добавить веху» внизу списка
- Если вех нет — сообщение «Вех пока нет. Добавьте первую!»
Форма добавления/редактирования (inline в модальном окне):
- Название — текстовый ввод (обязательное)
- Дата — ng-bootstrap datepicker (обязательное)
- Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)
- По умолчанию цвет =
#dc3545(красный)- Кнопки «Сохранить» и «Отмена»
При сохранении — вызов соответствующего метода сервиса (create/update)
При удалении — confirm dialog → вызов deleteMilestone
После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)
Инжектировать GanttMilestoneService и NgbModal
Этап FE-3: Отображение вех на таймлайне (Konva canvas)
Промт для агента:
Отобрази вехи (milestones) на canvas-диаграмме Ганта.
Проект использует Konva.js для рендеринга.
При загрузке данных диаграммы — извлечь массив
milestonesДля каждой вехи нарисовать на canvas:
- Маркер-флажок (flag marker) на позиции даты вехи на шкале времени
- Стиль: маленький треугольник или флаг на верхней границе таймлайна
- Цвет = milestone.color
- Пунктирная вертикальная линия от маркера вниз через все секции (на всю высоту области секций)
- Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7
Позиционирование:
- Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)
- Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)
- При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех
- При скролле — вехи скроллятся вместе с задачами
Tooltip при наведении:
- При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
- Если веха одна на эту дату — показать название
- Если несколько вех на одну дату — показать список: цветной кружок + название для каждой
Z-index:
- Линии вех рисуются поверх фона секций, но под задачами
- Маркеры-флажки — поверх всего (самый высокий z-index)
Реактивность:
- При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)
- Подписаться на изменения массива milestones через сервис/store
Порядок реализации
| # | Этап | Сторона | Зависимости |
|---|---|---|---|
| 1 | Миграция + Домен | Backend | — |
| 2 | CRUD вех | Backend | Этап 1 |
| 3 | Включение вех в ответ диаграммы | Backend | Этап 2 |
| 4 | Модели + Сервис | Frontend | — (параллельно с backend) |
| 5 | Модальное окно управления вехами | Frontend | Этапы 4 + backend 2 |
| 6 | Отображение вех на таймлайне | Frontend | Этапы 4 + backend 3 |