YetiForceCompany/YetiForceCRM

View on GitHub
modules/Project/models/Gantt.php

Summary

Maintainability
F
6 days
Test Coverage
F
0%
<?php

/**
 * Gantt Model class.
 *
 * @package Model
 *
 * @copyright YetiForce S.A.
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
 * @author    Rafal Pospiech <r.pospiech@yetiforce.com>
 * @author    Radosław Skrzypczak <r.skrzypczak@yetiforce.com>
 */
class Project_Gantt_Model
{
    /**
     * @var array project tasks,milesones and projects
     */
    private $tasks = [];

    /**
     * @var array rootNode needed for tree generation process
     */
    private $rootNode;

    /**
     * @var array task nodes as tree with children
     */
    private $tree = [];

    /**
     * @var array colors for statuses
     */
    public $statusColors = [];

    /**
     * @var array if some task is already loaded get it from here
     */
    private $tasksById = [];

    /**
     * @var array statuses - with closing value
     */
    private $statuses;

    /**
     * @var array - without closing value - for JS filter
     */
    private $activeStatuses;

    /**
     * Get parent nodes id as associative array [taskId]=>[parentId1,parentId2,...].
     *
     * @param int|string $parentId
     * @param array      $parents  initial value
     *
     * @return array
     */
    private function getParentRecordsIdsRecursive($parentId, $parents = [])
    {
        if (empty($parentId)) {
            return $parents;
        }
        if (!\in_array($parentId, $parents)) {
            $parents[] = $parentId;
        }
        foreach ($this->tasks as $task) {
            if ($task['id'] === $parentId) {
                if (!empty($task['parent'])) {
                    $parents = $this->getParentRecordsIdsRecursive($task['parent'], $parents);
                }
                break;
            }
        }
        return $parents;
    }

    /**
     * Collect all parents of all tasks.
     *
     * @return array
     */
    private function getAllParentRecordsIds()
    {
        $parents = [];
        foreach ($this->tasks as $task) {
            if (!empty($task['parent'])) {
                $parents[$task['id']] = $this->getParentRecordsIdsRecursive($task['parent']);
            } else {
                $parents[$task['id']] = [];
            }
        }
        return $parents;
    }

    /**
     * Calculate task levels and dependencies.
     */
    private function calculateLevels()
    {
        $parents = $this->getAllParentRecordsIds();
        foreach ($this->tasks as &$task) {
            $task['level'] = \count($parents[$task['id']]);
            $task['parents'] = $parents[$task['id']];
        }
        unset($task);
        $hasChild = [];
        foreach ($parents as $parentsId) {
            foreach ($parentsId as $parentId) {
                if (!\in_array($parentId, $hasChild)) {
                    $hasChild[] = $parentId;
                }
            }
        }
        foreach ($this->tasks as &$task) {
            if (\in_array($task['id'], $hasChild)) {
                $task['hasChild'] = true;
            } else {
                $task['hasChild'] = false;
            }
        }
        unset($parents);
    }

    /**
     * Calculate duration in seconds.
     *
     * @param string $startDateStr
     * @param string $endDateStr
     *
     * @return int
     */
    private function calculateDuration($startDateStr, $endDateStr): int
    {
        return ((int) (new DateTime($startDateStr))->diff(new DateTime($endDateStr), true)->format('%a')) * 24 * 60 * 60 * 1000;
    }

    /**
     * Normalize task parent property set as 0 if not exists (root node).
     */
    private function normalizeParents()
    {
        // not set parents are children of root node
        foreach ($this->tasks as &$task) {
            if (!isset($task['parent']) && 0 !== $task['id']) {
                $task['parent'] = 0;
            }
        }
        // if parent id is set but we don't have it - it means that project is subproject so connect it to root node
        foreach ($this->tasks as &$task) {
            if (!empty($task['parent'])) {
                $idExists = false;
                foreach ($this->tasks as $parent) {
                    if ($task['parent'] === $parent['id']) {
                        $idExists = true;
                        break;
                    }
                }
                if (!$idExists) {
                    $task['parent'] = 0;
                }
            }
        }
    }

