Project_Gantt_Model   F
last analyzed

Complexity

Total Complexity 124

Size/Duplication

Total Lines 737
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 124
eloc 404
c 1
b 0
f 0
dl 0
loc 737
ccs 0
cts 385
cp 0
rs 2

21 Methods

Rating   Name   Duplication   Size   Complexity  
B normalizeParents() 0 20 9
F getStatuses() 0 42 11
A getAllParentRecordsIds() 0 11 3
B findOutEndDates() 0 27 7
A collectChildrens() 0 3 1
A calculateDuration() 0 3 1
B getGanttMilestones() 0 66 7
A prepareRecords() 0 9 1
A flattenRecordTasks() 0 9 3
A getParentRecordsIdsRecursive() 0 17 6
A calculateDurations() 0 5 4
A addRootNode() 0 4 1
B calculateLevels() 0 24 7
B getById() 0 33 6
B findOutStartDates() 0 29 10
B cleanup() 0 25 8
A getAllData() 0 23 3
A iterateNodes() 0 12 4
A getRecordWithChildren() 0 11 5
B getGanttTasks() 0 67 7
F getProject() 0 91 20

How to fix   Complexity   

Complex Class

Complex classes like Project_Gantt_Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Project_Gantt_Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Gantt Model class.
5
 *
6
 * @package Model
7
 *
8
 * @copyright YetiForce S.A.
9
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
10
 * @author    Rafal Pospiech <[email protected]>
11
 * @author    Radosław Skrzypczak <[email protected]>
12
 */
