Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
InventorySettingsHtmxController
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
7 / 7
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 table
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 modal
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 toggle
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 delete
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 htmlResponse
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
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\Inventory\Presentation\Htmx;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Service\InventoryApplicationService;
12use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface;
13use Nyholm\Psr7\Factory\Psr17Factory;
14use Psr\Http\Message\ResponseInterface;
15use Psr\Http\Message\ServerRequestInterface;
16use Throwable;
17use Twig\Environment as TwigEnvironment;
18
19/**
20 * HTMX controller for interactive inventory fields management partials and modals.
21 */
22final readonly class InventorySettingsHtmxController
23{
24    private const string HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
25
26    public function __construct(
27        private InventoryApplicationService $inventoryService,
28        private MetadataRepositoryInterface $metadataRepository,
29        private TwigEnvironment $twig,
30        private Psr17Factory $psr17Factory,
31    ) {
32    }
33
34    /**
35     * Renders inventory fields table partial (GET /htmx/inventory-fields/{moduleId:\d+}/table).
36     */
37    public function table(ServerRequestInterface $request, int $moduleId): ResponseInterface
38    {
39        $request->getMethod();
40        $module = $this->metadataRepository->findModuleById($moduleId);
41        $fields = $this->inventoryService->getInventoryFields($moduleId);
42
43        $html = $this->twig->render('inventory/partials/fields_table.twig', [
44            'module' => $module,
45            'fields' => $fields,
46        ]);
47
48        return $this->htmlResponse($html);
49    }
50
51    /**
52     * Renders add/edit inventory field modal (GET /htmx/inventory-fields/{moduleId:\d+}/modal).
53     */
54    public function modal(ServerRequestInterface $request, int $moduleId): ResponseInterface
55    {
56        $request->getMethod();
57        $module = $this->metadataRepository->findModuleById($moduleId);
58
59        $html = $this->twig->render('inventory/partials/field_modal.twig', [
60            'module' => $module,
61        ]);
62
63        return $this->htmlResponse($html);
64    }
65
66    /**
67     * Saves new inventory field via HTMX form submission (POST /htmx/inventory-fields/{moduleId:\d+}/save).
68     */
69    public function save(ServerRequestInterface $request, int $moduleId): ResponseInterface
70    {
71        $data = $request->getParsedBody();
72        $body = is_array($data) ? $data : [];
73
74        try {
75            $fieldKey = trim((string) ($body['field_key'] ?? ''));
76            $label = trim((string) ($body['label'] ?? ''));
77            $uitypeId = (int) ($body['uitype_id'] ?? 1);
78            $isMandatory = !empty($body['is_mandatory']) ? 1 : 0;
79            $sortOrder = (int) ($body['sort_order'] ?? 50);
80
81            $this->inventoryService->addInventoryField($moduleId, [
82                'field_key' => $fieldKey,
83                'label' => $label,
84                'uitype_id' => $uitypeId,
85                'is_mandatory' => $isMandatory,
86                'sort_order' => $sortOrder,
87                'is_system' => 0,
88                'status' => 'active',
89            ]);
90
91            return $this->table($request, $moduleId);
92        } catch (Throwable $e) {
93            $escaped = htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
94            return $this->htmlResponse("<div class='alert alert-danger mb-3'>{$escaped}</div>", 400);
95        }
96    }
97
98    /**
99     * Toggles field active/inactive status (POST /htmx/inventory-fields/{moduleId:\d+}/toggle/{fieldId:\d+}).
100     */
101    public function toggle(ServerRequestInterface $request, int $moduleId, int $fieldId): ResponseInterface
102    {
103        $fields = $this->inventoryService->getInventoryFields($moduleId);
104        foreach ($fields as $field) {
105            if ((int) $field['id'] === $fieldId) {
106                $newStatus = ($field['status'] === 'active') ? 'inactive' : 'active';
107                $this->inventoryService->updateInventoryField($fieldId, ['status' => $newStatus]);
108                break;
109            }
110        }
111
112        return $this->table($request, $moduleId);
113    }
114
115    /**
116     * Deletes user-defined inventory field (DELETE /htmx/inventory-fields/{moduleId:\d+}/delete/{fieldId:\d+}).
117     */
118    public function delete(ServerRequestInterface $request, int $moduleId, int $fieldId): ResponseInterface
119    {
120        $this->inventoryService->deleteInventoryField($fieldId);
121        return $this->table($request, $moduleId);
122    }
123
124    private function htmlResponse(string $html, int $status = 200): ResponseInterface
125    {
126        $response = $this->psr17Factory->createResponse($status)
127            ->withHeader('Content-Type', self::HTML_CONTENT_TYPE);
128        $response->getBody()->write($html);
129
130        return $response;
131    }
132}