    /**
     * Collect task all parent nodes.
     *
     * @param array $task
     *
     * @return array task with parents property int[]
     */
    private function &getRecordWithChildren(&$task)
    {
        foreach ($this->tasks as &$child) {
            if (isset($child['parent']) && $child['parent'] === $task['id']) {
                if (empty($task['children'])) {
                    $task['children'] = [];
                }
                $task['children'][] = &$this->getRecordWithChildren($child);
            }
        }
        return $task;
    }

    /**
     * Flatten task tree with proper order to use it in frontend gantt lib.
     *
     * @param       $nodes tasks tree
     * @param array $flat  initial array
     *
     * @return task[]
     */
    private function flattenRecordTasks($nodes, $flat = [])
    {
        foreach ($nodes as $node) {
            $flat[] = $node;
            if (!empty($node['children'])) {
                $flat = $this->flattenRecordTasks($node['children'], $flat);
            }
        }
        return $flat;
    }

    /**
     * Sort all node types (task,milestones,projects) so each parent task is before its child (frontend lib needs this).
     */
    private function collectChildrens()
    {
        $this->tree = &$this->getRecordWithChildren($this->rootNode);
    }

    /**
     * Add root node to generate tree structure.
     */
    private function addRootNode()
    {
        $this->rootNode = ['id' => 0];
        array_unshift($this->tasks, $this->rootNode);
    }

    /**
     * Remove root node and children because they are not needed anymore.
     *
     * @param task[] $tasks
     *
     * @return task[] new array (not mutated)
     */
    private function cleanup($tasks)
    {
        $clean = [];
        foreach ($tasks as $task) {
            if (0 !== $task['id']) {
                if (0 === $task['parent']) {
                    unset($task['parent']);
                    $task['depends'] = '';
                }
                if (isset($task['children'])) {
                    unset($task['children']);
                }
                if (isset($task['parents'])) {
                    unset($task['parents']);
                }
                if (isset($task['depends'])) {
                    unset($task['depends']);
                }
                if (!isset($task['progress'])) {
                    $task['progress'] = 100;
                }
                $clean[] = $task;
            }
        }
        return $clean;
    }

    /**
     * Iterate through all tasks in tree.
     *
     * @param array    $node         starting point - might by rootNode
     * @param mixed    $currentValue initial result which will be evaluated if there are some child nodes like array reduce
     * @param callable $callback     what to do with task
     *
     * @return mixed
     */
    public function iterateNodes(&$node, $currentValue, $callback)
    {
        if (empty($node['children'])) {
            return $currentValue;
        }
        foreach ($node['children'] as &$child) {
            $currentValue = $callback($child, $currentValue);
            if (!empty($child['children'])) {
                $currentValue = $this->iterateNodes($child, $currentValue, $callback);
            }
        }
        return $currentValue;
    }

    /**
     * Iterate through children and search for start date.
     *
     * @param array $node
     *
     * @return int timestamp
     */
    private function findOutStartDates(&$node)
    {
        $maxTimeStampValue = 2147483647;
        $firstDate = $this->iterateNodes($node, $maxTimeStampValue, function (&$child, $firstDate) {
            if (!empty($child['start_date']) && '1970-01-01' !== $child['start_date']) {
                $taskStartDate = strtotime($child['start_date']);
                if ($taskStartDate < $firstDate && $taskStartDate > 0) {
                    return $taskStartDate;
                }
            }
            return $firstDate;
        });
        if ($firstDate < 0 || '2038-01-19' === date('Y-m-d', $firstDate)) {
            $firstDate = strtotime(date('Y-m-d'));
            $node['duration'] = 24 * 60 * 60 * 1000;
        }
        if (empty($node['start_date'])) {
            $node['start_date'] = date('Y-m-d', $firstDate);
            $node['start'] = date('Y-m-d H:i:s', $firstDate);
        }
        // iterate one more time setting up empty dates
        $this->iterateNodes($node, $firstDate, function (&$child, $firstDate) {
            if (empty($child['start_date']) || '1970-01-01' === $child['start_date']) {
                $child['start_date'] = date('Y-m-d', $firstDate);
                $child['start'] = date('Y-m-d H:i:s', $firstDate);
            }
            return $firstDate;
        });
        return $firstDate;
    }