13
class Project_Gantt_Model
14
{
15
	/**
16
	 * @var array project tasks,milesones and projects
17
	 */
18
	private $tasks = [];
19
20
	/**
21
	 * @var array rootNode needed for tree generation process
22
	 */
23
	private $rootNode;
24
25
	/**
26
	 * @var array task nodes as tree with children
27
	 */
28
	private $tree = [];
29
30
	/**
31
	 * @var array colors for statuses
32
	 */
33
	public $statusColors = [];
34
35
	/**
36
	 * @var array if some task is already loaded get it from here
37
	 */
38
	private $tasksById = [];
39
40
	/**
41
	 * @var array statuses - with closing value
42
	 */
43
	private $statuses;
44
45
	/**
46
	 * @var array - without closing value - for JS filter
47
	 */
48
	private $activeStatuses;
49
50
	/**
51
	 * Get parent nodes id as associative array [taskId]=>[parentId1,parentId2,...].
52
	 *
53
	 * @param int|string $parentId
54
	 * @param array      $parents  initial value
55
	 *
56
	 * @return array
57
	 */
58
	private function getParentRecordsIdsRecursive($parentId, $parents = [])
59
	{
60
		if (empty($parentId)) {
61
			return $parents;
62
		}
63
		if (!\in_array($parentId, $parents)) {
64
			$parents[] = $parentId;
65
		}
66
		foreach ($this->tasks as $task) {
67
			if ($task['id'] === $parentId) {
68
				if (!empty($task['parent'])) {
69
					$parents = $this->getParentRecordsIdsRecursive($task['parent'], $parents);
70
				}
71
				break;
72
			}
73
		}
74
		return $parents;
75
	}
76
77
	/**
78
	 * Collect all parents of all tasks.
79
	 *
80
	 * @return array
81
	 */
82
	private function getAllParentRecordsIds()
83
	{
84
		$parents = [];
85
		foreach ($this->tasks as $task) {
86
			if (!empty($task['parent'])) {
87
				$parents[$task['id']] = $this->getParentRecordsIdsRecursive($task['parent']);
88
			} else {
89
				$parents[$task['id']] = [];
90
			}
91
		}
92
		return $parents;
93
	}
94
95
	/**
96
	 * Calculate task levels and dependencies.
97
	 */
98
	private function calculateLevels()
99
	{
100
		$parents = $this->getAllParentRecordsIds();
101
		foreach ($this->tasks as &$task) {
102
			$task['level'] = \count($parents[$task['id']]);
103
			$task['parents'] = $parents[$task['id']];
104
		}
105
		unset($task);
106
		$hasChild = [];
107
		foreach ($parents as $parentsId) {
108
			foreach ($parentsId as $parentId) {
109
				if (!\in_array($parentId, $hasChild)) {
110
					$hasChild[] = $parentId;
111
				}
112
			}
113
		}
114
		foreach ($this->tasks as &$task) {
115
			if (\in_array($task['id'], $hasChild)) {
116
				$task['hasChild'] = true;
117
			} else {
118
				$task['hasChild'] = false;
119
			}
120
		}
121
		unset($parents);
122
	}
123
124
	/**
125
	 * Calculate duration in seconds.
126
	 *
127
	 * @param string $startDateStr
128
	 * @param string $endDateStr
129
	 *
130
	 * @return int
131
	 */
132
	private function calculateDuration($startDateStr, $endDateStr): int
133
	{
134
		return ((int) (new DateTime($startDateStr))->diff(new DateTime($endDateStr), true)->format('%a')) * 24 * 60 * 60 * 1000;
135
	}
136
137
	/**
138
	 * Normalize task parent property set as 0 if not exists (root node).
139
	 */
140
	private function normalizeParents()
141
	{
142
		// not set parents are children of root node
143
		foreach ($this->tasks as &$task) {
144
			if (!isset($task['parent']) && 0 !== $task['id']) {
145
				$task['parent'] = 0;
146
			}
147
		}
148
		// if parent id is set but we don't have it - it means that project is subproject so connect it to root node
149
		foreach ($this->tasks as &$task) {
150
			if (!empty($task['parent'])) {
151
				$idExists = false;
152
				foreach ($this->tasks as $parent) {
153
					if ($task['parent'] === $parent['id']) {
154
						$idExists = true;
155
						break;
156
					}
157
				}
158
				if (!$idExists) {
159
					$task['parent'] = 0;
160
				}
161
			}
162
		}
163
	}
164
165
	/**
166
	 * Collect task all parent nodes.
167
	 *
168
	 * @param array $task
169
	 *
170
	 * @return array task with parents property int[]
171
	 */
172
	private function &getRecordWithChildren(&$task)
173
	{
174
		foreach ($this->tasks as &$child) {
175
			if (isset($child['parent']) && $child['parent'] === $task['id']) {
176
				if (empty($task['children'])) {
177
					$task['children'] = [];
178
				}
179
				$task['children'][] = &$this->getRecordWithChildren($child);
180
			}
181
		}
182
		return $task;
183
	}
184
185
	/**
186
	 * Flatten task tree with proper order to use it in frontend gantt lib.
187
	 *
188
	 * @param       $nodes tasks tree
189
	 * @param array $flat  initial array
190
	 *
191
	 * @return task[]
192
	 */
193
	private function flattenRecordTasks($nodes, $flat = [])
194
	{
195
		foreach ($nodes as $node) {
196
			$flat[] = $node;
197
			if (!empty($node['children'])) {
198
				$flat = $this->flattenRecordTasks($node['children'], $flat);
199
			}
200
		}
201
		return $flat;
202
	}
203
204
	/**
205
	 * Sort all node types (task,milestones,projects) so each parent task is before its child (frontend lib needs this).
206
	 */
207
	private function collectChildrens()
208
	{
209
		$this->tree = &$this->getRecordWithChildren($this->rootNode);
210
	}
211
212
	/**
213
	 * Add root node to generate tree structure.
214
	 */
215
	private function addRootNode()
216
	{
217
		$this->rootNode = ['id' => 0];
218
		array_unshift($this->tasks, $this->rootNode);
219
	}
220
221
	/**
222
	 * Remove root node and children because they are not needed anymore.
223
	 *
224
	 * @param task[] $tasks
225
	 *
226
	 * @return task[] new array (not mutated)
227
	 */
228
	private function cleanup($tasks)
229
	{
230
		$clean = [];
231
		foreach ($tasks as $task) {
232
			if (0 !== $task['id']) {
233
				if (0 === $task['parent']) {
234
					unset($task['parent']);
235
					$task['depends'] = '';
236
				}
237
				if (isset($task['children'])) {
238
					unset($task['children']);
239
				}
240
				if (isset($task['parents'])) {
241
					unset($task['parents']);
242
				}
243
				if (isset($task['depends'])) {
244
					unset($task['depends']);
245
				}
246
				if (!isset($task['progress'])) {
247
					$task['progress'] = 100;
248
				}
249
				$clean[] = $task;
250
			}
251
		}
252
		return $clean;
253
	}
254
255
	/**
256
	 * Iterate through all tasks in tree.
257
	 *
258
	 * @param array    $node         starting point - might by rootNode
259
	 * @param mixed    $currentValue initial result which will be evaluated if there are some child nodes like array reduce
260
	 * @param callable $callback     what to do with task
261
	 *
262
	 * @return mixed
263
	 */
264
	public function iterateNodes(&$node, $currentValue, $callback)
265
	{
266
		if (empty($node['children'])) {
267
			return $currentValue;
268
		}
269
		foreach ($node['children'] as &$child) {
270
			$currentValue = $callback($child, $currentValue);
271
			if (!empty($child['children'])) {
272
				$currentValue = $this->iterateNodes($child, $currentValue, $callback);
273
			}
274
		}
275
		return $currentValue;
276
	}
277
278
	/**
279
	 * Iterate through children and search for start date.
280
	 *
281
	 * @param array $node
282
	 *
283
	 * @return int timestamp
284
	 */
285
	private function findOutStartDates(&$node)
286
	{
287
		$maxTimeStampValue = 2147483647;
288
		$firstDate = $this->iterateNodes($node, $maxTimeStampValue, function (&$child, $firstDate) {
289
			if (!empty($child['start_date']) && '1970-01-01' !== $child['start_date']) {
290
				$taskStartDate = strtotime($child['start_date']);
291
				if ($taskStartDate < $firstDate && $taskStartDate > 0) {
292
					return $taskStartDate;
293
				}
294
			}
295
			return $firstDate;
296
		});
297
		if ($firstDate < 0 || '2038-01-19' === date('Y-m-d', $firstDate)) {
298
			$firstDate = strtotime(date('Y-m-d'));
299
			$node['duration'] = 24 * 60 * 60 * 1000;
300
		}
301
		if (empty($node['start_date'])) {
302
			$node['start_date'] = date('Y-m-d', $firstDate);
303
			$node['start'] = date('Y-m-d H:i:s', $firstDate);
304
		}
305
		// iterate one more time setting up empty dates
306
		$this->iterateNodes($node, $firstDate, function (&$child, $firstDate) {
307
			if (empty($child['start_date']) || '1970-01-01' === $child['start_date']) {
308
				$child['start_date'] = date('Y-m-d', $firstDate);
309
				$child['start'] = date('Y-m-d H:i:s', $firstDate);
310
			}
311
			return $firstDate;
312
		});
313
		return $firstDate;
314
	}
315
316
	/**
317
	 * Iterate through children and search for end date.
318
	 *
319
	 * @param array $node
320
	 *
321
	 * @return int timestamp
322
	 */
323
	private function findOutEndDates(&$node)
324
	{
325
		$lastDate = $this->iterateNodes($node, 0, function (&$child, $lastDate) {
326
			if (!empty($child['start_date']) && '1970-01-01' !== $child['start_date']) {
327
				$taskDate = strtotime($child['end_date']);
328
				if ($taskDate > $lastDate) {
329
					return $taskDate;
330
				}
331
			}
332
			return $lastDate;
333
		});
334
		if (0 === $lastDate) {
335
			$lastDate = strtotime(date('Y-m-d'));
336
		}
337
		if (empty($node['end_date'])) {
338
			$node['end_date'] = date('Y-m-d', $lastDate);
339
			$node['end'] = $lastDate;
340
		}
341
		// iterate one more time setting up empty dates
342
		$this->iterateNodes($node, $lastDate, function (&$child, $lastDate) {
343
			if (empty($child['end_date'])) {
344
				$child['end_date'] = date('Y-m-d', $lastDate);
345
				$child['end'] = $lastDate;
346
			}
347
			return $lastDate;
348
		});
349
		return $lastDate;
350
	}
351
352
	/**
353
	 * Calculate task duration in days.
354
	 */
355
	private function calculateDurations()
356
	{
357
		foreach ($this->tasks as &$task) {
358
			if (empty($task['duration']) && isset($task['start_date'], $task['end_date'])) {
359
				$task['duration'] = $this->calculateDuration($task['start_date'], $task['end_date']);
360
			}
361
		}
362
	}
363
364
	/**
365
	 * Collect all statuses.
366
	 */
367
	public function getStatuses()
368
	{
369
		$closingStatuses = Settings_RealizationProcesses_Module_Model::getStatusNotModify();
370
		if (empty($closingStatuses['Project'])) {
371
			$closingStatuses['Project'] = ['status' => []];
372
		}
373
		if (empty($closingStatuses['ProjectMilestone'])) {
374
			$closingStatuses['ProjectMilestone'] = ['status' => []];
375
		}
376
		if (empty($closingStatuses['ProjectTask'])) {
377
			$closingStatuses['ProjectTask'] = ['status' => []];
378
		}
379
		$colors = ['Project' => [], 'ProjectMilestone' => [], 'ProjectTask' => []];
380
		$project = array_values(App\Fields\Picklist::getValues('projectstatus'));
381
		foreach ($project as $value) {
382
			$this->statuses['Project'][] = $status = ['value' => $value['projectstatus'], 'label' => App\Language::translate($value['projectstatus'], 'Project'), 'closing' => \in_array($value['projectstatus'], $closingStatuses['Project']['status'])];
383
			if (!$status['closing']) {
384
				$this->activeStatuses['Project'][] = $status;
385
			}
386
			$colors['Project']['projectstatus'][$value['projectstatus']] = \App\Colors::get($value['color'] ?? '', $value['projectstatus']);
387
		}
388
		$projectMilestone = array_values(App\Fields\Picklist::getValues('projectmilestone_status'));
389
		foreach ($projectMilestone as $value) {
390
			$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'])];
391
			if (!$status['closing']) {
392
				$this->activeStatuses['ProjectMilestone'][] = $status;
393
			}
394
			$colors['ProjectMilestone']['projectmilestone_status'][$value['projectmilestone_status']] = \App\Colors::get($value['color'] ?? '', $value['projectmilestone_status']);
395
		}
396
		$projectTask = array_values(App\Fields\Picklist::getValues('projecttaskstatus'));
397
		foreach ($projectTask as $value) {
398
			$this->statuses['ProjectTask'][] = $status = ['value' => $value['projecttaskstatus'], 'label' => App\Language::translate($value['projecttaskstatus'], 'ProjectTask'), 'closing' => \in_array($value['projecttaskstatus'], $closingStatuses['ProjectTask']['status'])];
399
			if (!$status['closing']) {
400
				$this->activeStatuses['ProjectTask'][] = $status;
401
			}
402
			$colors['ProjectTask']['projecttaskstatus'][$value['projecttaskstatus']] = \App\Colors::get($value['color'] ?? '', $value['projecttaskstatus']);
403
		}
404
		$configColors = \App\Config::module('Project', 'defaultGanttColors');
405
		if (!empty($configColors)) {
406
			$this->statusColors = $configColors;
407
		} else {
408
			$this->statusColors = $colors;
409
		}
410
	}
411
412
	/**
413
	 * Prepare tasks and gather some information.
414
	 */
415
	private function prepareRecords()
416
	{
417
		$this->addRootNode();
418
		$this->normalizeParents();
419
		$this->collectChildrens();
420
		$this->calculateLevels();
421
		$this->findOutStartDates($this->rootNode);
422
		$this->findOutEndDates($this->rootNode);
423
		$this->calculateDurations();
424
	}
425
426
	/**
427
	 * Get project data.
428
	 *
429
	 * @param array|int  $id       project id
430
	 * @param mixed|null $viewName
431
	 *
432
	 * @return array
433
	 */
434
	private function getProject($id, $viewName = null)
435
	{
436
		if (!\is_array($id) && isset($this->tasksById[$id])) {
437
			return [$this->tasksById[$id]];
438
		}
439
		if (!\is_array($id)) {
440
			$id = [$id];
441
		}
442
		$projects = [];
443
		$queryGenerator = new App\QueryGenerator('Project');
444
		$queryGenerator->setFields(['id', 'projectid', 'parentid', 'projectname', 'projectpriority', 'description', 'project_no', 'projectstatus', 'sum_time', 'startdate', 'actualenddate', 'targetenddate', 'progress', 'assigned_user_id', 'estimated_work_time']);
445
		if ($id !== [0]) {
446
			// empty id means that we want all projects
447
			$queryGenerator->addNativeCondition([
448
				'or',
449
				['parentid' => $id],
450
				['projectid' => array_diff($id, array_keys($this->tasksById))]
451
			]);
452
		}
453
		if ($viewName) {
454
			$query = $queryGenerator->getCustomViewQueryById($viewName);
455
		} else {
456
			$query = $queryGenerator->createQuery();
457
		}
458
		$dataReader = $query->createCommand()->query();
459
		while ($row = $dataReader->read()) {
460
			$projectName = $queryGenerator->getModuleField('projectname')->getDisplayValue($row['projectname'], $row['id'], false, true);
461
			$project = [
462
				'id' => $row['id'],
463
				'name' => $projectName,
464
				'label' => $projectName,
465
				'url' => 'index.php?module=Project&view=Detail&record=' . $row['id'],
466
				'parentId' => !empty($row['parentid']) ? $row['parentid'] : null,
467
				'priority' => $queryGenerator->getModuleField('projectpriority')->getDisplayValue($row['projectpriority'], $row['id'], false, true),
468
				'priority_label' => \App\Language::translate($row['projectpriority'] ?? '', 'Project'),
469
				'sum_time' => $queryGenerator->getModuleField('sum_time')->getDisplayValue($row['sum_time'], $row['id'], false, true),
470
				'estimated_work_time' => $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true),
471
				'status' => 'STATUS_ACTIVE',
472
				'type' => 'project',
473
				'module' => 'Project',
474
				'open' => true,
475
				'canWrite' => false,
476
				'canDelete' => false,
477
				'cantWriteOnParent' => false,
478
				'canAdd' => false,
479
				'description' => $queryGenerator->getModuleField('description')->getDisplayValue($row['description'], $row['id'], false, true),
480
				'no' => $queryGenerator->getModuleField('project_no')->getDisplayValue($row['project_no'], $row['id'], false, true),
481
				'normalized_status' => $queryGenerator->getModuleField('projectstatus')->getDisplayValue($row['projectstatus'], $row['id'], false, true),
482
				'progress' => (int) $row['progress'],
483
				'status_label' => App\Language::translate($row['projectstatus'], 'Project'),
484
				'assigned_user_id' => $row['assigned_user_id'],
485
				'assigned_user_name' => \App\Fields\Owner::getUserLabel($row['assigned_user_id']),
486
				'color' => ($row['projectstatus'] && isset($this->statusColors['Project']['projectstatus'][$row['projectstatus']])) ? $this->statusColors['Project']['projectstatus'][$row['projectstatus']] : \App\Colors::getRandomColor('projectstatus_' . $row['id']),
487
			];
488
			$project['number'] = '<a class="showReferenceTooltip js-popover-tooltip--record" title="' . $project['no'] . '" href="' . $project['url'] . '" target="_blank" rel="noreferrer noopener">' . $project['no'] . '</a>';
489
			if (empty($project['parentId'])) {
490
				unset($project['parentId']);
491
			} else {
492
				$project['dependentOn'] = [$project['parentId']];
493
			}
494
			if (!empty($row['startdate'])) {
495
				$project['start_date'] = date('Y-m-d', strtotime($row['startdate']));
496
				$project['start'] = date('Y-m-d H:i:s', strtotime($row['startdate']));
497
			}
498
			$project['end_date'] = $row['actualenddate'] ?: $row['targetenddate'] ?: '';
499
			$project['target_end_date'] = $row['targetenddate'] ? date('Y-m-d', strtotime($row['targetenddate'])) : '';
500
			if (empty($project['end_date']) && !empty($row['targetenddate'])) {
501
				$endDate = strtotime(date('Y-m-d', strtotime($row['targetenddate'])) . ' +1 days');
502
				$project['end_date'] = date('Y-m-d', $endDate);
503
				$project['end'] = strtotime($project['end_date']);
504
			}
505
			$project['planned_duration'] = $project['estimated_work_time'];
506
			$project['style'] = [
507
				'base' => [
508
					'fill' => $project['color'],
509
					'border' => $project['color']
510
				]
511
			];
512
			unset($project['color']);
513
			$this->tasksById[$row['id']] = $project;
514
			$projects[] = $project;
515
			if ($id !== [0] && !\in_array($row['id'], $id)) {
516
				$childrenIds[] = $row['id'];
517
			}
518
		}
519
		$dataReader->close();
520
		if (!empty($childrenIds)) {
521
			$projects = array_merge($projects, $this->getProject($childrenIds, $viewName));
522
		}
523
		unset($queryGenerator, $query, $dataReader, $project);
524
		return $projects;
525
	}
