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

840 lines
46 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: Ограничения секций в диаграмме Ганта — 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> «В секции &ldquo;[название секции]&rdquo; на [дата/диапазон] превышен лимит одновременных задач (максимум: 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> «В секции &ldquo;[название секции]&rdquo; задача пересекает блок &ldquo;[название блока]&rdquo; ([дата] — [дата])»</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 &gt;= 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">&gt;=</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 &gt;= 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 &gt;= 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">&#39;concurrency&#39;</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">&#39;block&#39;</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 &gt;= 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 &gt; 0: посчитать кол-во задач (исключая task_id), пересекающихся с [start_date, end_date] на каждый день. Если в любой день count &gt; 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">&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>