    /**
     * Iterate through children and search for end date.
     *
     * @param array $node
     *
     * @return int timestamp
     */
    private function findOutEndDates(&$node)
    {
        $lastDate = $this->iterateNodes($node, 0, function (&$child, $lastDate) {
            if (!empty($child['start_date']) && '1970-01-01' !== $child['start_date']) {
                $taskDate = strtotime($child['end_date']);
                if ($taskDate > $lastDate) {
                    return $taskDate;
                }
            }
            return $lastDate;
        });
        if (0 === $lastDate) {
            $lastDate = strtotime(date('Y-m-d'));
        }
        if (empty($node['end_date'])) {
            $node['end_date'] = date('Y-m-d', $lastDate);
            $node['end'] = $lastDate;
        }
        // iterate one more time setting up empty dates
        $this->iterateNodes($node, $lastDate, function (&$child, $lastDate) {
            if (empty($child['end_date'])) {
                $child['end_date'] = date('Y-m-d', $lastDate);
                $child['end'] = $lastDate;
            }
            return $lastDate;
        });
        return $lastDate;
    }

    /**
     * Calculate task duration in days.
     */
    private function calculateDurations()
    {
        foreach ($this->tasks as &$task) {
            if (empty($task['duration']) && isset($task['start_date'], $task['end_date'])) {
                $task['duration'] = $this->calculateDuration($task['start_date'], $task['end_date']);
            }
        }
    }

    /**
     * Collect all statuses.
     */
    public function getStatuses()
    {
        $closingStatuses = Settings_RealizationProcesses_Module_Model::getStatusNotModify();
        if (empty($closingStatuses['Project'])) {
            $closingStatuses['Project'] = ['status' => []];
        }
        if (empty($closingStatuses['ProjectMilestone'])) {
            $closingStatuses['ProjectMilestone'] = ['status' => []];
        }
        if (empty($closingStatuses['ProjectTask'])) {
            $closingStatuses['ProjectTask'] = ['status' => []];
        }
        $colors = ['Project' => [], 'ProjectMilestone' => [], 'ProjectTask' => []];
        $project = array_values(App\Fields\Picklist::getValues('projectstatus'));
        foreach ($project as $value) {
            $this->statuses['Project'][] = $status = ['value' => $value['projectstatus'], 'label' => App\Language::translate($value['projectstatus'], 'Project'), 'closing' => \in_array($value['projectstatus'], $closingStatuses['Project']['status'])];
            if (!$status['closing']) {
                $this->activeStatuses['Project'][] = $status;
            }
            $colors['Project']['projectstatus'][$value['projectstatus']] = \App\Colors::get($value['color'] ?? '', $value['projectstatus']);
        }
        $projectMilestone = array_values(App\Fields\Picklist::getValues('projectmilestone_status'));
        foreach ($projectMilestone as $value) {
            $this->statuses['ProjectMilestone'][] = $status = ['value' => $value['projectmilestone_status'], 'label' => App\Language::translate($value['projectmilestone_status'], 'ProjectMilestone'), 'closing' => \in_array($value['projectmilestone_status'], $closingStatuses['ProjectMilestone']['status'])];
            if (!$status['closing']) {
                $this->activeStatuses['ProjectMilestone'][] = $status;
            }
            $colors['ProjectMilestone']['projectmilestone_status'][$value['projectmilestone_status']] = \App\Colors::get($value['color'] ?? '', $value['projectmilestone_status']);
        }
        $projectTask = array_values(App\Fields\Picklist::getValues('projecttaskstatus'));
        foreach ($projectTask as $value) {
            $this->statuses['ProjectTask'][] = $status = ['value' => $value['projecttaskstatus'], 'label' => App\Language::translate($value['projecttaskstatus'], 'ProjectTask'), 'closing' => \in_array($value['projecttaskstatus'], $closingStatuses['ProjectTask']['status'])];
            if (!$status['closing']) {
                $this->activeStatuses['ProjectTask'][] = $status;
            }
            $colors['ProjectTask']['projecttaskstatus'][$value['projecttaskstatus']] = \App\Colors::get($value['color'] ?? '', $value['projecttaskstatus']);
        }
        $configColors = \App\Config::module('Project', 'defaultGanttColors');
        if (!empty($configColors)) {
            $this->statusColors = $configColors;
        } else {
            $this->statusColors = $colors;
        }
    }