526
527
	/**
528
	 * Get all projects from the system.
529
	 *
530
	 * @param mixed|null $viewName
531
	 *
532
	 * @return array projects,milestones,tasks
533
	 */
534
	public function getAllData($viewName = null)
535
	{
536
		$this->getStatuses();
537
		$projects = $this->getProject(0, $viewName);
538
		$projectIds = array_column($projects, 'id');
539
		$milestones = $this->getGanttMilestones($projectIds);
540
		$ganttTasks = $this->getGanttTasks($projectIds);
541
		$this->tasks = array_merge($projects, $milestones, $ganttTasks);
542
		$this->prepareRecords();
543
		$response = [
544
			'statusColors' => $this->statusColors,
545
			'canWrite' => false,
546
			'canDelete' => false,
547
			'cantWriteOnParent' => false,
548
			'canAdd' => false,
549
			'statuses' => $this->statuses,
550
			'activeStatuses' => $this->activeStatuses,
551
		];
552
		if (!empty($this->tree) && !empty($this->tree['children'])) {
553
			$response['tasks'] = $this->cleanup($this->flattenRecordTasks($this->tree['children']));
554
		}
555
		unset($projectIds, $milestones, $ganttTasks, $projects, $queryGenerator, $rootProjectIds, $projectIdsRows);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rootProjectIds seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $queryGenerator seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $projectIdsRows does not exist. Did you maybe mean $projectIds?
Loading history...
556
		return $response;
557
	}
