Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
ProjectGanttDataTransformer
100.00% covered (success)
100.00%
72 / 72
100.00% covered (success)
100.00%
2 / 2
15
100.00% covered (success)
100.00%
1 / 1
 transform
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
8
 resolveStatusColor
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Projects\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Projects\Domain\Model\Project;
12use App\Modules\Projects\Domain\Model\ProjectStage;
13use App\Modules\Projects\Domain\Model\ProjectTask;
14
15/**
16 * Transforms Project, Stages, and Tasks into standard hierarchical Gantt chart dataset.
17 *
18 * @package App\Modules\Projects\Application\Service
19 */
20final class ProjectGanttDataTransformer
21{
22    private const TYPE_STAGE = 'stage';
23    private const TYPE_MILESTONE = 'milestone';
24    private const TYPE_TASK = 'task';
25
26    /**
27     * Transforms project hierarchy into interactive Gantt chart payload.
28     *
29     * @param Project            $project Project domain aggregate.
30     * @param list<ProjectStage> $stages  List of project stages.
31     * @param list<ProjectTask>  $tasks   List of project tasks.
32     * @return array{
33     *     project: array<string, mixed>,
34     *     items: list<array<string, mixed>>,
35     *     links: list<array<string, mixed>>
36     * }
37     */
38    public function transform(Project $project, array $stages, array $tasks): array
39    {
40        $items = [];
41        $links = [];
42
43        foreach ($stages as $stage) {
44            $stageId = 'stage_' . $stage->id;
45            $isMilestone = $stage->isMilestone;
46            $items[] = [
47                'id'           => $stageId,
48                'raw_id'       => $stage->id,
49                'type'         => $isMilestone ? self::TYPE_MILESTONE : self::TYPE_STAGE,
50                'text'         => $stage->name,
51                'start_date'   => $stage->startDate ?? $project->startDate ?? date('Y-m-d'),
52                'end_date'     => $stage->endDate,
53                'progress'     => $stage->progress,
54                'parent'       => $stage->parentId !== null ? 'stage_' . $stage->parentId : '0',
55                'open'         => true,
56                'is_milestone' => $isMilestone,
57                'status'       => $stage->status,
58                'color'        => $this->resolveStatusColor($stage->status),
59            ];
60        }
61
62        foreach ($tasks as $task) {
63            $taskId = 'task_' . $task->id;
64            $parentStage = 'stage_' . $task->stageId;
65            $parent = $task->parentTaskId !== null ? 'task_' . $task->parentTaskId : $parentStage;
66
67            $items[] = [
68                'id'              => $taskId,
69                'raw_id'          => $task->id,
70                'type'            => self::TYPE_TASK,
71                'text'            => $task->name,
72                'start_date'      => $task->startDate,
73                'end_date'        => $task->endDate,
74                'actual_end_date' => $task->actualEndDate,
75                'progress'        => (float) $task->progress,
76                'parent'          => $parent,
77                'open'            => false,
78                'status'          => $task->status,
79                'priority'        => $task->priority,
80                'estimated_hours' => $task->estimatedHours,
81                'actual_hours'    => $task->actualHours,
82                'owner'           => $task->owner,
83                'color'           => $this->resolveStatusColor($task->status),
84            ];
85
86            if ($task->dependsOnTaskId !== null && $task->dependsOnTaskId > 0) {
87                $links[] = [
88                    'id'     => 'link_' . $task->dependsOnTaskId . '_' . $task->id,
89                    'source' => 'task_' . $task->dependsOnTaskId,
90                    'target' => $taskId,
91                    'type'   => '0', // Finish-to-Start (FS)
92                ];
93            }
94        }
95
96        return [
97            'project' => [
98                'id'              => $project->id,
99                'name'            => $project->name,
100                'status'          => $project->status,
101                'start_date'      => $project->startDate,
102                'target_end_date' => $project->targetEndDate,
103                'actual_end_date' => $project->actualEndDate,
104                'progress'        => $project->progress,
105                'estimated_hours' => $project->estimatedHours,
106                'actual_hours'    => $project->actualHours,
107                'color'           => $project->color,
108            ],
109            'items'   => $items,
110            'links'   => $links,
111        ];
112    }
113
114    /**
115     * Resolves status badge or bar hex color for Gantt items.
116     *
117     * @param string $status Entity status string.
118     * @return string Hex color code.
119     */
120    private function resolveStatusColor(string $status): string
121    {
122        return match ($status) {
123            'completed'     => '#2fb344',
124            'in_progress'   => '#206bc4',
125            'in_acceptance' => '#ae3ec9',
126            'on_hold'       => '#f59f00',
127            'cancelled'     => '#6c757d',
128            default         => '#4299e1',
129        };
130    }
131}