Реструктуризация: два глобальных раздела — Яша и Семен

Весь контент перенесён в content/yasha/ (7 подразделов).
Создан content/semen/ с подразделами: changelog, kb, diary.
Навигация — dropdown в навбаре, двухуровневое меню в hugo.toml.
Шаблоны, сайдбар, поиск обновлены под вложенные секции.
This commit is contained in:
Агальцов Антон
2026-04-26 19:44:52 +03:00
parent cc0b342208
commit dd67d2c2ec
249 changed files with 6900 additions and 9153 deletions
+329
View File
@@ -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 |