558
559
	/**
560
	 * Get project data to display in view as gantt.
561
	 *
562
	 * @param int|string $id
563
	 *
564
	 * @return array - projects,milestones,tasks
565
	 */
566
	public function getById($id)
567
	{
568
		$this->getStatuses();
569
		$projects = $this->getProject($id);
570
		$title = '';
571
		if (!empty((int) $id)) {
572
			foreach ($projects as $project) {
573
				if ($project['id'] === $id) {
574
					$title = $project['label'];
575
				}
576
			}
577
		}
578
		$projectIds = array_column($projects, 'id');
579
		$milestones = $this->getGanttMilestones($projectIds);
580
		$projectIds = array_merge($projectIds, array_column($milestones, 'id'));
581
		$ganttTasks = $this->getGanttTasks($projectIds);
582
		$this->tasks = array_merge($projects, $milestones, $ganttTasks);
583
		$this->prepareRecords();
584
		$response = [
585
			'statusColors' => $this->statusColors,
586
			'canWrite' => false,
587
			'canDelete' => false,
588
			'cantWriteOnParent' => false,
589
			'canAdd' => false,
590
			'statuses' => $this->statuses,
591
			'activeStatuses' => $this->activeStatuses,
592
			'title' => $title
593
		];
594
		if (!empty($this->tree) && !empty($this->tree['children'])) {
595
			$response['tasks'] = $this->cleanup($this->flattenRecordTasks($this->tree['children']));
596
		}
597
		unset($projects, $projectIds, $milestones, $ganttTasks);
598
		return $response;
599
	}