    /**
     * Prepare tasks and gather some information.
     */
    private function prepareRecords()
    {
        $this->addRootNode();
        $this->normalizeParents();
        $this->collectChildrens();
        $this->calculateLevels();
        $this->findOutStartDates($this->rootNode);
        $this->findOutEndDates($this->rootNode);
        $this->calculateDurations();
    }

    /**
     * Get project data.
     *
     * @param array|int  $id       project id
     * @param mixed|null $viewName
     *
     * @return array
     */
    private function getProject($id, $viewName = null)
    {
        if (!\is_array($id) && isset($this->tasksById[$id])) {
            return [$this->tasksById[$id]];
        }
        if (!\is_array($id)) {
            $id = [$id];
        }
        $projects = [];
        $queryGenerator = new App\QueryGenerator('Project');
        $queryGenerator->setFields(['id', 'projectid', 'parentid', 'projectname', 'projectpriority', 'description', 'project_no', 'projectstatus', 'sum_time', 'startdate', 'actualenddate', 'targetenddate', 'progress', 'assigned_user_id', 'estimated_work_time']);
        if ($id !== [0]) {
            // empty id means that we want all projects
            $queryGenerator->addNativeCondition([
                'or',
                ['parentid' => $id],
                ['projectid' => array_diff($id, array_keys($this->tasksById))]
            ]);
        }
        if ($viewName) {
            $query = $queryGenerator->getCustomViewQueryById($viewName);
        } else {
            $query = $queryGenerator->createQuery();
        }
        $dataReader = $query->createCommand()->query();
        while ($row = $dataReader->read()) {
            $projectName = $queryGenerator->getModuleField('projectname')->getDisplayValue($row['projectname'], $row['id'], false, true);
            $project = [
                'id' => $row['id'],
                'name' => $projectName,
                'label' => $projectName,
                'url' => 'index.php?module=Project&view=Detail&record=' . $row['id'],
                'parentId' => !empty($row['parentid']) ? $row['parentid'] : null,
                'priority' => $queryGenerator->getModuleField('projectpriority')->getDisplayValue($row['projectpriority'], $row['id'], false, true),
                'priority_label' => \App\Language::translate($row['projectpriority'] ?? '', 'Project'),
                'sum_time' => $queryGenerator->getModuleField('sum_time')->getDisplayValue($row['sum_time'], $row['id'], false, true),
                'estimated_work_time' => $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true),
                'status' => 'STATUS_ACTIVE',
                'type' => 'project',
                'module' => 'Project',
                'open' => true,
                'canWrite' => false,
                'canDelete' => false,
                'cantWriteOnParent' => false,
                'canAdd' => false,
                'description' => $queryGenerator->getModuleField('description')->getDisplayValue($row['description'], $row['id'], false, true),
                'no' => $queryGenerator->getModuleField('project_no')->getDisplayValue($row['project_no'], $row['id'], false, true),
                'normalized_status' => $queryGenerator->getModuleField('projectstatus')->getDisplayValue($row['projectstatus'], $row['id'], false, true),
                'progress' => (int) $row['progress'],
                'status_label' => App\Language::translate($row['projectstatus'], 'Project'),
                'assigned_user_id' => $row['assigned_user_id'],
                'assigned_user_name' => \App\Fields\Owner::getUserLabel($row['assigned_user_id']),
                'color' => ($row['projectstatus'] && isset($this->statusColors['Project']['projectstatus'][$row['projectstatus']])) ? $this->statusColors['Project']['projectstatus'][$row['projectstatus']] : \App\Colors::getRandomColor('projectstatus_' . $row['id']),
            ];
            $project['number'] = '<a class="showReferenceTooltip js-popover-tooltip--record" title="' . $project['no'] . '" href="' . $project['url'] . '" target="_blank" rel="noreferrer noopener">' . $project['no'] . '</a>';
            if (empty($project['parentId'])) {
                unset($project['parentId']);
            } else {
                $project['dependentOn'] = [$project['parentId']];
            }
            if (!empty($row['startdate'])) {
                $project['start_date'] = date('Y-m-d', strtotime($row['startdate']));
                $project['start'] = date('Y-m-d H:i:s', strtotime($row['startdate']));
            }
            $project['end_date'] = $row['actualenddate'] ?: $row['targetenddate'] ?: '';
            $project['target_end_date'] = $row['targetenddate'] ? date('Y-m-d', strtotime($row['targetenddate'])) : '';
            if (empty($project['end_date']) && !empty($row['targetenddate'])) {
                $endDate = strtotime(date('Y-m-d', strtotime($row['targetenddate'])) . ' +1 days');
                $project['end_date'] = date('Y-m-d', $endDate);
                $project['end'] = strtotime($project['end_date']);
            }
            $project['planned_duration'] = $project['estimated_work_time'];
            $project['style'] = [
                'base' => [
                    'fill' => $project['color'],
                    'border' => $project['color']
                ]
            ];
            unset($project['color']);
            $this->tasksById[$row['id']] = $project;
            $projects[] = $project;
            if ($id !== [0] && !\in_array($row['id'], $id)) {
                $childrenIds[] = $row['id'];
            }
        }
        $dataReader->close();
        if (!empty($childrenIds)) {
            $projects = array_merge($projects, $this->getProject($childrenIds, $viewName));
        }
        unset($queryGenerator, $query, $dataReader, $project);
        return $projects;
    }

    /**
     * Get all projects from the system.
     *
     * @param mixed|null $viewName
     *
     * @return array projects,milestones,tasks
     */
    public function getAllData($viewName = null)
    {
        $this->getStatuses();
        $projects = $this->getProject(0, $viewName);
        $projectIds = array_column($projects, 'id');
        $milestones = $this->getGanttMilestones($projectIds);
        $ganttTasks = $this->getGanttTasks($projectIds);
        $this->tasks = array_merge($projects, $milestones, $ganttTasks);
        $this->prepareRecords();
        $response = [
            'statusColors' => $this->statusColors,
            'canWrite' => false,
            'canDelete' => false,
            'cantWriteOnParent' => false,
            'canAdd' => false,
            'statuses' => $this->statuses,
            'activeStatuses' => $this->activeStatuses,
        ];
        if (!empty($this->tree) && !empty($this->tree['children'])) {
            $response['tasks'] = $this->cleanup($this->flattenRecordTasks($this->tree['children']));
        }
        unset($projectIds, $milestones, $ganttTasks, $projects, $queryGenerator, $rootProjectIds, $projectIdsRows);
        return $response;
    }

    /**
     * Get project data to display in view as gantt.
     *
     * @param int|string $id
     *
     * @return array - projects,milestones,tasks
     */
    public function getById($id)
    {
        $this->getStatuses();
        $projects = $this->getProject($id);
        $title = '';
        if (!empty((int) $id)) {
            foreach ($projects as $project) {
                if ($project['id'] === $id) {
                    $title = $project['label'];
                }
            }
        }
        $projectIds = array_column($projects, 'id');
        $milestones = $this->getGanttMilestones($projectIds);
        $projectIds = array_merge($projectIds, array_column($milestones, 'id'));
        $ganttTasks = $this->getGanttTasks($projectIds);
        $this->tasks = array_merge($projects, $milestones, $ganttTasks);
        $this->prepareRecords();
        $response = [
            'statusColors' => $this->statusColors,
            'canWrite' => false,
            'canDelete' => false,
            'cantWriteOnParent' => false,
            'canAdd' => false,
            'statuses' => $this->statuses,
            'activeStatuses' => $this->activeStatuses,
            'title' => $title
        ];
        if (!empty($this->tree) && !empty($this->tree['children'])) {
            $response['tasks'] = $this->cleanup($this->flattenRecordTasks($this->tree['children']));
        }
        unset($projects, $projectIds, $milestones, $ganttTasks);
        return $response;
    }

    /**
     * Get project milestones.
     *
     * @param int|int[] $projectIds
     *
     * @return milestone[]
     */
    public function getGanttMilestones($projectIds)
    {
        $queryGenerator = new App\QueryGenerator('ProjectMilestone');
        $queryGenerator->setFields(['id', 'parentid', 'projectid', 'projectmilestonename', 'projectmilestonedate', 'projectmilestone_no', 'projectmilestone_progress', 'projectmilestone_priority', 'sum_time', 'estimated_work_time', 'projectmilestone_status', 'assigned_user_id']);
        $queryGenerator->addNativeCondition(['vtiger_projectmilestone.projectid' => $projectIds]);
        $dataReader = $queryGenerator->createQuery()->createCommand()->query();
        $milestones = [];
        while ($row = $dataReader->read()) {
            $row['parentid'] = (int) $row['parentid'];
            $row['projectid'] = (int) $row['projectid'];
            $milestoneName = $queryGenerator->getModuleField('projectmilestonename')->getDisplayValue($row['projectmilestonename'], $row['id'], false, true);
            $milestone = [
                'id' => $row['id'],
                'name' => $milestoneName,
                'label' => $milestoneName,
                'url' => 'index.php?module=ProjectMilestone&view=Detail&record=' . $row['id'],
                'parentId' => !empty($row['parentid']) ? $row['parentid'] : $row['projectid'],
                'module' => 'ProjectMilestone',
                'progress' => (int) $row['projectmilestone_progress'],
                'priority' => $queryGenerator->getModuleField('projectmilestone_priority')->getDisplayValue($row['projectmilestone_priority'], $row['id'], false, true),
                'priority_label' => $queryGenerator->getModuleField('projectmilestone_priority')->getDisplayValue($row['projectmilestone_priority'], $row['id'], false, true),
                'sum_time' => $queryGenerator->getModuleField('sum_time')->getDisplayValue($row['sum_time'], $row['id'], false, true),
                'estimated_work_time' => $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true),
                'open' => true,
                'type' => 'milestone',
                'normalized_status' => $queryGenerator->getModuleField('projectmilestone_status')->getDisplayValue($row['projectmilestone_status'], $row['id'], false, true),
                'status_label' => $queryGenerator->getModuleField('projectmilestone_status')->getDisplayValue($row['projectmilestone_status'], $row['id'], false, true),
                'canWrite' => false,
                'canDelete' => false,
                'status' => 'STATUS_ACTIVE',
                'cantWriteOnParent' => false,
                'canAdd' => false,
                'no' => $queryGenerator->getModuleField('projectmilestone_no')->getDisplayValue($row['projectmilestone_no'], $row['id'], false, true),
                'assigned_user_id' => $row['assigned_user_id'],
                'assigned_user_name' => \App\Fields\Owner::getUserLabel($row['assigned_user_id']),
                'startIsMilestone' => true,
                'color' => ($row['projectmilestone_status'] && isset($this->statusColors['ProjectMilestone']['projectmilestone_status'][$row['projectmilestone_status']])) ? $this->statusColors['ProjectMilestone']['projectmilestone_status'][$row['projectmilestone_status']] : App\Colors::getRandomColor('projectmilestone_status_' . $row['id']),
            ];
            $milestone['number'] = '<a class="showReferenceTooltip js-popover-tooltip--record" title="' . $milestone['no'] . '" href="' . $milestone['url'] . '" target="_blank">' . $milestone['no'] . '</a>';
            if (empty($milestone['parentId'])) {
                unset($milestone['parentId']);
            } else {
                $milestone['dependentOn'] = [$milestone['parentId']];
            }
            if ($pmDate = $row['projectmilestonedate']) {
                $milestone['duration'] = 24 * 60 * 60 * 1000;
                $milestone['start'] = date('Y-m-d H:i:s', strtotime($pmDate));
                $milestone['start_date'] = date('Y-m-d', strtotime($pmDate));
                $endDate = strtotime(date('Y-m-d', strtotime($pmDate)) . ' +1 days');
                $milestone['end'] = $endDate;
                $milestone['end_date'] = date('Y-m-d', $endDate);
                $milestone['v'] = $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true);
            }
            $milestone['planned_duration'] = $milestone['estimated_work_time'];
            $milestone['style'] = [
                'base' => [
                    'fill' => $milestone['color'],
                    'border' => $milestone['color']
                ]
            ];
            unset($milestone['color']);
            $milestones[] = $milestone;
        }
        $dataReader->close();
        unset($dataReader, $queryGenerator);
        return $milestones;
    }

    /**
     * Get project tasks.
     *
     * @param int|int[] $projectIds
     *
     * @return task[]
     */
    public function getGanttTasks($projectIds)
    {
        $taskTime = 0;
        $queryGenerator = new App\QueryGenerator('ProjectTask');
        $queryGenerator->setFields(['id', 'projectid', 'projecttaskname', 'parentid', 'projectmilestoneid', 'projecttaskprogress', 'projecttaskpriority', 'startdate', 'enddate', 'targetenddate', 'sum_time', 'projecttask_no', 'projecttaskstatus', 'estimated_work_time', 'assigned_user_id']);
        $queryGenerator->addNativeCondition([
            'or',
            ['vtiger_projecttask.projectid' => $projectIds],
            ['vtiger_projecttask.projectmilestoneid' => $projectIds]
        ]);
        $dataReader = $queryGenerator->createQuery()->createCommand()->query();
        $ganttTasks = [];
        while ($row = $dataReader->read()) {
            $taskName = $queryGenerator->getModuleField('projecttaskname')->getDisplayValue($row['projecttaskname'], $row['id'], false, true);
            $task = [
                'id' => $row['id'],
                'name' => $taskName,
                'label' => $taskName,
                'url' => 'index.php?module=ProjectTask&view=Detail&record=' . $row['id'],
                'parentId' => (int) ($row['parentid'] ?? 0),
                'canWrite' => false,
                'canDelete' => false,
                'cantWriteOnParent' => false,
                'canAdd' => false,
                'progress' => (int) $row['projecttaskprogress'],
                'priority' => $queryGenerator->getModuleField('projecttaskpriority')->getDisplayValue($row['projecttaskpriority'], $row['id'], false, true),
                'priority_label' => \App\Language::translate($row['projecttaskpriority'] ?? '', 'ProjectTask'),
                'sum_time' => $queryGenerator->getModuleField('sum_time')->getDisplayValue($row['sum_time'], $row['id'], false, true),
                'no' => $queryGenerator->getModuleField('projecttask_no')->getDisplayValue($row['projecttask_no'], $row['id'], false, true),
                'normalized_status' => $queryGenerator->getModuleField('projecttaskstatus')->getDisplayValue($row['projecttaskstatus'], $row['id'], false, true),
                'status_label' => App\Language::translate($row['projecttaskstatus'], 'ProjectTask'),
                'color' => ($row['projecttaskstatus'] && isset($this->statusColors['ProjectTask']['projecttaskstatus'][$row['projecttaskstatus']])) ? $this->statusColors['ProjectTask']['projecttaskstatus'][$row['projecttaskstatus']] : App\Colors::getRandomColor('projecttaskstatus_' . $row['id']),
                'start_date' => date('Y-m-d', strtotime($row['startdate'])),
                'start' => date('Y-m-d H:i:s', strtotime($row['startdate'])),
                'end_date' => $row['enddate'] ?: $row['targetenddate'],
                'target_end_date' => $row['targetenddate'],
                'assigned_user_id' => $row['assigned_user_id'],
                'assigned_user_name' => \App\Fields\Owner::getUserLabel($row['assigned_user_id']),
                'open' => true,
                'type' => 'task',
                'module' => 'ProjectTask',
                'status' => 'STATUS_ACTIVE',
            ];
            $task['number'] = '<a class="showReferenceTooltip js-popover-tooltip--record" title="' . $task['no'] . '" href="' . $task['url'] . '" target="_blank">' . $task['no'] . '</a>';
            if (empty($task['parentId'])) {
                $parentId = (int) ($row['projectmilestoneid'] ?? $row['projectid']);
                if ($parentId) {
                    $task['parentId'] = $parentId;
                    $task['dependentOn'] = [$parentId];
                }
            }
            $task['style'] = [
                'base' => [
                    'fill' => $task['color'],
                    'border' => $task['color']
                ]
            ];
            unset($task['color']);
            $endDate = date('Y-m-d', strtotime('+1 day', strtotime($task['end_date'])));
            $task['duration'] = $this->calculateDuration($task['start_date'], $endDate);
            $task['planned_duration'] = $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true);
            $taskTime += $row['estimated_work_time'];
            $ganttTasks[] = $task;
        }
        $dataReader->close();
        unset($dataReader, $queryGenerator, $taskTime, $endDate);
        return $ganttTasks;
    }
}