777 lines
42 KiB
HTML
777 lines
42 KiB
HTML
<!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">'#dc3545'</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 ‘#dc3545’), 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:"id"`</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:"gantt_chart_id"`</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:"title"`</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:"date"`</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:"color"`</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:"created_by"`</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:"updated_by"`</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:"created_at"`</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:"updated_at"`</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:"deleted_at,omitempty"`</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 ‘#dc3545’)</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:"title" binding:"required,min=1,max=255"`</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:"date" binding:"required"`</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:"color" binding:"omitempty,max=20"`</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:"title" binding:"omitempty,min=1,max=255"`</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:"date" binding:"omitempty"`</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:"color" binding:"omitempty,max=20"`</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:"id"`</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:"gantt_chart_id"`</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:"title"`</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:"date"`</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:"color"`</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:"created_by_user"`</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:"updated_by_user"`</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:"created_at"`</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:"updated_at"`</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:"milestones"`</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<GanttMilestone[]></code></li>
|
||
<li><code>createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable<GanttMilestone></code></li>
|
||
<li><code>updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable<GanttMilestone></code></li>
|
||
<li><code>deleteMilestone(chartId: string, milestoneId: string): Observable<void></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">© 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>
|