# Гант. 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\_$

- ввиде задачи 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`