600
601
	/**
602
	 * Get project milestones.
603
	 *
604
	 * @param int|int[] $projectIds
605
	 *
606
	 * @return milestone[]
607
	 */
608
	public function getGanttMilestones($projectIds)
609
	{
610
		$queryGenerator = new App\QueryGenerator('ProjectMilestone');
611
		$queryGenerator->setFields(['id', 'parentid', 'projectid', 'projectmilestonename', 'projectmilestonedate', 'projectmilestone_no', 'projectmilestone_progress', 'projectmilestone_priority', 'sum_time', 'estimated_work_time', 'projectmilestone_status', 'assigned_user_id']);
612
		$queryGenerator->addNativeCondition(['vtiger_projectmilestone.projectid' => $projectIds]);
613
		$dataReader = $queryGenerator->createQuery()->createCommand()->query();
614
		$milestones = [];
615
		while ($row = $dataReader->read()) {
616
			$row['parentid'] = (int) $row['parentid'];
617
			$row['projectid'] = (int) $row['projectid'];
618
			$milestoneName = $queryGenerator->getModuleField('projectmilestonename')->getDisplayValue($row['projectmilestonename'], $row['id'], false, true);
619
			$milestone = [
620
				'id' => $row['id'],
621
				'name' => $milestoneName,
622
				'label' => $milestoneName,
623
				'url' => 'index.php?module=ProjectMilestone&view=Detail&record=' . $row['id'],
624
				'parentId' => !empty($row['parentid']) ? $row['parentid'] : $row['projectid'],
625
				'module' => 'ProjectMilestone',
626
				'progress' => (int) $row['projectmilestone_progress'],
627
				'priority' => $queryGenerator->getModuleField('projectmilestone_priority')->getDisplayValue($row['projectmilestone_priority'], $row['id'], false, true),
628
				'priority_label' => $queryGenerator->getModuleField('projectmilestone_priority')->getDisplayValue($row['projectmilestone_priority'], $row['id'], false, true),
629
				'sum_time' => $queryGenerator->getModuleField('sum_time')->getDisplayValue($row['sum_time'], $row['id'], false, true),
630
				'estimated_work_time' => $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true),
631
				'open' => true,
632
				'type' => 'milestone',
633
				'normalized_status' => $queryGenerator->getModuleField('projectmilestone_status')->getDisplayValue($row['projectmilestone_status'], $row['id'], false, true),
634
				'status_label' => $queryGenerator->getModuleField('projectmilestone_status')->getDisplayValue($row['projectmilestone_status'], $row['id'], false, true),
635
				'canWrite' => false,
636
				'canDelete' => false,
637
				'status' => 'STATUS_ACTIVE',
638
				'cantWriteOnParent' => false,
639
				'canAdd' => false,
640
				'no' => $queryGenerator->getModuleField('projectmilestone_no')->getDisplayValue($row['projectmilestone_no'], $row['id'], false, true),
641
				'assigned_user_id' => $row['assigned_user_id'],
642
				'assigned_user_name' => \App\Fields\Owner::getUserLabel($row['assigned_user_id']),
643
				'startIsMilestone' => true,
644
				'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']),
