---
# System prepended metadata

title: Гант. API

---

# Гант. 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`
