Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.53% covered (success)
91.53%
108 / 118
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
StructureApiController
91.45% covered (success)
91.45%
107 / 117
71.43% covered (warning)
71.43%
5 / 7
25.39
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tree
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 checkCanDelete
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 reassignAndDelete
66.67% covered (warning)
66.67%
16 / 24
0.00% covered (danger)
0.00%
0 / 1
4.59
 users
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 syncUsers
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 mapPoints
96.77% covered (success)
96.77%
60 / 62
0.00% covered (danger)
0.00%
0 / 1
15
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\Structure\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Structure\Application\Service\StructureReassignmentServiceInterface;
12use App\Modules\Structure\Application\Service\StructureTreeBuilderInterface;
13use App\Modules\Structure\Domain\Repository\StructureMembershipRepositoryInterface;
14use App\Modules\Structure\Domain\Repository\StructureRepositoryInterface;
15use App\Shared\Infrastructure\Http\ApiResponseTrait;
16use Psr\Http\Message\ResponseFactoryInterface;
17use Psr\Http\Message\ResponseInterface;
18use Psr\Http\Message\ServerRequestInterface;
19use Throwable;
20
21/**
22 * Structure Management REST API Controller.
23 *
24 * Exposes hierarchy tree, membership management, and Reassign Flow validation endpoints.
25 *
26 * @package App\Modules\Structure\Presentation\Api
27 */
28final readonly class StructureApiController
29{
30    use ApiResponseTrait;
31
32    /**
33     * StructureApiController constructor.
34     *
35     * @param ResponseFactoryInterface                    $responseFactory     PSR-7 response factory.
36     * @param StructureMembershipRepositoryInterface      $membershipRepo      Membership repository.
37     * @param StructureTreeBuilderInterface               $treeBuilder         Tree builder service.
38     * @param StructureReassignmentServiceInterface       $reassignmentService Reassignment service.
39     */
40    public function __construct(
41        private ResponseFactoryInterface $responseFactory,
42        private StructureMembershipRepositoryInterface $membershipRepo,
43        private StructureTreeBuilderInterface $treeBuilder,
44        private StructureReassignmentServiceInterface $reassignmentService,
45        private ?StructureRepositoryInterface $structureRepo = null
46    ) {
47    }
48
49    /**
50     * Returns full structure hierarchy tree.
51     *
52     * @return ResponseInterface JSON response.
53     */
54    public function tree(): ResponseInterface
55    {
56        $tree = $this->treeBuilder->buildTree();
57
58        return $this->jsonStatusResponse(
59            $this->responseFactory,
60            true,
61            'Structure tree fetched successfully',
62            $tree
63        );
64    }
65
66    /**
67     * Checks whether a structure node can be safely deleted or requires reassignment (Rule 2).
68     *
69     * @param int $id Structure node ID.
70     * @return ResponseInterface JSON response.
71     */
72    public function checkCanDelete(int $id): ResponseInterface
73    {
74        $check = $this->reassignmentService->canDeleteStructure($id);
75
76        return $this->jsonStatusResponse(
77            $this->responseFactory,
78            true,
79            'Structure deletion check evaluated',
80            $check
81        );
82    }
83
84    /**
85     * Reassigns all dependencies of a structure node and removes it (Rule 2).
86     *
87     * @param ServerRequestInterface $request Incoming HTTP request.
88     * @param int                    $id      Source structure ID.
89     * @return ResponseInterface JSON response.
90     */
91    public function reassignAndDelete(ServerRequestInterface $request, int $id): ResponseInterface
92    {
93        $body = (array)$request->getParsedBody();
94        $targetId = (int)($body['target_id'] ?? 0);
95
96        if ($targetId <= 0 || $targetId === $id) {
97            return $this->jsonStatusResponse(
98                $this->responseFactory,
99                false,
100                'Valid target structure ID is required for reassignment.',
101                null,
102                400
103            );
104        }
105
106        try {
107            $this->reassignmentService->reassignAndRemoveStructure($id, $targetId);
108            return $this->jsonStatusResponse(
109                $this->responseFactory,
110                true,
111                'Structure reallocated and deleted successfully.'
112            );
113        } catch (Throwable $e) {
114            return $this->jsonStatusResponse(
115                $this->responseFactory,
116                false,
117                $e->getMessage(),
118                null,
119                422
120            );
121        }
122    }
123
124    /**
125     * Returns assigned users for a structure node.
126     *
127     * @param int $id Structure node ID.
128     * @return ResponseInterface JSON response.
129     */
130    public function users(int $id): ResponseInterface
131    {
132        $users = $this->membershipRepo->getStructureUsers($id);
133
134        return $this->jsonStatusResponse(
135            $this->responseFactory,
136            true,
137            'Assigned users fetched successfully',
138            $users
139        );
140    }
141
142    /**
143     * Synchronizes assigned users for a structure node.
144     *
145     * @param ServerRequestInterface $request
146     * @param int $id Structure node ID.
147     * @return ResponseInterface
148     */
149    public function syncUsers(ServerRequestInterface $request, int $id): ResponseInterface
150    {
151        $body = (array)$request->getParsedBody();
152        $rawUserIds = $body['user_ids'] ?? [];
153        $userIds = is_array($rawUserIds) ? array_map('intval', $rawUserIds) : [];
154
155        $this->membershipRepo->syncStructureUsers($id, $userIds);
156
157        return $this->jsonStatusResponse(
158            $this->responseFactory,
159            true,
160            'Assigned users synchronized successfully'
161        );
162    }
163
164    /**
165     * Returns all active structure nodes with coordinates for map rendering.
166     *
167     * @param int $currentId Active structure node ID to highlight.
168     * @return ResponseInterface JSON response.
169     */
170    public function mapPoints(int $currentId = 0): ResponseInterface
171    {
172        if ($this->structureRepo === null) {
173            return $this->jsonStatusResponse(
174                $this->responseFactory,
175                true,
176                'Structure map points fetched',
177                []
178            );
179        }
180
181        $nodes = $this->structureRepo->findAllActive();
182        $currentNode = null;
183        if ($currentId > 0) {
184            foreach ($nodes as $candidate) {
185                if ((int) $candidate->getId() === $currentId) {
186                    $currentNode = $candidate;
187                    break;
188                }
189            }
190        }
191        $currentParentId = $currentNode?->getParentId();
192        $points = [];
193
194        foreach ($nodes as $node) {
195            $lat = $node->getAddressLatitude();
196            $lng = $node->getAddressLongitude();
197            if ($lat === null || $lng === null) {
198                continue;
199            }
200
201            $nodeId = (int) $node->getId();
202            $isCurrent = ($nodeId === $currentId);
203            $isParent = ($currentId > 0 && $currentParentId !== null && $nodeId === $currentParentId);
204            $isChild = ($currentId > 0 && $node->getParentId() === $currentId);
205            $isRelated = ($isParent || $isChild);
206
207            $relation = 'other';
208            $relationLabel = 'Inna jednostka';
209            if ($isCurrent) {
210                $relation = 'current';
211                $relationLabel = 'Bieżąca jednostka';
212            } elseif ($isParent) {
213                $relation = 'parent';
214                $relationLabel = 'Jednostka nadrzędna';
215            } elseif ($isChild) {
216                $relation = 'child';
217                $relationLabel = 'Jednostka podrzędna';
218            }
219
220            $points[] = [
221                'id'                       => $nodeId,
222                'name'                     => $node->getName(),
223                'code'                     => $node->getCode(),
224                'structure_type'           => $node->getStructureType()->value,
225                'structure_type_label'     => $node->getStructureType()->label(),
226                'address_street'           => $node->getAddressStreet(),
227                'address_building_number'  => $node->getAddressBuildingNumber(),
228                'address_apartment_number' => $node->getAddressApartmentNumber(),
229                'address_postal_code'      => $node->getAddressPostalCode(),
230                'address_city'             => $node->getAddressCity(),
231                'address_country'          => $node->getAddressCountry(),
232                'address_latitude'         => $lat,
233                'address_longitude'        => $lng,
234                'is_current'               => $isCurrent,
235                'is_related'               => $isRelated,
236                'relation'                 => $relation,
237                'relation_label'           => $relationLabel,
238                'assigned_users_count'     => $node->getAssignedUsersCount(),
239                'assigned_user_names'      => $node->getAssignedUserNames(),
240            ];
241        }
242
243        return $this->jsonStatusResponse(
244            $this->responseFactory,
245            true,
246            'Structure map points fetched',
247            $points
248        );
249    }
250}