645
			];
646
			$milestone['number'] = '<a class="showReferenceTooltip js-popover-tooltip--record" title="' . $milestone['no'] . '" href="' . $milestone['url'] . '" target="_blank">' . $milestone['no'] . '</a>';
647
			if (empty($milestone['parentId'])) {
648
				unset($milestone['parentId']);
649
			} else {
650
				$milestone['dependentOn'] = [$milestone['parentId']];
651
			}
652
			if ($pmDate = $row['projectmilestonedate']) {
653
				$milestone['duration'] = 24 * 60 * 60 * 1000;
654
				$milestone['start'] = date('Y-m-d H:i:s', strtotime($pmDate));
655
				$milestone['start_date'] = date('Y-m-d', strtotime($pmDate));
656
				$endDate = strtotime(date('Y-m-d', strtotime($pmDate)) . ' +1 days');
657
				$milestone['end'] = $endDate;
658
				$milestone['end_date'] = date('Y-m-d', $endDate);
659
				$milestone['v'] = $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true);
660
			}
661
			$milestone['planned_duration'] = $milestone['estimated_work_time'];
662
			$milestone['style'] = [
663
				'base' => [
664
					'fill' => $milestone['color'],
665
					'border' => $milestone['color']
666
				]
667
			];
668
			unset($milestone['color']);
