840 lines
46 KiB
HTML
840 lines
46 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: Ограничения секций в диаграмме Ганта — 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: Ограничения секций в диаграмме Ганта</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-section-constraints.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-section-constraints.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-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
|
||
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
|
||
<p><strong>Поведение:</strong></p>
|
||
<ul>
|
||
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
|
||
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
|
||
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
|
||
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
|
||
</ul>
|
||
<p><strong>Срабатывание при:</strong></p>
|
||
<ol>
|
||
<li>Создании задачи — если добавление нарушает лимит → Alert</li>
|
||
<li>Перетаскивании задачи (drag) — если новая позиция нарушает лимит → Alert</li>
|
||
<li>Растягивании задачи (resize) — если новый размер нарушает лимит → Alert</li>
|
||
<li>Редактировании задачи (форма) — если изменённые даты нарушают лимит → Alert</li>
|
||
</ol>
|
||
<p><strong>Важно:</strong> Действие <strong>НЕ отменяется</strong>. Задача создаётся/перемещается в любом случае, но пользователь видит предупреждение.</p>
|
||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” на [дата/диапазон] превышен лимит одновременных задач (максимум: N)»</p>
|
||
<hr>
|
||
<h3 id="br-2-блоки-секций-section-blocks">BR-2: Блоки секций (Section Blocks)</h3>
|
||
<p>У каждой секции появляется набор <strong>блоков</strong> — особый вид записей, отображаемых на таймлайне как заштрихованные области.</p>
|
||
<p><strong>Сущность GanttSectionBlock:</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>section_id</td>
|
||
<td>UUID</td>
|
||
<td>Принадлежность к секции</td>
|
||
</tr>
|
||
<tr>
|
||
<td>title</td>
|
||
<td>string</td>
|
||
<td>Название (например: «Отпуск», «Командировка»)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>start_date</td>
|
||
<td>date</td>
|
||
<td>Начало интервала (минимум 1 день)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>end_date</td>
|
||
<td>date</td>
|
||
<td>Конец интервала</td>
|
||
</tr>
|
||
<tr>
|
||
<td>block_concurrency</td>
|
||
<td>boolean</td>
|
||
<td>Запрещать ли пересечение обычных задач с данным блоком</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p><strong>Валидация:</strong></p>
|
||
<ul>
|
||
<li><code>end_date</code> не может быть раньше <code>start_date</code></li>
|
||
<li>Все поля обязательны</li>
|
||
<li>Блок может быть в прошлом</li>
|
||
</ul>
|
||
<p><strong>Визуальное отображение на Ганте:</strong></p>
|
||
<ul>
|
||
<li>Заштрихованная область на <strong>всю высоту секции</strong> (включая все строки задач в секции)</li>
|
||
<li>Название блока отображается <strong>поверх штриховки с подложкой</strong> (для читаемости)</li>
|
||
</ul>
|
||
<p><strong>Поведение block_concurrency:</strong></p>
|
||
<ul>
|
||
<li>Если <code>block_concurrency = true</code> — при пересечении задачи с данным блоком показывается Alert (аналогично BR-1), но задача <strong>всё равно создаётся/перемещается</strong></li>
|
||
<li>Если <code>block_concurrency = false</code> — блок отображается визуально, но не влияет на задачи</li>
|
||
</ul>
|
||
<p><strong>Срабатывание при:</strong></p>
|
||
<ol>
|
||
<li>Создании задачи — пересекает блок с <code>block_concurrency = true</code> → Alert</li>
|
||
<li>Перетаскивании задачи (drag) → Alert</li>
|
||
<li>Растягивании задачи (resize) → Alert</li>
|
||
<li>Редактировании задачи (форма) → Alert</li>
|
||
</ol>
|
||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” задача пересекает блок “[название блока]” ([дата] — [дата])»</p>
|
||
<hr>
|
||
<h3 id="br-3-система-alert-ов">BR-3: Система Alert-ов</h3>
|
||
<p>Все предупреждения (от BR-1 и BR-2) выводятся в <strong>единую область</strong> над диаграммой Ганта.</p>
|
||
<p><strong>Поведение:</strong></p>
|
||
<ul>
|
||
<li>Alert-ы — Bootstrap Alert (warning), <strong>постоянные</strong></li>
|
||
<li>Пользователь <strong>НЕ может</strong> закрыть Alert вручную</li>
|
||
<li>Alert исчезает <strong>автоматически</strong>, когда текущая дата прошла дату конфликта (конфликт больше не актуален)</li>
|
||
<li>Проверка конфликтов — <strong>только на фронтенде</strong> (на основе загруженных данных: задачи секции, блоки, max_concurrent_tasks)</li>
|
||
<li>Если Alert-ов много — область <strong>сворачиваемая</strong> с счётчиком в бейдже (например: «⚠️ 3 предупреждения»)</li>
|
||
<li>По умолчанию — развёрнуто</li>
|
||
<li>Сворачивание/разворачивание — клик по заголовку области</li>
|
||
</ul>
|
||
<hr>
|
||
<h3 id="br-4-настройки-секции">BR-4: Настройки секции</h3>
|
||
<ul>
|
||
<li>Настройки совмещены с <strong>существующим модальным окном редактирования секции</strong> (не отдельное окно)</li>
|
||
</ul>
|
||
<p><strong>Окно создания секции</strong> — добавляется:</p>
|
||
<ul>
|
||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита, по умолчанию 0)</li>
|
||
</ul>
|
||
<p><strong>Окно редактирования секции</strong> — добавляется:</p>
|
||
<ul>
|
||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита)</li>
|
||
<li>Список блоков (таблица/список с CRUD: добавить, редактировать, удалить)
|
||
<ul>
|
||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||
<li>Каждый блок — форма: title, start_date, end_date, block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<hr>
|
||
<h3 id="br-5-наследование">BR-5: Наследование</h3>
|
||
<ul>
|
||
<li>При появлении функционала копирования/дублирования диаграммы — настройки секций (лимит + блоки) наследуются</li>
|
||
</ul>
|
||
<hr>
|
||
<h2 id="технические-требования">Технические требования</h2>
|
||
<h3 id="база-данных">База данных</h3>
|
||
<h4 id="новая-таблица-gantt_section_blocks">Новая таблица: gantt_section_blocks</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_section_blocks (
|
||
</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> section_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_sections(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> start_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||
</span></span><span style="display:flex;"><span> end_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||
</span></span><span style="display:flex;"><span> block_concurrency BOOLEAN <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#66d9ef">false</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 style="color:#66d9ef">CONSTRAINT</span> chk_block_dates <span style="color:#66d9ef">CHECK</span> (end_date <span style="color:#f92672">>=</span> start_date)
|
||
</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_section_blocks_section <span style="color:#66d9ef">ON</span> gantt_section_blocks(section_id);
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_dates <span style="color:#66d9ef">ON</span> gantt_section_blocks(start_date, end_date);
|
||
</span></span></code></pre></div><h4 id="изменение-таблицы-gantt_sections">Изменение таблицы: gantt_sections</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">ALTER</span> <span style="color:#66d9ef">TABLE</span> gantt_sections <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> max_concurrent_tasks INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#ae81ff">0</span>;
|
||
</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_section_blocks</code> и колонку <code>max_concurrent_tasks</code> в <code>gantt_sections</code>.</p>
|
||
<ol>
|
||
<li>Создай миграцию в <code>cmd/migrate/</code>:
|
||
<ul>
|
||
<li>Новая таблица <code>gantt_section_blocks</code> с полями: id (UUID PK), section_id (UUID FK → gantt_sections, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), start_date (DATE NOT NULL), end_date (DATE NOT NULL), block_concurrency (BOOLEAN DEFAULT false), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. CHECK constraint: end_date >= start_date. Индексы по section_id и (start_date, end_date).</li>
|
||
<li>ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0.</li>
|
||
</ul>
|
||
</li>
|
||
<li>Создай доменную модель <code>GanttSectionBlock</code> в <code>internal/domain/gantt.go</code> со всеми полями таблицы.</li>
|
||
<li>Добавь поле <code>MaxConcurrentTasks int</code> в структуру <code>GanttSection</code>.</li>
|
||
<li>Обнови маппинг в relevant repository файлах, чтобы новое поле <code>max_concurrent_tasks</code> читалось/писалось.</li>
|
||
</ol>
|
||
</blockquote>
|
||
<h4 id="этап-2-crud-для-ganttsectionblocks">Этап 2: CRUD для GanttSectionBlocks</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>Реализуй полный CRUD для сущности <code>GanttSectionBlock</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||
<p><strong>Роуты (внутри группы секции):</strong></p>
|
||
<ul>
|
||
<li><code>GET /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — список блоков секции</li>
|
||
<li><code>POST /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — создать блок</li>
|
||
<li><code>PUT /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — обновить блок</li>
|
||
<li><code>DELETE /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — удалить блок (soft delete)</li>
|
||
</ul>
|
||
<p><strong>Repository:</strong> <code>internal/repository/gantt_section_block_repository.go</code></p>
|
||
<ul>
|
||
<li><code>GetBySectionID(ctx, sectionID) ([]domain.GanttSectionBlock, error)</code></li>
|
||
<li><code>GetByID(ctx, id) (*domain.GanttSectionBlock, error)</code></li>
|
||
<li><code>Create(ctx, block) error</code></li>
|
||
<li><code>Update(ctx, block) 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_section_block_service.go</code></p>
|
||
<ul>
|
||
<li>Бизнес-логика: валидация dates (end >= start), проверка прав доступа к секции</li>
|
||
<li>Inject repository</li>
|
||
</ul>
|
||
<p><strong>Handler:</strong> <code>internal/handler/gantt_section_block_handler.go</code></p>
|
||
<ul>
|
||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||
<li>Валидация входных данных</li>
|
||
<li>Ошибки в формате проекта (GanttErrorResponse)</li>
|
||
</ul>
|
||
<p><strong>Модели запросов:</strong></p>
|
||
<ul>
|
||
<li>Create: title (required), start_date (required), end_date (required), block_concurrency (bool, default false)</li>
|
||
<li>Update: title, start_date, end_date, block_concurrency — все опциональны</li>
|
||
</ul>
|
||
</blockquote>
|
||
<h4 id="этап-3-обновление-настроек-секции">Этап 3: Обновление настроек секции</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>Обнови CRUD секций (gantt_sections) для поддержки нового поля <code>max_concurrent_tasks</code>.</p>
|
||
<ol>
|
||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionCreateRequest</code> (опциональное, int ≥ 0, default 0)</li>
|
||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionUpdateRequest</code> (опциональное, int ≥ 0)</li>
|
||
<li>Обнови SQL-запросы в repository: INSERT и UPDATE должны включать <code>max_concurrent_tasks</code></li>
|
||
<li>При GET секции (или списке секций) — возвращать <code>max_concurrent_tasks</code> в ответе</li>
|
||
<li>Валидация: значение должно быть ≥ 0</li>
|
||
</ol>
|
||
</blockquote>
|
||
<h4 id="этап-4-включение-блоков-в-ответ-секций">Этап 4: Включение блоков в ответ секций</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>При запросе списка секций или одной секции включай связанные блоки в ответ.</p>
|
||
<ol>
|
||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections/:sectionId</code> — добавить поле <code>blocks</code> с массивом GanttSectionBlock</li>
|
||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections</code> — добавить поле <code>blocks</code> для каждой секции</li>
|
||
<li>Блоки с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||
<li>Блоки сортируются по start_date по убыванию (будущее сверху)</li>
|
||
</ol>
|
||
</blockquote>
|
||
<hr>
|
||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>Добавь в проект поддержку ограничений секций Ганта.</p>
|
||
<ol>
|
||
<li>В <code>pages/gantt/models/gantt-chart.model.ts</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">GanttSectionBlock</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">section_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">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency</span>: <span style="color:#66d9ef">boolean</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span>: <span style="color:#66d9ef">UserInfo</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">GanttSectionBlockCreateRequest</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">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</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">GanttSectionBlockUpdateRequest</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">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</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">ConflictCheck</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">has_conflicts</span>: <span style="color:#66d9ef">boolean</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conflicts</span>: <span style="color:#66d9ef">ConflictItem</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">ConflictItem</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'concurrency'</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">'block'</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_title</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_title?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">message</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dates?</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>max_concurrent_tasks: number</code> в интерфейс <code>GanttSection</code></p>
|
||
</li>
|
||
<li>
|
||
<p>Добавь <code>blocks: GanttSectionBlock[]</code> в интерфейс <code>GanttSection</code></p>
|
||
</li>
|
||
<li>
|
||
<p>Добавь <code>max_concurrent_tasks?: number</code> в <code>GanttSectionCreateRequest</code> и <code>GanttSectionUpdateRequest</code></p>
|
||
</li>
|
||
<li>
|
||
<p>Создай сервис <code>pages/gantt/services/gantt-section-block.service.ts</code>:</p>
|
||
</li>
|
||
</ol>
|
||
<ul>
|
||
<li>Inject HttpClient</li>
|
||
<li>Методы: getBlocks(chartId, sectionId), createBlock(chartId, sectionId, data), updateBlock(chartId, sectionId, blockId, data), deleteBlock(chartId, sectionId, blockId)</li>
|
||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||
</ul>
|
||
</blockquote>
|
||
<h4 id="этап-fe-2-интеграция-в-модальные-окна-секции">Этап FE-2: Интеграция в модальные окна секции</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>Интегрируй настройки ограничений в существующие модальные окна создания и редактирования секции Ганта.</p>
|
||
<ol>
|
||
<li>Найди существующие компоненты модальных окон секции (создание/редактирование).</li>
|
||
</ol>
|
||
<p><strong>Окно создания секции</strong> — добавить:</p>
|
||
<ul>
|
||
<li>Числовой ввод: «Максимум одновременных задач»</li>
|
||
<li>Подсказка: «0 = без ограничений»</li>
|
||
<li>Валидация: число ≥ 0</li>
|
||
<li>Значение по умолчанию: 0</li>
|
||
<li>Отправляется в <code>GanttSectionCreateRequest</code> как <code>max_concurrent_tasks</code></li>
|
||
</ul>
|
||
<p><strong>Окно редактирования секции</strong> — добавить:</p>
|
||
<ul>
|
||
<li>Числовой ввод: «Максимум одновременных задач» (аналогично созданию)</li>
|
||
<li>Секция «Блоки (запретные интервалы)»:
|
||
<ul>
|
||
<li>Таблица/список существующих блоков с колонками: Название, Начало, Конец, Блокировка (чекбокс/иконка), Действия (редактировать/удалить)</li>
|
||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||
<li>Кнопка «Добавить блок» — inline-форма:
|
||
<ul>
|
||
<li>title (текстовый ввод, обязательный)</li>
|
||
<li>start_date (datepicker, обязательный)</li>
|
||
<li>end_date (datepicker, обязательный)</li>
|
||
<li>block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||
</ul>
|
||
</li>
|
||
<li>Редактирование — inline или через ту же форму</li>
|
||
<li>Удаление — с подтверждением</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<ol start="2">
|
||
<li>При открытии окна редактирования — загрузить текущие настройки секции + блоки</li>
|
||
<li>При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков</li>
|
||
<li>Инжектировать GanttChartService и GanttSectionBlockService</li>
|
||
</ol>
|
||
</blockquote>
|
||
<h4 id="этап-fe-3-визуализация-блоков-на-ганте">Этап FE-3: Визуализация блоков на Ганте</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>Отобрази блоки секций (GanttSectionBlock) на canvas-диаграмме Ганта.</p>
|
||
<p>Проект использует Konva.js для рендеринга. Основной компонент: <code>gantt-chart-canvas</code>.</p>
|
||
<ol>
|
||
<li>При загрузке данных диаграммы — извлечь блоки из каждой секции</li>
|
||
<li>В рендерере секций (canvas/timeline-renderer.ts или аналогичный) — для каждого блока:
|
||
<ul>
|
||
<li>Нарисовать заштрихованную область (diagonal hatch pattern) на <strong>всю высоту секции</strong> (все строки задач в этой секции)</li>
|
||
<li>Область ограничена по горизонтали: от start_date до end_date (в пикселях по шкале времени)</li>
|
||
<li>Цвет штриховки: полупрозрачный серый или configurable</li>
|
||
<li>Поверх штриховки — текст с названием блока (block.title) на подложке (прямоугольник с фоном, чтобы текст читался)</li>
|
||
</ul>
|
||
</li>
|
||
<li>Блоки рендерятся <strong>до</strong> задач (z-index ниже), чтобы задачи были поверх</li>
|
||
<li>Блоки с <code>block_concurrency = true</code> и <code>false</code> визуально не отличаются (штриховка одинаковая)</li>
|
||
<li>При скролле/зуме — блоки пересчитываются как обычные элементы таймлайна</li>
|
||
</ol>
|
||
</blockquote>
|
||
<h4 id="этап-fe-4-система-alert-ов">Этап FE-4: Система Alert-ов</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>Создай компонент для отображения Alert-ов о конфликтах на диаграмме Ганта.</p>
|
||
<ol>
|
||
<li>
|
||
<p>Создай компонент <code>pages/gantt/components/gantt-conflict-alerts/</code></p>
|
||
</li>
|
||
<li>
|
||
<p>Компонент размещается <strong>над</strong> canvas-диаграммой Ганта</p>
|
||
</li>
|
||
<li>
|
||
<p>Входные данные: массив <code>ConflictItem[]</code> (из модели)</p>
|
||
</li>
|
||
<li>
|
||
<p>Визуал:</p>
|
||
<ul>
|
||
<li>Bootstrap Alert (warning) для каждого конфликта</li>
|
||
<li>Каждый Alert показывает message из ConflictItem</li>
|
||
<li><strong>Без кнопки закрытия</strong> — Alert нельзя закрыть вручную</li>
|
||
<li>Alert исчезает автоматически, когда текущая дата прошла дату конфликта</li>
|
||
<li>Область <strong>сворачиваемая</strong>: заголовок «⚠️ Предупреждения (N)» — клик сворачивает/разворачивает список</li>
|
||
<li>По умолчанию — развёрнуто</li>
|
||
<li>При 0 конфликтов — область скрыта</li>
|
||
</ul>
|
||
</li>
|
||
<li>
|
||
<p>Состояние хранится в сервисе или в компоненте диаграммы:</p>
|
||
<ul>
|
||
<li>Добавление нового конфликта → area auto-expands</li>
|
||
<li>Alert удаляется из массива <strong>автоматически</strong>, когда текущая дата прошла дату конфликта</li>
|
||
<li>Проверка актуальности конфликтов — при каждой отрисовке / изменении данных</li>
|
||
<li>Не использовать toast — Alert-ы постоянные</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
</blockquote>
|
||
<h4 id="этап-fe-5-проверка-конфликтов-при-операциях-с-задачами">Этап FE-5: Проверка конфликтов при операциях с задачами</h4>
|
||
<p><strong>Промт для агента:</strong></p>
|
||
<blockquote>
|
||
<p>Интегрируй проверку конфликтов во все операции с задачами на диаграмме Ганта.</p>
|
||
<p>Проект использует interaction-handler.ts для drag/resize и формы для создания/редактирования.</p>
|
||
<p><strong>Важно:</strong> Проверка конфликтов — <strong>только на фронтенде</strong>, без вызова API. Используются загруженные данные секции (задачи, блоки, max_concurrent_tasks).</p>
|
||
<ol>
|
||
<li>
|
||
<p>Вынеси логику проверки конфликтов <code>checkConflicts()</code> в отдельный метод в gantt-canvas.service.ts:</p>
|
||
<ul>
|
||
<li>Принимает: section (GanttSection — содержит max_concurrent_tasks и blocks), tasks (GanttTask[] — задачи секции), task_id (nullable — исключить текущую задачу), start_date, end_date</li>
|
||
<li>Логика:
|
||
a. Если section.max_concurrent_tasks > 0: посчитать кол-во задач (исключая task_id), пересекающихся с [start_date, end_date] на каждый день. Если в любой день count > max_concurrent_tasks — добавить конфликт concurrency
|
||
b. Для каждого блока секции с block_concurrency = true, пересекающегося с [start_date, end_date] — добавить конфликт block</li>
|
||
<li>Возвращает ConflictCheck</li>
|
||
</ul>
|
||
</li>
|
||
<li>
|
||
<p>После <strong>каждой</strong> успешной операции с задачей (создание, обновление, drag, resize):</p>
|
||
<ul>
|
||
<li>Вызвать checkConflicts с параметрами задачи</li>
|
||
<li>Если has_conflicts = true — передать конфликты в GanttConflictAlertsComponent</li>
|
||
<li>Alert-ы показываются, действие <strong>НЕ отменяется</strong></li>
|
||
</ul>
|
||
</li>
|
||
<li>
|
||
<p>Точки интеграции:</p>
|
||
<ul>
|
||
<li><strong>Создание задачи</strong> (форма): после успешного POST → checkConflicts → show alerts</li>
|
||
<li><strong>Редактирование задачи</strong> (форма): после успешного PUT → checkConflicts → show alerts</li>
|
||
<li><strong>Drag задачи</strong> (interaction-handler.ts): после успешного обновления позиции → checkConflicts → show alerts</li>
|
||
<li><strong>Resize задачи</strong> (interaction-handler.ts): после успешного обновления размера → checkConflicts → show alerts</li>
|
||
</ul>
|
||
</li>
|
||
<li>
|
||
<p>При переключении диаграммы или секции — очищать Alert-ы</p>
|
||
</li>
|
||
<li>
|
||
<p>Alert-ы автоматически удаляются, когда текущая дата прошла дату конфликта — область скрывается, когда конфликтов не осталось</p>
|
||
</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>Этап 1</td>
|
||
</tr>
|
||
<tr>
|
||
<td>4</td>
|
||
<td>Включение блоков в ответ секций</td>
|
||
<td>Backend</td>
|
||
<td>Этап 2</td>
|
||
</tr>
|
||
<tr>
|
||
<td>5</td>
|
||
<td>Модели + Сервис</td>
|
||
<td>Frontend</td>
|
||
<td>— (можно параллельно с backend)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>6</td>
|
||
<td>Интеграция в модальные окна</td>
|
||
<td>Frontend</td>
|
||
<td>Этапы 5 + backend 2,3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>7</td>
|
||
<td>Визуализация блоков на Ганте</td>
|
||
<td>Frontend</td>
|
||
<td>Этапы 5 + backend 4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>8</td>
|
||
<td>Система Alert-ов</td>
|
||
<td>Frontend</td>
|
||
<td>Этап 5</td>
|
||
</tr>
|
||
<tr>
|
||
<td>9</td>
|
||
<td>Проверка конфликтов при операциях</td>
|
||
<td>Frontend</td>
|
||
<td>Этапы 5,7,8</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>
|