Files
docs.a2v.space/public/guides/leadera-milestones/index.html
T
2026-04-26 17:09:31 +03:00

777 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru-ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Leadera: Вехи (Milestones) на диаграмме Ганта — docs.a2v.space</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../../css/custom.css">
</head>
<body>
<header class="site-header">
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href="../../">docs.a2v.space</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarMain">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="../../diary/">Дневник</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../../kb/">База знаний</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../../plans/">Планы</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../../guides/">Инструкции</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../../creds/">Креды</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
</li>
</ul>
<form class="d-flex search-form" role="search">
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
<button class="btn btn-outline-light" type="submit">Найти</button>
</form>
</div>
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
</svg>
</button>
</div>
</nav>
<div id="search-results" class="search-results" style="display: none;">
<div class="search-results-content">
<button type="button" class="btn-close float-end" id="close-search"></button>
<h5>Результаты поиска</h5>
<ul id="results-list"></ul>
</div>
</div>
</header>
<div class="container-fluid">
<div class="row">
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
<div class="sidebar-content">
<h6>Фильтры</h6>
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
</ul>
<h6 class="mt-3">Теги</h6>
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
</ul>
</div>
</aside>
<main class="col-md-9 col-lg-10 main-content">
<div class="article-content">
<h1>Leadera: Вехи (Milestones) на диаграмме Ганта</h1>
<div class="source-path mb-3">
<small class="text-muted">
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-milestones.md</code>
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-milestones.md')" title="Копировать путь">
📋
</button>
</small>
</div>
<div class="tags mb-3">
<a href="../../tags/leadera/" class="tag">leadera</a>
<a href="../../tags/gantt/" class="tag">gantt</a>
<a href="../../tags/angular/" class="tag">angular</a>
<a href="../../tags/go/" class="tag">go</a>
<a href="../../tags/api/" class="tag">api</a>
<a href="../../tags/ux/" class="tag">ux</a>
</div>
<h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
<p><strong>Поля вехи:</strong></p>
<table>
<thead>
<tr>
<th>Поле</th>
<th>Тип</th>
<th>Описание</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>UUID</td>
<td>Идентификатор</td>
</tr>
<tr>
<td>gantt_chart_id</td>
<td>UUID</td>
<td>Принадлежность к диаграмме (не к секции!)</td>
</tr>
<tr>
<td>title</td>
<td>string</td>
<td>Название вехи (обязательное, 1–255 символов)</td>
</tr>
<tr>
<td>date</td>
<td>date</td>
<td>Дата вехи (без времени, обязательная)</td>
</tr>
<tr>
<td>color</td>
<td>string</td>
<td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
</tr>
<tr>
<td>created_by</td>
<td>UUID</td>
<td>Кто создал (не отображается в UI, хранится для аудита)</td>
</tr>
<tr>
<td>updated_by</td>
<td>UUID</td>
<td>Кто обновил (не отображается в UI)</td>
</tr>
<tr>
<td>created_at</td>
<td>timestamp</td>
<td>Дата создания</td>
</tr>
<tr>
<td>updated_at</td>
<td>timestamp</td>
<td>Дата обновления</td>
</tr>
<tr>
<td>deleted_at</td>
<td>timestamp</td>
<td>Soft delete</td>
</tr>
</tbody>
</table>
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p>
<p><strong>Особенности:</strong></p>
<ul>
<li>Вех может быть несколько в проекте</li>
<li>Несколько вех могут иметь одну и ту же дату</li>
<li>Вехи сортируются строго по дате (по возрастанию)</li>
<li>Вехи не привязаны к секциям — они отображаются <strong>сквозь все секции</strong></li>
</ul>
<hr>
<h3 id="br-2-управление-вехами-модальное-окно">BR-2: Управление вехами (модальное окно)</h3>
<p><strong>Вызов:</strong> Кнопка «Вехи» рядом с кнопкой «Создать секцию» на странице диаграммы.</p>
<p><strong>Модальное окно содержит:</strong></p>
<ul>
<li>Заголовок: «Вехи проекта»</li>
<li>Список всех вех текущей диаграммы (сортировка по дате по возрастанию)</li>
<li>Каждая веха в списке показывает: цвет (индикатор), название, дату, кнопки «Редактировать» и «Удалить»</li>
<li>Кнопка «Добавить веху»</li>
</ul>
<p><strong>Форма добавления/редактирования вехи:</strong></p>
<ul>
<li>Название — текстовый ввод (обязательное, 1–255 символов)</li>
<li>Дата — datepicker (обязательное)</li>
<li>Цвет — выбор цвета (по аналогии с палитрой цветов в задачах)</li>
<li>По умолчанию цвет = красный (<code>#dc3545</code>)</li>
</ul>
<p><strong>Удаление:</strong> с подтверждением (confirm dialog).</p>
<hr>
<h3 id="br-3-отображение-вех-на-таймлайне">BR-3: Отображение вех на таймлайне</h3>
<p><strong>Визуальное представление:</strong></p>
<ul>
<li><strong>Маркер-флажок</strong> (flag marker) на шкале таймлайна</li>
<li>От маркера вниз идёт <strong>пунктирная вертикальная линия</strong> через все секции (на всю высоту диаграммы)</li>
<li>Цвет линии и маркера = цвет вехи</li>
</ul>
<p><strong>Позиционирование:</strong></p>
<ul>
<li>Веха ставится ровно на свою дату на шкале времени</li>
<li>Если дата вехи выходит за рамки текущего диапазона задач — таймлайн <strong>расширяется</strong> (аналогично задачам)</li>
<li>При изменении масштаба (days/weeks/months/quarters) позиция вехи пересчитывается</li>
</ul>
<p><strong>Tooltip при наведении:</strong></p>
<ul>
<li>При наведении на маркер/линию — показывается tooltip с названием вехи</li>
<li>Если на одну дату приходится несколько вех — в tooltip показываются <strong>все</strong> вехи этой даты с их названиями (каждая с индикацией своего цвета)</li>
</ul>
<hr>
<h2 id="технические-требования">Технические требования</h2>
<h3 id="база-данных">База данных</h3>
<h4 id="новая-таблица-gantt_milestones">Новая таблица: gantt_milestones</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_milestones (
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
</span></span><span style="display:flex;"><span> gantt_chart_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_charts(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span> date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span> color VARCHAR(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#e6db74">&#39;#dc3545&#39;</span>,
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_chart <span style="color:#66d9ef">ON</span> gantt_milestones(gantt_chart_id);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_date <span style="color:#66d9ef">ON</span> gantt_milestones(date);
</span></span></code></pre></div><hr>
<h3 id="backend-go-api">Backend (Go API)</h3>
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
<p><strong>Промт для агента:</strong></p>
<blockquote>
<p>Добавь в проект новую таблицу <code>gantt_milestones</code>.</p>
<ol>
<li>
<p>Создай миграцию в <code>cmd/migrate/</code>:</p>
<ul>
<li>Новая таблица <code>gantt_milestones</code> с полями: 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 &lsquo;#dc3545&rsquo;), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. Индексы по gantt_chart_id и date.</li>
</ul>
</li>
<li>
<p>Создай доменную модель <code>GanttMilestone</code> в <code>internal/domain/gantt.go</code>:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestone</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:&#34;id&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:&#34;gantt_chart_id&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;title&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;date&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;color&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:&#34;created_by&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:&#34;updated_by&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;created_at&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;updated_at&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">DeletedAt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;deleted_at,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></blockquote>
<h4 id="этап-2-crud-для-ganttmilestones">Этап 2: CRUD для GanttMilestones</h4>
<p><strong>Промт для агента:</strong></p>
<blockquote>
<p>Реализуй полный CRUD для сущности <code>GanttMilestone</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
<p><strong>Роуты:</strong></p>
<ul>
<li><code>GET /api/gantt-charts/:chartId/milestones</code> — список вех диаграммы</li>
<li><code>POST /api/gantt-charts/:chartId/milestones</code> — создать веху</li>
<li><code>PUT /api/gantt-charts/:chartId/milestones/:milestoneId</code> — обновить веху</li>
<li><code>DELETE /api/gantt-charts/:chartId/milestones/:milestoneId</code> — удалить веху (soft delete)</li>
</ul>
<p><strong>Repository:</strong> <code>internal/repository/gantt_milestone_repository.go</code></p>
<ul>
<li><code>GetByChartID(ctx, chartID) ([]domain.GanttMilestone, error)</code> — все неудалённые вехи диаграммы, отсортированные по date ASC</li>
<li><code>GetByID(ctx, id) (*domain.GanttMilestone, error)</code></li>
<li><code>Create(ctx, milestone) error</code></li>
<li><code>Update(ctx, milestone) error</code></li>
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
</ul>
<p><strong>Service:</strong> <code>internal/service/gantt_milestone_service.go</code></p>
<ul>
<li>Валидация: title (1–255 символов), date (обязательное), color (валидный HEX, default &lsquo;#dc3545&rsquo;)</li>
<li>Проверка прав доступа: пользователь должен иметь права выше «только чтение» в пространстве (space) диаграммы</li>
<li>Проверка что chartId существует и не архивирован</li>
</ul>
<p><strong>Handler:</strong> <code>internal/handler/gantt_milestone_handler.go</code></p>
<ul>
<li>Swagger-аннотации для всех эндпоинтов</li>
<li>Валидация входных данных</li>
<li>Ошибки в формате проекта</li>
</ul>
<p><strong>DTO:</strong> <code>internal/dto/gantt/milestones_dto.go</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CreateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;title&#34; binding:&#34;required,min=1,max=255&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;date&#34; binding:&#34;required&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;color&#34; binding:&#34;omitempty,max=20&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UpdateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;title&#34; binding:&#34;omitempty,min=1,max=255&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;date&#34; binding:&#34;omitempty&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;color&#34; binding:&#34;omitempty,max=20&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:&#34;id&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:&#34;gantt_chart_id&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;title&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;date&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;color&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:&#34;created_by_user&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:&#34;updated_by_user&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;created_at&#34;`</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;updated_at&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestonesListResponse</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Milestones</span> []<span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#e6db74">`json:&#34;milestones&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Регистрация роутов:</strong> Добавь группу milestone-роутов в роутер рядом с существующими роутами секций/задач. Проверка прав через существующий middleware.</p>
</blockquote>
<h4 id="этап-3-включение-вех-в-ответ-диаграммы">Этап 3: Включение вех в ответ диаграммы</h4>
<p><strong>Промт для агента:</strong></p>
<blockquote>
<p>При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.</p>
<ol>
<li>Добавь эндпоинт или расширь существующий <code>GET /api/gantt-charts/:chartId</code> — включить поле <code>milestones</code> с массивом <code>GanttMilestoneResponse</code></li>
<li>Вехи с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
<li>Вехи сортируются по <code>date</code> ASC</li>
<li>Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта</li>
</ol>
</blockquote>
<hr>
<h3 id="frontend-angular">Frontend (Angular)</h3>
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
<p><strong>Промт для агента:</strong></p>
<blockquote>
<p>Добавь в проект поддержку вех (milestones) на диаграмме Ганта.</p>
<ol>
<li>В <code>pages/gantt/models/</code> (или соответствующем файле моделей) добавь:</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestone</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">gantt_chart_id</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneCreateRequest</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneUpdateRequest</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date?</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ol start="2">
<li>
<p>Добавь <code>milestones: GanttMilestone[]</code> в интерфейс/модель полной загрузки данных диаграммы</p>
</li>
<li>
<p>Создай сервис <code>pages/gantt/services/gantt-milestone.service.ts</code>:</p>
</li>
</ol>
<ul>
<li>Inject HttpClient</li>
<li>Методы:
<ul>
<li><code>getMilestones(chartId: string): Observable&lt;GanttMilestone[]&gt;</code></li>
<li><code>createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable&lt;GanttMilestone&gt;</code></li>
<li><code>updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable&lt;GanttMilestone&gt;</code></li>
<li><code>deleteMilestone(chartId: string, milestoneId: string): Observable&lt;void&gt;</code></li>
</ul>
</li>
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
</ul>
</blockquote>
<h4 id="этап-fe-2-модальное-окно-управления-вехами">Этап FE-2: Модальное окно управления вехами</h4>
<p><strong>Промт для агента:</strong></p>
<blockquote>
<p>Создай компонент модального окна для управления вехами диаграммы Ганта.</p>
<ol>
<li>
<p>Создай компонент <code>pages/gantt/components/milestones-modal/</code></p>
</li>
<li>
<p><strong>Кнопка вызова:</strong> Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.</p>
</li>
<li>
<p><strong>Модальное окно</strong> (ng-bootstrap modal):</p>
<ul>
<li>Заголовок: «Вехи проекта»</li>
<li>Список вех (сортировка по date ASC):
<ul>
<li>Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)</li>
</ul>
</li>
<li>Кнопка «Добавить веху» внизу списка</li>
<li>Если вех нет — сообщение «Вех пока нет. Добавьте первую!»</li>
</ul>
</li>
<li>
<p><strong>Форма добавления/редактирования</strong> (inline в модальном окне):</p>
<ul>
<li>Название — текстовый ввод (обязательное)</li>
<li>Дата — ng-bootstrap datepicker (обязательное)</li>
<li>Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)</li>
<li>По умолчанию цвет = <code>#dc3545</code> (красный)</li>
<li>Кнопки «Сохранить» и «Отмена»</li>
</ul>
</li>
<li>
<p>При сохранении — вызов соответствующего метода сервиса (create/update)</p>
</li>
<li>
<p>При удалении — confirm dialog → вызов deleteMilestone</p>
</li>
<li>
<p>После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)</p>
</li>
<li>
<p>Инжектировать GanttMilestoneService и NgbModal</p>
</li>
</ol>
</blockquote>
<h4 id="этап-fe-3-отображение-вех-на-таймлайне-konva-canvas">Этап FE-3: Отображение вех на таймлайне (Konva canvas)</h4>
<p><strong>Промт для агента:</strong></p>
<blockquote>
<p>Отобрази вехи (milestones) на canvas-диаграмме Ганта.</p>
<p>Проект использует Konva.js для рендеринга.</p>
<ol>
<li>
<p>При загрузке данных диаграммы — извлечь массив <code>milestones</code></p>
</li>
<li>
<p>Для каждой вехи нарисовать на canvas:</p>
<ul>
<li><strong>Маркер-флажок</strong> (flag marker) на позиции даты вехи на шкале времени
<ul>
<li>Стиль: маленький треугольник или флаг на верхней границе таймлайна</li>
<li>Цвет = milestone.color</li>
</ul>
</li>
<li><strong>Пунктирная вертикальная линия</strong> от маркера вниз через все секции (на всю высоту области секций)
<ul>
<li>Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Позиционирование:</strong></p>
<ul>
<li>Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)</li>
<li>Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)</li>
<li>При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех</li>
<li>При скролле — вехи скроллятся вместе с задачами</li>
</ul>
</li>
<li>
<p><strong>Tooltip при наведении:</strong></p>
<ul>
<li>При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
<ul>
<li>Если веха одна на эту дату — показать название</li>
<li>Если несколько вех на одну дату — показать список: цветной кружок + название для каждой</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Z-index:</strong></p>
<ul>
<li>Линии вех рисуются <strong>поверх</strong> фона секций, но <strong>под</strong> задачами</li>
<li>Маркеры-флажки — поверх всего (самый высокий z-index)</li>
</ul>
</li>
<li>
<p><strong>Реактивность:</strong></p>
<ul>
<li>При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)</li>
<li>Подписаться на изменения массива milestones через сервис/store</li>
</ul>
</li>
</ol>
</blockquote>
<hr>
<h2 id="порядок-реализации">Порядок реализации</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Этап</th>
<th>Сторона</th>
<th>Зависимости</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Миграция + Домен</td>
<td>Backend</td>
<td></td>
</tr>
<tr>
<td>2</td>
<td>CRUD вех</td>
<td>Backend</td>
<td>Этап 1</td>
</tr>
<tr>
<td>3</td>
<td>Включение вех в ответ диаграммы</td>
<td>Backend</td>
<td>Этап 2</td>
</tr>
<tr>
<td>4</td>
<td>Модели + Сервис</td>
<td>Frontend</td>
<td>— (параллельно с backend)</td>
</tr>
<tr>
<td>5</td>
<td>Модальное окно управления вехами</td>
<td>Frontend</td>
<td>Этапы 4 + backend 2</td>
</tr>
<tr>
<td>6</td>
<td>Отображение вех на таймлайне</td>
<td>Frontend</td>
<td>Этапы 4 + backend 3</td>
</tr>
</tbody>
</table>
</div>
</main>
</div>
</div>
<footer class="site-footer">
<div class="container">
<p class="text-center mb-0">&copy; 2026 docs.a2v.space</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
<script src="../../js/search.js"></script>
<script src="../../js/creds.js"></script>
</body>
</html>