Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
64 / 66
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
DashboardHtmxController
96.92% covered (success)
96.92%
63 / 65
85.71% covered (warning)
85.71%
6 / 7
14
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
 widgetCurrentEvents
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 widgetOverdueEvents
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 widgetPickerButton
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 toggleWidget
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
6.01
 eventModal
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 htmlResponse
100.00% covered (success)
100.00%
3 / 3
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\Dashboard\Presentation\Htmx;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\PermissionContext;
12use App\Modules\Dashboard\Application\Service\DashboardCalendarActionServiceInterface;
13use App\Modules\Dashboard\Application\Service\DashboardWidgetsServiceInterface;
14use Psr\Http\Message\ResponseFactoryInterface;
15use Psr\Http\Message\ResponseInterface;
16use Psr\Http\Message\ServerRequestInterface;
17use Twig\Environment as TwigEnvironment;
18
19/**
20 * Dashboard HTMX Controller.
21 *
22 * Serves dynamic HTML partials for dashboard widgets, widget picker button,
23 * and calendar action modals.
24 *
25 * @package App\Modules\Dashboard\Presentation\Htmx
26 */
27final readonly class DashboardHtmxController
28{
29    /**
30     * DashboardHtmxController constructor.
31     *
32     * @param DashboardWidgetsServiceInterface        $widgetsService  Widgets service.
33     * @param DashboardCalendarActionServiceInterface $actionService   Calendar action service.
34     * @param TwigEnvironment                         $twig            Twig renderer.
35     * @param ResponseFactoryInterface                $responseFactory PSR-7 Response factory.
36     */
37    public function __construct(
38        private DashboardWidgetsServiceInterface        $widgetsService,
39        private DashboardCalendarActionServiceInterface $actionService,
40        private TwigEnvironment                         $twig,
41        private ResponseFactoryInterface                $responseFactory,
42    ) {
43    }
44
45    /**
46     * Renders "Current events" widget tile partial.
47     *
48     * @param ServerRequestInterface $request PSR-7 server request.
49     * @param PermissionContext      $context Security context.
50     * @return ResponseInterface HTML partial response.
51     */
52    public function widgetCurrentEvents(
53        ServerRequestInterface $request,
54        PermissionContext      $context
55    ): ResponseInterface {
56        $queryParams = $request->getQueryParams();
57        $limit = isset($queryParams['limit']) ? max(1, (int) $queryParams['limit']) : 5;
58        $events = $this->widgetsService->getCurrentCalendarEvents($context, $limit);
59        $totalCount = $this->widgetsService->getCurrentCalendarEventsCount($context);
60
61        $html = $this->twig->render('dashboard/partials/widget_current_events.twig', [
62            'events'      => $events,
63            'total_count' => $totalCount,
64            'limit'       => $limit,
65        ]);
66
67        return $this->htmlResponse($html);
68    }
69
70    /**
71     * Renders "Overdue events" widget tile partial.
72     *
73     * @param ServerRequestInterface $request PSR-7 server request.
74     * @param PermissionContext      $context Security context.
75     * @return ResponseInterface HTML partial response.
76     */
77    public function widgetOverdueEvents(
78        ServerRequestInterface $request,
79        PermissionContext      $context
80    ): ResponseInterface {
81        $queryParams = $request->getQueryParams();
82        $limit = isset($queryParams['limit']) ? max(1, (int) $queryParams['limit']) : 5;
83        $events = $this->widgetsService->getOverdueCalendarEvents($context, $limit);
84        $totalCount = $this->widgetsService->getOverdueCalendarEventsCount($context);
85
86        $html = $this->twig->render('dashboard/partials/widget_overdue_events.twig', [
87            'events'      => $events,
88            'total_count' => $totalCount,
89            'limit'       => $limit,
90        ]);
91
92        return $this->htmlResponse($html);
93    }
94
95    /**
96     * Renders "Add widget" button and dropdown partial.
97     *
98     * @param ServerRequestInterface $request PSR-7 server request.
99     * @param PermissionContext      $context Security context.
100     * @return ResponseInterface HTML partial response.
101     */
102    public function widgetPickerButton(
103        PermissionContext      $context
104    ): ResponseInterface {
105        $userId = $context->actorUserId;
106        $inactive = $this->widgetsService->getInactiveWidgets($userId, $context);
107        $html = $this->twig->render('dashboard/partials/widget_picker_button.twig', [
108            'inactive_widgets' => $inactive,
109        ]);
110
111        return $this->htmlResponse($html);
112    }
113
114    /**
115     * Handles widget toggle via HTMX and returns updated grid & picker partials.
116     *
117     * @param ServerRequestInterface $request PSR-7 server request.
118     * @param PermissionContext      $context Security context.
119     * @return ResponseInterface HTML response.
120     */
121    public function toggleWidget(
122        ServerRequestInterface $request,
123        PermissionContext      $context
124    ): ResponseInterface {
125        $queryParams = $request->getQueryParams();
126        $parsedBody = (array) ($request->getParsedBody() ?? []);
127        $widgetKey = (string) ($parsedBody['widget'] ?? $queryParams['widget'] ?? '');
128        $enable = (bool) ($parsedBody['enable'] ?? $queryParams['enable'] ?? false);
129
130        if ($widgetKey !== '') {
131            $this->widgetsService->toggleWidget($context->actorUserId, $widgetKey, $enable);
132        }
133
134        $userId = $context->actorUserId;
135        $activeKeys = $this->widgetsService->getActiveWidgetKeys($userId);
136        $inactive = $this->widgetsService->getInactiveWidgets($userId, $context);
137        $currentEvents = in_array(DashboardWidgetsServiceInterface::WIDGET_CURRENT, $activeKeys, true)
138            ? $this->widgetsService->getCurrentCalendarEvents($context, 5)
139            : [];
140        $currentTotalCount = in_array(DashboardWidgetsServiceInterface::WIDGET_CURRENT, $activeKeys, true)
141            ? $this->widgetsService->getCurrentCalendarEventsCount($context)
142            : 0;
143        $overdueEvents = in_array(DashboardWidgetsServiceInterface::WIDGET_OVERDUE, $activeKeys, true)
144            ? $this->widgetsService->getOverdueCalendarEvents($context, 5)
145            : [];
146        $overdueTotalCount = in_array(DashboardWidgetsServiceInterface::WIDGET_OVERDUE, $activeKeys, true)
147            ? $this->widgetsService->getOverdueCalendarEventsCount($context)
148            : 0;
149
150        $html = $this->twig->render('dashboard/partials/widgets_container.twig', [
151            'active_keys'         => $activeKeys,
152            'inactive_widgets'    => $inactive,
153            'current_events'      => $currentEvents,
154            'current_total_count' => $currentTotalCount,
155            'overdue_events'      => $overdueEvents,
156            'overdue_total_count' => $overdueTotalCount,
157        ]);
158
159        return $this->htmlResponse($html);
160    }
161
162    /**
163     * Renders event action modal content.
164     *
165     * @param int                    $id      Event record ID.
166     * @param PermissionContext      $context Security context.
167     * @return ResponseInterface HTML modal body response.
168     */
169    public function eventModal(
170        int                    $id,
171        PermissionContext      $context
172    ): ResponseInterface {
173        $event = $this->actionService->getEventDetails($id, $context);
174        $html = $this->twig->render('dashboard/partials/event_action_modal.twig', [
175            'event' => $event,
176        ]);
177
178        return $this->htmlResponse($html);
179    }
180
181    /**
182     * Creates HTML PSR-7 response.
183     *
184     * @param string $html Rendered HTML snippet.
185     * @return ResponseInterface PSR-7 response.
186     */
187    private function htmlResponse(string $html): ResponseInterface
188    {
189        $response = $this->responseFactory->createResponse(200);
190        $response->getBody()->write($html);
191
192        return $response->withHeader('Content-Type', 'text/html; charset=utf-8');
193    }
194}