669
			$milestones[] = $milestone;
670
		}
671
		$dataReader->close();
672
		unset($dataReader, $queryGenerator);
673
		return $milestones;
674
	}
675
676
	/**
677
	 * Get project tasks.
678
	 *
679
	 * @param int|int[] $projectIds
680
	 *
681
	 * @return task[]
682
	 */
683
	public function getGanttTasks($projectIds)
684
	{
685
		$taskTime = 0;
686
		$queryGenerator = new App\QueryGenerator('ProjectTask');
687
		$queryGenerator->setFields(['id', 'projectid', 'projecttaskname', 'parentid', 'projectmilestoneid', 'projecttaskprogress', 'projecttaskpriority', 'startdate', 'enddate', 'targetenddate', 'sum_time', 'projecttask_no', 'projecttaskstatus', 'estimated_work_time', 'assigned_user_id']);
688
		$queryGenerator->addNativeCondition([
689
			'or',
690
			['vtiger_projecttask.projectid' => $projectIds],
691
			['vtiger_projecttask.projectmilestoneid' => $projectIds]
692
		]);
693
		$dataReader = $queryGenerator->createQuery()->createCommand()->query();
694
		$ganttTasks = [];
695
		while ($row = $dataReader->read()) {
696
			$taskName = $queryGenerator->getModuleField('projecttaskname')->getDisplayValue($row['projecttaskname'], $row['id'], false, true);
697
			$task = [
698
				'id' => $row['id'],
699
				'name' => $taskName,
700
				'label' => $taskName,
701
				'url' => 'index.php?module=ProjectTask&view=Detail&record=' . $row['id'],
702
				'parentId' => (int) ($row['parentid'] ?? 0),
703
				'canWrite' => false,
704
				'canDelete' => false,
705
				'cantWriteOnParent' => false,
706
				'canAdd' => false,
707
				'progress' => (int) $row['projecttaskprogress'],
708
				'priority' => $queryGenerator->getModuleField('projecttaskpriority')->getDisplayValue($row['projecttaskpriority'], $row['id'], false, true),
709
				'priority_label' => \App\Language::translate($row['projecttaskpriority'] ?? '', 'ProjectTask'),
710
				'sum_time' => $queryGenerator->getModuleField('sum_time')->getDisplayValue($row['sum_time'], $row['id'], false, true),
711
				'no' => $queryGenerator->getModuleField('projecttask_no')->getDisplayValue($row['projecttask_no'], $row['id'], false, true),
712
				'normalized_status' => $queryGenerator->getModuleField('projecttaskstatus')->getDisplayValue($row['projecttaskstatus'], $row['id'], false, true),
713
				'status_label' => App\Language::translate($row['projecttaskstatus'], 'ProjectTask'),
714
				'color' => ($row['projecttaskstatus'] && isset($this->statusColors['ProjectTask']['projecttaskstatus'][$row['projecttaskstatus']])) ? $this->statusColors['ProjectTask']['projecttaskstatus'][$row['projecttaskstatus']] : App\Colors::getRandomColor('projecttaskstatus_' . $row['id']),
715
				'start_date' => date('Y-m-d', strtotime($row['startdate'])),
716
				'start' => date('Y-m-d H:i:s', strtotime($row['startdate'])),
717
				'end_date' => $row['enddate'] ?: $row['targetenddate'],
718
				'target_end_date' => $row['targetenddate'],
719
				'assigned_user_id' => $row['assigned_user_id'],
720
				'assigned_user_name' => \App\Fields\Owner::getUserLabel($row['assigned_user_id']),
721
				'open' => true,
722
				'type' => 'task',
723
				'module' => 'ProjectTask',
724
				'status' => 'STATUS_ACTIVE',
725
			];
726
			$task['number'] = '<a class="showReferenceTooltip js-popover-tooltip--record" title="' . $task['no'] . '" href="' . $task['url'] . '" target="_blank">' . $task['no'] . '</a>';
727
			if (empty($task['parentId'])) {
728
				$parentId = (int) ($row['projectmilestoneid'] ?? $row['projectid']);
729
				if ($parentId) {
730
					$task['parentId'] = $parentId;
731
					$task['dependentOn'] = [$parentId];
732
				}
733
			}
734
			$task['style'] = [
735
				'base' => [
736
					'fill' => $task['color'],
737
					'border' => $task['color']
738
				]
739
			];
740
			unset($task['color']);
741
			$endDate = date('Y-m-d', strtotime('+1 day', strtotime($task['end_date'])));
742
			$task['duration'] = $this->calculateDuration($task['start_date'], $endDate);
743
			$task['planned_duration'] = $queryGenerator->getModuleField('estimated_work_time')->getDisplayValue($row['estimated_work_time'], $row['id'], false, true);
744
			$taskTime += $row['estimated_work_time'];
745
			$ganttTasks[] = $task;
746
		}
747
		$dataReader->close();
748
		unset($dataReader, $queryGenerator, $taskTime, $endDate);
749
		return $ganttTasks;
750
	}
751
}
752