Leadera: Вехи (Milestones) на диаграмме Ганта

📁 ~/Projects/docs-a2v/content/guides/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.

  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:

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

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/ (или соответствующем файле моделей) добавь:
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;
}
  1. Добавь milestones: GanttMilestone[] в интерфейс/модель полной загрузки данных диаграммы

  2. Создай сервис 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