Реструктуризация: два глобальных раздела — Яша и Семен
Весь контент перенесён в content/yasha/ (7 подразделов). Создан content/semen/ с подразделами: changelog, kb, diary. Навигация — dropdown в навбаре, двухуровневое меню в hugo.toml. Шаблоны, сайдбар, поиск обновлены под вложенные секции.
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
---
|
||||
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 |
|
||||
Reference in New Issue
Block a user