# Гант. API ## Словарь Между задачами существуе три типа связей 1. Связь ( C ) 2. Блок (Б) - окончание первой привязано к началу второй 3. Вложенность (В) Из-за особенностей библиотеки, мы не можем отображать связь В(В()), поэтому представляем её через В(C(В)) Пример: $a \rightarrow b \rightarrow c$, где $\rightarrow$ - связь типа "вложенность" Чтобы отобразить это, мы раскладываем $b$ на - $b_t$ (task) - волжена в задачу $a$ - $b_p$ (project) - в неё вложена задача $c$ Тогда $a \rightarrow b \rightarrow c \Leftrightarrow a \rightarrow b_t; b_p \rightarrow c$ ## Идеальная модель данных `*` - обязательные поля: ```javascript class Task { *id: string, *type: "project" | "task", // project - если задача является родительской в связи типа В *start: Date, // дата, как обычно, фронт распарсит *end: Date, // дата, как обычно, фронт распарсит *progress: int, // сейчас 0 везде dependencies: [], // id связей Б и С project: string, // id родителя связи В *displayOrder: int, // порядок вывода, расскажу ниже } ``` ### Как высчитать связи Сейчас связи на фронте можно получить только через запрос от каждой задачи, это очень неудобно для Ганта. Алгоритм следующий: 1. Получаем связи для всех имеющихся задач, начинаем, когда все загрузятся (условно кладём их в `task.links`) > Т.к. id задач должны быть универсальными, а в для отображения связи В(В()) нужно дублировать задачу, то я решила использовать **префиксы к id-шникам**: > - $p\_$ - project > - $t\_$ - task > ==Если дублируемая задача была с кем-то связана (Б, С), то связи повторяются на обе копии== 2. Заполняем массив `dependencies` - Выбираем связи нужного типа - Убираем повторяющиеся связи (если мы уже находили связь между этими задачами - не обрабатываем, сравниваем по префмксу $t\_$, как по минимальному необходимому) - Добавляем связь на id с префиксами $p\_$ и $t\_$ (project and task), т.к. мы пока не знаем, будут задачи дублироваться, или нет (==Этот момент можно исправить==) Кодом на js это так: ```javascript task.dependencies = task.links .filter((link) => ['Блокирует', 'Связана'].includes(link.connected_type)) .map((link) => link.task_id) .filter((link) => !accumulator.find((t) => t.id.includes(link) && t.dependencies?.includes('t_' + task.id))) .reduce((acc: string[], link: string) => [...acc, 'p_' + link, 't_' + link], []); ``` 3. Раздираемся с вложенностью - Достаём id задач, которые связаны через - "Входит в" (`projects`) - "Состоит из" (`children`) - Рассматриваем все 4 возможных случая: - Оба массива не пустые: - Добавляем к результату две копии себя: - ввиде проекта (меняем `id` (префикс $p\_$) и `type="project"`) + в `dependencies` добавляем свой `id` c префиксом $t\_$ ![](https://i.imgur.com/M0lgD8r.png) - ввиде задачи c проектом из `projects` (не забываем менять `id`) - Массив `projects` не пустой: - добавляем к результату себя ввиде задачи с проектом из `projects` - Массив `children` не пустой: - добавляем к результату себя ввиде проекта - Оба массива пусты - добавляем к результату себя ввиде задачи (меняем только `id`-шник) Код на js (Это всё в огромном reduce, который перекладывает массив в массив): ```javascript= // убираем дублирующуюся вложенность const projects = task.links.filter((link) => ['Входит в'].includes(link.connected_type)); const children = task.links.filter((link) => ['Состоит из'].includes(link.connected_type)); const selfProjectRepresentation = { ...task, id: 'p_' + task.id, type: 'project', hideChildren: false, }; if (projects.length && children.length) { // дублировать себя на ребёнка и родителя return [ ...accumulator, { ...task, id: 't_' + task.id, project: 'p_' + projects[0].task_id }, { ...selfProjectRepresentation, dependencies: [...task.dependencies, 't_' + task.id] }, ]; } if (projects.length) { // просто добавить родителя return [...accumulator, { ...task, id: 't_' + task.id, project: 'p_' + projects[0].task_id }]; } if (children.length) { // просто стать проектом return [...accumulator, selfProjectRepresentation]; } return [...accumulator, { ...task, id: 't_' + task.id }]; ``` 4. Сортируем данные Связь В отображается всегда, как задача, которая ниже корневой. Чтобы задать порядок, меняем поле `displayOrder` Для этого нам понадобится немного рекурсии 1. Обнаруживаем, какие ноды являются корневыми. Это те ноды, которые есть только с префиксом $p\_$ ```javascript // taskWithReducedLinks - результат прошлых шагов // projects, which are not tasks const rootNodes = taskWithReducedLinks.filter((task) => { const realTaskId = task.id.slice(2); return task.type === 'project' && !taskWithReducedLinks.find((t) => t.id === 't_' + realTaskId); }); ``` 2. Ударяем по ним рекурсивным методом (`orderForRoot(root: Task)`): Для понятности кода `counter` будем увеличивать и присваивать через функцию `nextOrder` ```javascript let counter = 1; const nextOrder = (task: Task) => { task.displayOrder = counter++; }; ``` 2.1. Присваимваем индекс `root`-у, т.к. он выше всех 2.2. Ищем всех тех, у кого он упомянут, как `project`, но тех, кто при этом является задачей (`task`) (это важно). Кладём, например, в массив `projectTasks` 2.3 Для каждого из `projectTasks`: - Присваиваем индекс (`nextOrder`) - Проверяем, существует ли такой же товарищ, но с префиксом $p\_$ в id-шнике - Если такой есть: даём индекс себе ввиде задачи, потом запускам рекурсию по себе ввиде проекта ```javascript= const orderForRoot = (root: Task) => { nextOrder(root); taskWithReducedLinks .filter((t) => t.project === root.id && t.type === 'task') .forEach((projectTask) => { const realTaskId = projectTask.id.slice(2); const project = taskWithReducedLinks.find((t) => t.id === 'p_' + realTaskId); nextOrder(projectTask); if (project) { orderForRoot(project); } }); }; rootNodes.forEach((root) => orderForRoot(root)); ``` 3. Добавляем в самом конце тех, кто избежал связи типа В: ```javascript // tasks without projects taskWithReducedLinks.forEach((task) => { if (!task.displayOrder) nextOrder(task); }); ``` ### Полный код (ну вдруг надо) ```javascript! const taskWithReducedLinks: Task[] = tasks.reduce((accumulator: Task[], task: ExtendedTask) => { // убираем повторяющиеся связи (смотрим на префикс t_) + расширяем их на id с префиксами p_ и t_ (project and task) task.dependencies = task.links .filter((link) => ['Блокирует', 'Связана'].includes(link.connected_type)) .map((link) => link.task_id) .filter((link) => !accumulator.find((t) => t.id.includes(link) && t.dependencies?.includes('t_' + task.id))) .reduce((acc: string[], link: string) => [...acc, 'p_' + link, 't_' + link], []); // убираем дублирующуюся вложенность const projects = task.links.filter((link) => ['Входит в'].includes(link.connected_type)); const children = task.links.filter((link) => ['Состоит из'].includes(link.connected_type)); const selfProjectRepresentation = { ...task, id: 'p_' + task.id, type: 'project', hideChildren: false, }; if (projects.length && children.length) { // дублировать себя на ребёнка и родителя return [ ...accumulator, { ...task, id: 't_' + task.id, project: 'p_' + projects[0].task_id }, { ...selfProjectRepresentation, dependencies: [...task.dependencies, 't_' + task.id] }, ]; } if (projects.length) { // просто добавить родителя return [...accumulator, { ...task, id: 't_' + task.id, project: 'p_' + projects[0].task_id }]; } if (children.length) { // просто стать проектом return [...accumulator, selfProjectRepresentation]; } return [...accumulator, { ...task, id: 't_' + task.id }]; }, []); // projects, which are not tasks const rootNodes = taskWithReducedLinks.filter((task) => { const realTaskId = task.id.slice(2); return task.type === 'project' && !taskWithReducedLinks.find((t) => t.id === 't_' + realTaskId); }); let counter = 1; const nextOrder = (task: Task) => { task.displayOrder = counter++; }; const orderForRoot = (root: Task) => { nextOrder(root); taskWithReducedLinks .filter((t) => t.project === root.id && t.type === 'task') .forEach((projectTask) => { const realTaskId = projectTask.id.slice(2); const project = taskWithReducedLinks.find((t) => t.id === 'p_' + realTaskId); nextOrder(projectTask); if (project) { orderForRoot(project); } }); }; rootNodes.forEach((root) => orderForRoot(root)); // tasks without projects taskWithReducedLinks.forEach((task) => { if (!task.displayOrder) nextOrder(task); }); setTasks(taskWithReducedLinks); } ``` ## Дополнительный функционал, который нужен будет со стороны бэка - Ручка на update задачи (временных границ): - Обновляет саму задачу - Обновляет все связанные задачи - Б - сдвигает только зависимую (Блокируемую) - В - родитель начинается с самой первой его подзадачей и заканчивается самой последней ## JSON-ы ### Идеальный json, который хочется: ```jsonld [ { start: "2023-01-01T14:51:30", end: "2023-02-15T14:51:30", name: "First Project", id: "p_0", progress: 0, type: "project", hideChildren: false, // это поле просто для проектов, его фронт и сам добавить может }, { start: "2023-01-01T14:51:30", end: "2023-02-09T14:51:30", name: "First Task", id: "t_0", progress: 0, project: "p_0", type: "task", }, { start: "2023-01-10T14:51:30", end: "2023-02-15T14:51:30", name: "Second Task", id: "t_1", progress: 0, type: "task", dependencies: ["p_0"], }, ]; ``` \* более подробное описание см "Идеальная модель данных", п.1 ### То, с чем работаю сейчас: - Endpoint для задач: `/api/ProjectTasksAll` - Endpoint для связей: `/api/TaskLinks`