Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
DataMappingHtmxController
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
4 / 4
18
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
 editor
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
9
 save
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
7
 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\DataMapping\Presentation\Htmx;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Service\DataMappingApplicationService;
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 data mapping configuration editor and partials.
21 */
22final readonly class DataMappingHtmxController
23{
24    private const string HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
25
26    public function __construct(
27        private DataMappingApplicationService $mappingService,
28        private MetadataRepositoryInterface $metadataRepository,
29        private TwigEnvironment $twig,
30        private Psr17Factory $psr17Factory,
31    ) {
32    }
33
34    /**
35     * Renders mapping editor for selected source and target modules (GET /htmx/data-mapping/editor).
36     */
37    public function editor(ServerRequestInterface $request): ResponseInterface
38    {
39        $params = $request->getQueryParams();
40        $sourceId = (int) ($params['source_id'] ?? 0);
41        $targetId = (int) ($params['target_id'] ?? 0);
42
43        if ($sourceId <= 0 || $targetId <= 0) {
44            return $this->htmlResponse(
45                '<div class="alert alert-info">Please select both source and target modules to configure.</div>'
46            );
47        }
48
49        $sourceModule = $this->metadataRepository->findModuleById($sourceId);
50        $targetModule = $this->metadataRepository->findModuleById($targetId);
51        $sourceFields = $this->metadataRepository->findFields($sourceId);
52        $targetFields = $this->metadataRepository->findFields($targetId);
53
54        $existing = $this->mappingService->getMapping($sourceId, $targetId);
55        $savedRules = [];
56        $copyInventory = false;
57
58        if ($existing && !empty($existing['mapping_rules']) && is_array($existing['mapping_rules'])) {
59            $copyInventory = !empty($existing['mapping_rules']['copy_inventory_items']);
60            $fieldList = $existing['mapping_rules']['fields'] ?? $existing['mapping_rules'];
61            if (is_array($fieldList)) {
62                foreach ($fieldList as $rule) {
63                    if (isset($rule['target_field'], $rule['source_field'])) {
64                        $savedRules[(string) $rule['target_field']] = (string) $rule['source_field'];
65                    }
66                }
67            }
68        }
69
70        $html = $this->twig->render('data_mapping/partials/editor.twig', [
71            'source_module' => $sourceModule,
72            'target_module' => $targetModule,
73            'source_fields' => $sourceFields,
74            'target_fields' => $targetFields,
75            'saved_rules' => $savedRules,
76            'copy_inventory' => $copyInventory,
77            'mapping_name' => $existing['mapping_name'] ?? "{$sourceModule->name} to {$targetModule->name}",
78        ]);
79
80        return $this->htmlResponse($html);
81    }
82
83    /**
84     * Saves data mapping rules via HTMX submission (POST /htmx/data-mapping/save).
85     */
86    public function save(ServerRequestInterface $request): ResponseInterface
87    {
88        $data = $request->getParsedBody();
89        $body = is_array($data) ? $data : [];
90
91        try {
92            $sourceId = (int) ($body['source_module_id'] ?? 0);
93            $targetId = (int) ($body['target_module_id'] ?? 0);
94            $name = trim((string) ($body['mapping_name'] ?? 'Data Mapping'));
95            $copyInventory = !empty($body['copy_inventory_items']);
96            $rawRules = is_array($body['rules'] ?? null) ? $body['rules'] : [];
97
98            $fieldsRules = [];
99            foreach ($rawRules as $targetField => $sourceField) {
100                if (!empty($sourceField) && is_string($sourceField)) {
101                    $fieldsRules[] = [
102                        'source_field' => (string) $sourceField,
103                        'target_field' => (string) $targetField,
104                        'type' => 'copy',
105                    ];
106                }
107            }
108
109            $this->mappingService->saveMapping([
110                'source_module_id' => $sourceId,
111                'target_module_id' => $targetId,
112                'mapping_name' => $name,
113                'mapping_rules' => [
114                    'copy_inventory_items' => $copyInventory,
115                    'fields' => $fieldsRules,
116                ],
117            ]);
118
119            $queryParams = ['source_id' => $sourceId, 'target_id' => $targetId];
120            $editorResponse = $this->editor($request->withQueryParams($queryParams));
121            $alert = '<div class="alert alert-success alert-dismissible mb-3">Mapping saved successfully!</div>';
122
123            return $this->htmlResponse($alert . (string) $editorResponse->getBody());
124        } catch (Throwable $e) {
125            $escaped = htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
126            return $this->htmlResponse("<div class='alert alert-danger mb-3'>{$escaped}</div>", 400);
127        }
128    }
129
130    private function htmlResponse(string $html, int $status = 200): ResponseInterface
131    {
132        $response = $this->psr17Factory->createResponse($status)
133            ->withHeader('Content-Type', self::HTML_CONTENT_TYPE);
134        $response->getBody()->write($html);
135
136        return $response;
137    }
138}