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); |
|
|
|
|
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
|
|
|
|