Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
27 / 27 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
| KanbanApiController | |
100.00% |
26 / 26 |
|
100.00% |
5 / 5 |
10 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| actionKanbanData | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| actionUpdateStatus | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
| extractStatusFromRequest | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| performStatusUpdate | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Core\Engine\Presentation\Api; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Core\Engine\Application\Service\KanbanBoardQueryService; |
| 12 | use App\Core\Engine\Application\Service\KanbanStatusTransitionService; |
| 13 | use App\Modules\Projects\Application\Service\ProjectProgressCalculator; |
| 14 | use App\Shared\Infrastructure\Http\ApiResponseTrait; |
| 15 | use Nyholm\Psr7\Factory\Psr17Factory; |
| 16 | use PDO; |
| 17 | use Psr\Http\Message\ResponseInterface; |
| 18 | use Psr\Http\Message\ServerRequestInterface; |
| 19 | |
| 20 | /** |
| 21 | * Universal Kanban Board REST API Controller. |
| 22 | * |
| 23 | * Endpoints: |
| 24 | * - GET /api/v1/{module}/kanban -> Returns columns with picklist stages and rich cards |
| 25 | * - PATCH /api/v1/{module}/{id}/status -> Updates record status and cascades rollup progress |
| 26 | * |
| 27 | * @package App\Core\Engine\Presentation\Api |
| 28 | */ |
| 29 | final readonly class KanbanApiController |
| 30 | { |
| 31 | use ApiResponseTrait; |
| 32 | |
| 33 | private const string ERR_MODULE_NOT_SUPPORTED = 'Module does not support Kanban view.'; |
| 34 | private const string ERR_RECORD_NOT_FOUND = 'Record not found.'; |
| 35 | private const string ERR_STATUS_REQUIRED = 'Target status key is required.'; |
| 36 | |
| 37 | private KanbanBoardQueryService $queryService; |
| 38 | private KanbanStatusTransitionService $transitionService; |
| 39 | |
| 40 | /** |
| 41 | * KanbanApiController constructor. |
| 42 | * |
| 43 | * @param PDO $pdo Active database PDO handle. |
| 44 | * @param ProjectProgressCalculator $calculator Project progress calculator service. |
| 45 | * @param Psr17Factory $psr17 PSR-17 response factory. |
| 46 | * @param KanbanBoardQueryService|null $queryService Optional query service. |
| 47 | * @param KanbanStatusTransitionService|null $transitionService Optional transition service. |
| 48 | */ |
| 49 | public function __construct( |
| 50 | private PDO $pdo, |
| 51 | private ProjectProgressCalculator $calculator, |
| 52 | private Psr17Factory $psr17, |
| 53 | ?KanbanBoardQueryService $queryService = null, |
| 54 | ?KanbanStatusTransitionService $transitionService = null |
| 55 | ) { |
| 56 | $this->queryService = $queryService ?? new KanbanBoardQueryService($this->pdo); |
| 57 | $this->transitionService = $transitionService |
| 58 | ?? new KanbanStatusTransitionService($this->pdo, $this->calculator); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Returns Kanban board columns and cards for the requested module. |
| 63 | * |
| 64 | * @param string $module Module machine name. |
| 65 | * @return ResponseInterface JSON response. |
| 66 | */ |
| 67 | public function actionKanbanData(string $module): ResponseInterface |
| 68 | { |
| 69 | $data = $this->queryService->fetchBoardData($module); |
| 70 | if ($data === null) { |
| 71 | return $this->jsonError($this->psr17, self::ERR_MODULE_NOT_SUPPORTED, 400); |
| 72 | } |
| 73 | |
| 74 | return $this->jsonSuccess($this->psr17, $data); |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Updates record status upon Kanban card Drag & Drop, cascading progress rollups. |
| 79 | * |
| 80 | * @param ServerRequestInterface $request Active PSR-7 HTTP request. |
| 81 | * @param string $module Module machine name. |
| 82 | * @param int $id Record primary key. |
| 83 | * @return ResponseInterface JSON response with updated state. |
| 84 | */ |
| 85 | public function actionUpdateStatus(ServerRequestInterface $request, string $module, int $id): ResponseInterface |
| 86 | { |
| 87 | $config = $this->queryService->resolveModuleConfig($module); |
| 88 | if ($config === null) { |
| 89 | return $this->jsonError($this->psr17, self::ERR_MODULE_NOT_SUPPORTED, 400); |
| 90 | } |
| 91 | |
| 92 | $newStatus = $this->extractStatusFromRequest($request); |
| 93 | if ($newStatus === '') { |
| 94 | return $this->jsonError($this->psr17, self::ERR_STATUS_REQUIRED, 422); |
| 95 | } |
| 96 | |
| 97 | return $this->performStatusUpdate($config, $module, $id, $newStatus); |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Extracts target status string from PSR-7 request payload. |
| 102 | * |
| 103 | * @param ServerRequestInterface $request HTTP request. |
| 104 | * @return string Status value or empty string. |
| 105 | */ |
| 106 | private function extractStatusFromRequest(ServerRequestInterface $request): string |
| 107 | { |
| 108 | $body = (string) $request->getBody(); |
| 109 | $payload = json_decode($body, true); |
| 110 | |
| 111 | return is_array($payload) ? (string) ($payload['status'] ?? '') : ''; |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Executes status update and cascading progress rollup. |
| 116 | * |
| 117 | * @param array<string, mixed> $config Module configuration. |
| 118 | * @param string $module Module name. |
| 119 | * @param int $id Record ID. |
| 120 | * @param string $status Target status. |
| 121 | * @return ResponseInterface JSON response. |
| 122 | */ |
| 123 | private function performStatusUpdate( |
| 124 | array $config, |
| 125 | string $module, |
| 126 | int $id, |
| 127 | string $status |
| 128 | ): ResponseInterface { |
| 129 | $result = $this->transitionService->updateStatus($config, $module, $id, $status); |
| 130 | if (!$result['success']) { |
| 131 | return $this->jsonError($this->psr17, self::ERR_RECORD_NOT_FOUND, 404); |
| 132 | } |
| 133 | |
| 134 | return $this->jsonSuccess($this->psr17, [ |
| 135 | 'module' => $module, |
| 136 | 'record_id' => $id, |
| 137 | 'new_status' => $status, |
| 138 | 'rollup' => $result['rollup'], |
| 139 | ]); |
| 140 | } |
| 141 | } |