--- 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` > - `createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable` > - `updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable` > - `deleteMilestone(chartId: string, milestoneId: string): Observable` > - Все методы возвращают 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 |