Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 548
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventPageController
0.00% covered (danger)
0.00%
0 / 548
0.00% covered (danger)
0.00%
0 / 20
6806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 checkReadPermission
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 checkManagePermission
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 checkTemplatePermission
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 checkReportsPermission
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getStoreContext
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getEventOr404
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getEventStatistics
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
72
 dashboard
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
30
 listView
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
12
 create
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
12
 detail
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
42
 edit
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
20
 duplicate
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
20
 templates
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
30
 reports
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
12
 reportDetail
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
72
 archive
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
42
 getAuditLog
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 getEventCountsByType
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BuyerKiosk\EventManagement\Controllers;
4
5use BuyerKiosk\Core\Controllers\BaseController;
6use BuyerKiosk\EventManagement\Services\EventService;
7use BuyerKiosk\EventManagement\Services\TemplateService;
8use BuyerKiosk\EventManagement\Models\Event;
9
10/**
11 * EventPageController
12 *
13 * Handles rendering of the Event Management admin pages.
14 * These pages provide store managers with tools to create, manage,
15 * and track promotional events and their associated integrations.
16 *
17 * Routes served:
18 * - /admin/:typeNum/events                    → dashboard() - Kanban view
19 * - /admin/:typeNum/events/list               → listView() - Table view
20 * - /admin/:typeNum/events/create             → create() - Create wizard
21 * - /admin/:typeNum/events/:eventId           → detail() - Event detail page
22 * - /admin/:typeNum/events/:eventId/edit      → edit() - Edit wizard
23 * - /admin/:typeNum/events/:eventId/duplicate → duplicate() - Duplicate wizard
24 * - /admin/:typeNum/events/templates          → templates() - Template management
25 * - /admin/:typeNum/events/reports            → reports() - Reports dashboard
26 * - /admin/:typeNum/events/reports/:eventId   → reportDetail() - Single event report
27 * - /admin/:typeNum/events/archive            → archive() - Archived events
28 *
29 * @package BuyerKiosk\EventManagement\Controllers
30 */
31class EventPageController extends BaseController
32{
33    /**
34     * @var \Store Store object (pre-validated by route)
35     */
36    protected $store;
37
38    /**
39     * Constructor
40     *
41     * @param \Slim\Slim $app The Slim application instance
42     * @param \Store $store The store object (pre-validated by route)
43     */
44    public function __construct($app, \Store $store)
45    {
46        parent::__construct($app);
47        $this->store = $store;
48    }
49
50    /**
51     * Check read permission for events
52     *
53     * @return bool True if authorized
54     */
55    private function checkReadPermission(): bool
56    {
57        $app = $this->_app;
58
59        if (!$app->user) {
60            $app->redirect($app->urlFor('login'));
61            return false;
62        }
63
64        if (!$app->user->checkAccess('uri_events')) {
65            $app->notAuthorized();
66            return false;
67        }
68
69        return true;
70    }
71
72    /**
73     * Check write/manage permission for events
74     *
75     * @return bool True if authorized
76     */
77    private function checkManagePermission(): bool
78    {
79        $app = $this->_app;
80
81        if (!$app->user) {
82            $app->redirect($app->urlFor('login'));
83            return false;
84        }
85
86        if (!$app->user->checkAccess('uri_events_manage')) {
87            $app->notAuthorized();
88            return false;
89        }
90
91        return true;
92    }
93
94    /**
95     * Check template management permission
96     *
97     * @return bool True if authorized
98     */
99    private function checkTemplatePermission(): bool
100    {
101        $app = $this->_app;
102
103        if (!$app->user) {
104            $app->redirect($app->urlFor('login'));
105            return false;
106        }
107
108        if (!$app->user->checkAccess('uri_events_templates')) {
109            $app->notAuthorized();
110            return false;
111        }
112
113        return true;
114    }
115
116    /**
117     * Check reports permission
118     *
119     * @return bool True if authorized
120     */
121    private function checkReportsPermission(): bool
122    {
123        $app = $this->_app;
124
125        if (!$app->user) {
126            $app->redirect($app->urlFor('login'));
127            return false;
128        }
129
130        if (!$app->user->checkAccess('uri_events_reports')) {
131            $app->notAuthorized();
132            return false;
133        }
134
135        return true;
136    }
137
138    /**
139     * Validate store access and return store context
140     *
141     * @param string $typeNum Store type number
142     * @return array|null ['store' => Store, 'db' => PDO, 'storeDir' => string] or null on failure
143     */
144    private function getStoreContext(string $typeNum): ?array
145    {
146        $app = $this->_app;
147
148        // Check user has store access
149        if (!$app->user->checkStoreGroup($typeNum)) {
150            $app->notAuthorized();
151            return null;
152        }
153
154        // Store is already validated by routes, use it from constructor
155        $store = $this->store;
156
157        // Get database connection
158        $db = \dbConnectByName($store->getDbName());
159
160        // Get store directory for CSS/asset paths
161        $storeDir = \getStoreDirectory($store);
162
163        return [
164            'store' => $store,
165            'db' => $db,
166            'storeDir' => $storeDir,
167        ];
168    }
169
170    /**
171     * Get event by ID with 404 handling
172     *
173     * @param \PDO $db Database connection
174     * @param int $eventId Event ID
175     * @return Event|null Event object or null (with 404 rendered)
176     */
177    private function getEventOr404(\PDO $db, int $eventId): ?Event
178    {
179        $eventService = new EventService($db, $this->_app->user->id);
180        $event = $eventService->get($eventId);
181
182        if (!$event) {
183            $this->_app->notFound();
184            return null;
185        }
186
187        return $event;
188    }
189
190    /**
191     * Get summary statistics for the events dashboard
192     *
193     * @param \PDO $db Database connection
194     * @return array Statistics array
195     */
196    private function getEventStatistics(\PDO $db): array
197    {
198        $stats = [
199            'total' => 0,
200            'draft' => 0,
201            'scheduled' => 0,
202            'active' => 0,
203            'completed' => 0,
204            'cancelled' => 0,
205            'upcoming' => 0,
206            'buildUp' => 0,
207            'windDown' => 0,
208        ];
209
210        try {
211            // Check if events table exists
212            $tableCheck = $db->query("SHOW TABLES LIKE 'events'");
213            if ($tableCheck->rowCount() === 0) {
214                return $stats;
215            }
216
217            // Count by status
218            $stmt = $db->query("
219                SELECT status, COUNT(*) as cnt
220                FROM events
221                WHERE status != 'archived'
222                GROUP BY status
223            ");
224            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
225                $status = $row['status'];
226                $stats[$status] = (int) $row['cnt'];
227                $stats['total'] += (int) $row['cnt'];
228            }
229
230            // Count by phase (only for non-archived events)
231            $stmt = $db->query("
232                SELECT phase, COUNT(*) as cnt
233                FROM events
234                WHERE status NOT IN ('archived', 'cancelled', 'completed')
235                GROUP BY phase
236            ");
237            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
238                $phase = $row['phase'];
239                if ($phase === 'upcoming') {
240                    $stats['upcoming'] = (int) $row['cnt'];
241                } elseif ($phase === 'build_up') {
242                    $stats['buildUp'] = (int) $row['cnt'];
243                } elseif ($phase === 'wind_down') {
244                    $stats['windDown'] = (int) $row['cnt'];
245                }
246            }
247        } catch (\Exception $e) {
248            error_log("EventPageController::getEventStatistics error: " . $e->getMessage());
249        }
250
251        return $stats;
252    }
253
254    // =========================================================================
255    // PAGE METHODS
256    // =========================================================================
257
258    /**
259     * Render the events dashboard (Kanban view)
260     *
261     * GET /admin/:typeNum/events
262     *
263     * Displays events in a Kanban-style board grouped by status/phase.
264     * Default landing page for event management.
265     *
266     * @param string $typeNum Store type number
267     */
268    public function dashboard(string $typeNum): void
269    {
270        if (!$this->checkReadPermission()) {
271            return;
272        }
273
274        $context = $this->getStoreContext($typeNum);
275        if (!$context) {
276            return;
277        }
278
279        $app = $this->_app;
280        $store = $context['store'];
281        $db = $context['db'];
282
283        // Get summary statistics
284        $stats = $this->getEventStatistics($db);
285
286        // Get events grouped by status for Kanban view
287        $eventService = new EventService($db, $app->user->id);
288        $events = $eventService->list(['includeArchived' => false]);
289
290        // Group events by status for Kanban columns
291        $eventsByStatus = [
292            'draft' => [],
293            'scheduled' => [],
294            'active' => [],
295            'completed' => [],
296        ];
297
298        foreach ($events as $event) {
299            if (isset($eventsByStatus[$event->status])) {
300                $eventsByStatus[$event->status][] = $event->toArray();
301            }
302        }
303
304        $app->render('admin/event-management/dashboard.html', [
305            'page' => [
306                'title' => 'Event Management',
307                'description' => 'Manage store events and promotions',
308            ],
309            'store' => [
310                'typeNum' => $store->getTypeNum(),
311                'name' => $store->getCompanyName(),
312                'city' => $store->getCity(),
313                'storeType' => $context['storeDir'],
314            ],
315            'typeNum' => $typeNum,
316            'stats' => $stats,
317            'eventsByStatus' => $eventsByStatus,
318            'csrf_token' => \NoCSRF::generate('csrf_token'),
319            'user' => $app->user,
320            'canManage' => $app->user->checkAccess('uri_events_manage'),
321            'canViewReports' => $app->user->checkAccess('uri_events_reports'),
322            'canManageTemplates' => $app->user->checkAccess('uri_events_templates'),
323            'activePage' => 'dashboard',
324            'activeView' => 'kanban',
325        ]);
326    }
327
328    /**
329     * Render the events list view (Table view)
330     *
331     * GET /admin/:typeNum/events/list
332     *
333     * Displays events in a traditional table view with filtering and sorting.
334     *
335     * @param string $typeNum Store type number
336     */
337    public function listView(string $typeNum): void
338    {
339        if (!$this->checkReadPermission()) {
340            return;
341        }
342
343        $context = $this->getStoreContext($typeNum);
344        if (!$context) {
345            return;
346        }
347
348        $app = $this->_app;
349        $store = $context['store'];
350        $db = $context['db'];
351
352        // Get summary statistics
353        $stats = $this->getEventStatistics($db);
354
355        // Get all non-archived events
356        $eventService = new EventService($db, $app->user->id);
357        $events = $eventService->list(['includeArchived' => false]);
358
359        // Convert to arrays for template
360        $eventsArray = array_map(function ($event) {
361            return $event->toArray();
362        }, $events);
363
364        $app->render('admin/event-management/list.html', [
365            'page' => [
366                'title' => 'Event Management - List View',
367                'description' => 'View and manage store events',
368            ],
369            'store' => [
370                'typeNum' => $store->getTypeNum(),
371                'name' => $store->getCompanyName(),
372                'city' => $store->getCity(),
373                'storeType' => $context['storeDir'],
374            ],
375            'typeNum' => $typeNum,
376            'stats' => $stats,
377            'events' => $eventsArray,
378            'csrf_token' => \NoCSRF::generate('csrf_token'),
379            'user' => $app->user,
380            'canManage' => $app->user->checkAccess('uri_events_manage'),
381            'canViewReports' => $app->user->checkAccess('uri_events_reports'),
382            'canManageTemplates' => $app->user->checkAccess('uri_events_templates'),
383            'activePage' => 'dashboard',
384            'activeView' => 'list',
385        ]);
386    }
387
388    /**
389     * Render the event creation wizard
390     *
391     * GET /admin/:typeNum/events/create
392     *
393     * Multi-step wizard for creating new events with integrations.
394     *
395     * @param string $typeNum Store type number
396     */
397    public function create(string $typeNum): void
398    {
399        if (!$this->checkManagePermission()) {
400            return;
401        }
402
403        $context = $this->getStoreContext($typeNum);
404        if (!$context) {
405            return;
406        }
407
408        $app = $this->_app;
409        $store = $context['store'];
410
411        // Get available templates from central database
412        $centralDb = \dbConnectByName('kiosk_buykiosk');
413        $templateService = new TemplateService($centralDb);
414
415        // Get store type prefix (first 2 chars of typeNum)
416        $storeType = substr($typeNum, 0, 2);
417        $templates = $templateService->list($storeType);
418
419        // Convert templates to arrays for template
420        $templatesArray = array_map(function ($template) {
421            return $template->toArray();
422        }, $templates);
423
424        // Get event types and default values
425        $eventTypes = Event::getValidTypes();
426
427        $app->render('admin/event-management/create.html', [
428            'page' => [
429                'title' => 'Create Event',
430                'description' => 'Create a new store event',
431            ],
432            'store' => [
433                'typeNum' => $store->getTypeNum(),
434                'name' => $store->getCompanyName(),
435                'city' => $store->getCity(),
436                'storeType' => $context['storeDir'],
437            ],
438            'typeNum' => $typeNum,
439            'templates' => $templatesArray,
440            'eventTypes' => $eventTypes,
441            'defaultBuildUpDays' => 14,
442            'defaultWindDownDays' => 7,
443            'currentYear' => (int) date('Y'),
444            'csrf_token' => \NoCSRF::generate('csrf_token'),
445            'user' => $app->user,
446            'activePage' => 'create',
447        ]);
448    }
449
450    /**
451     * Render the event detail page
452     *
453     * GET /admin/:typeNum/events/:eventId
454     *
455     * Shows complete event information including integrations, timeline, and audit log.
456     *
457     * @param string $typeNum Store type number
458     * @param int $eventId Event ID
459     */
460    public function detail(string $typeNum, int $eventId): void
461    {
462        if (!$this->checkReadPermission()) {
463            return;
464        }
465
466        $context = $this->getStoreContext($typeNum);
467        if (!$context) {
468            return;
469        }
470
471        $app = $this->_app;
472        $store = $context['store'];
473        $db = $context['db'];
474
475        // Get event
476        $eventService = new EventService($db, $app->user->id);
477        $event = $eventService->get($eventId);
478
479        if (!$event) {
480            $app->notFound();
481            return;
482        }
483
484        // Get integrations for this event
485        $integrations = $eventService->getIntegrations($eventId);
486
487        // Convert integrations to arrays
488        $integrationsArray = array_map(function ($integration) {
489            return $integration->toArray();
490        }, $integrations);
491
492        // Group integrations by type
493        $integrationsByType = [];
494        foreach ($integrationsArray as $integration) {
495            $type = $integration['integrationType'];
496            if (!isset($integrationsByType[$type])) {
497                $integrationsByType[$type] = [];
498            }
499            $integrationsByType[$type][] = $integration;
500        }
501
502        // Get audit log entries
503        $auditLog = $this->getAuditLog($db, $eventId);
504
505        $app->render('admin/event-management/detail.html', [
506            'page' => [
507                'title' => $event->name,
508                'description' => 'Event details and management',
509            ],
510            'store' => [
511                'typeNum' => $store->getTypeNum(),
512                'name' => $store->getCompanyName(),
513                'city' => $store->getCity(),
514                'storeType' => $context['storeDir'],
515            ],
516            'typeNum' => $typeNum,
517            'event' => $event->toArray(),
518            'integrations' => $integrationsArray,
519            'integrationsByType' => $integrationsByType,
520            'auditLog' => $auditLog,
521            'csrf_token' => \NoCSRF::generate('csrf_token'),
522            'user' => $app->user,
523            'canManage' => $app->user->checkAccess('uri_events_manage'),
524            'canViewReports' => $app->user->checkAccess('uri_events_reports'),
525            'activePage' => 'detail',
526        ]);
527    }
528
529    /**
530     * Render the event edit wizard
531     *
532     * GET /admin/:typeNum/events/:eventId/edit
533     *
534     * Edit existing event settings and integrations.
535     *
536     * @param string $typeNum Store type number
537     * @param int $eventId Event ID
538     */
539    public function edit(string $typeNum, int $eventId): void
540    {
541        if (!$this->checkManagePermission()) {
542            return;
543        }
544
545        $context = $this->getStoreContext($typeNum);
546        if (!$context) {
547            return;
548        }
549
550        $app = $this->_app;
551        $store = $context['store'];
552        $db = $context['db'];
553
554        // Get event
555        $eventService = new EventService($db, $app->user->id);
556        $event = $eventService->get($eventId);
557
558        if (!$event) {
559            $app->notFound();
560            return;
561        }
562
563        // Check if event can be edited (only draft and scheduled can be fully edited)
564        $canFullEdit = in_array($event->status, [Event::STATUS_DRAFT, Event::STATUS_SCHEDULED]);
565
566        // Get integrations for this event
567        $integrations = $eventService->getIntegrations($eventId);
568
569        // Convert integrations to arrays
570        $integrationsArray = array_map(function ($integration) {
571            return $integration->toArray();
572        }, $integrations);
573
574        // Get event types
575        $eventTypes = Event::getValidTypes();
576
577        $app->render('admin/event-management/edit.html', [
578            'page' => [
579                'title' => 'Edit: ' . $event->name,
580                'description' => 'Edit event settings and integrations',
581            ],
582            'store' => [
583                'typeNum' => $store->getTypeNum(),
584                'name' => $store->getCompanyName(),
585                'city' => $store->getCity(),
586                'storeType' => $context['storeDir'],
587            ],
588            'typeNum' => $typeNum,
589            'event' => $event->toArray(),
590            'integrations' => $integrationsArray,
591            'eventTypes' => $eventTypes,
592            'canFullEdit' => $canFullEdit,
593            'csrf_token' => \NoCSRF::generate('csrf_token'),
594            'user' => $app->user,
595            'activePage' => 'edit',
596        ]);
597    }
598
599    /**
600     * Render the event duplication wizard
601     *
602     * GET /admin/:typeNum/events/:eventId/duplicate
603     *
604     * Create a new event based on an existing one with new dates.
605     *
606     * @param string $typeNum Store type number
607     * @param int $eventId Source event ID
608     */
609    public function duplicate(string $typeNum, int $eventId): void
610    {
611        if (!$this->checkManagePermission()) {
612            return;
613        }
614
615        $context = $this->getStoreContext($typeNum);
616        if (!$context) {
617            return;
618        }
619
620        $app = $this->_app;
621        $store = $context['store'];
622        $db = $context['db'];
623
624        // Get source event
625        $eventService = new EventService($db, $app->user->id);
626        $event = $eventService->get($eventId);
627
628        if (!$event) {
629            $app->notFound();
630            return;
631        }
632
633        // Get integrations for this event (to show what will be duplicated)
634        $integrations = $eventService->getIntegrations($eventId);
635
636        // Convert integrations to arrays
637        $integrationsArray = array_map(function ($integration) {
638            return $integration->toArray();
639        }, $integrations);
640
641        // Get event types
642        $eventTypes = Event::getValidTypes();
643
644        $app->render('admin/event-management/duplicate.html', [
645            'page' => [
646                'title' => 'Duplicate: ' . $event->name,
647                'description' => 'Create a copy of this event with new dates',
648            ],
649            'store' => [
650                'typeNum' => $store->getTypeNum(),
651                'name' => $store->getCompanyName(),
652                'city' => $store->getCity(),
653                'storeType' => $context['storeDir'],
654            ],
655            'typeNum' => $typeNum,
656            'sourceEvent' => $event->toArray(),
657            'integrations' => $integrationsArray,
658            'integrationCount' => count($integrationsArray),
659            'eventTypes' => $eventTypes,
660            'currentYear' => (int) date('Y'),
661            'suggestedName' => $event->name . ' (Copy)',
662            'csrf_token' => \NoCSRF::generate('csrf_token'),
663            'user' => $app->user,
664            'activePage' => 'duplicate',
665        ]);
666    }
667
668    /**
669     * Render the template management page
670     *
671     * GET /admin/:typeNum/events/templates
672     *
673     * Manage event templates available for this store.
674     *
675     * @param string $typeNum Store type number
676     */
677    public function templates(string $typeNum): void
678    {
679        if (!$this->checkTemplatePermission()) {
680            return;
681        }
682
683        $context = $this->getStoreContext($typeNum);
684        if (!$context) {
685            return;
686        }
687
688        $app = $this->_app;
689        $store = $context['store'];
690
691        // Get available templates from central database
692        $centralDb = \dbConnectByName('kiosk_buykiosk');
693        $templateService = new TemplateService($centralDb);
694
695        // Get store type prefix
696        $storeType = substr($typeNum, 0, 2);
697
698        // Get all templates available for this store
699        $templates = $templateService->list($storeType);
700
701        // Convert templates to arrays and group by scope
702        $templatesByScope = [
703            'global' => [],
704            'franchise' => [],
705            'store' => [],
706        ];
707
708        foreach ($templates as $template) {
709            $templateArray = $template->toArray();
710            $scope = $templateArray['scope'] ?? 'global';
711            if (isset($templatesByScope[$scope])) {
712                $templatesByScope[$scope][] = $templateArray;
713            }
714        }
715
716        // Get event types for template creation
717        $eventTypes = Event::getValidTypes();
718
719        $app->render('admin/event-management/templates.html', [
720            'page' => [
721                'title' => 'Event Templates',
722                'description' => 'Manage event templates for your store',
723            ],
724            'store' => [
725                'typeNum' => $store->getTypeNum(),
726                'name' => $store->getCompanyName(),
727                'city' => $store->getCity(),
728                'storeType' => $context['storeDir'],
729            ],
730            'typeNum' => $typeNum,
731            'templatesByScope' => $templatesByScope,
732            'totalTemplates' => count($templates),
733            'eventTypes' => $eventTypes,
734            'storeType' => $storeType,
735            'csrf_token' => \NoCSRF::generate('csrf_token'),
736            'user' => $app->user,
737            'activePage' => 'templates',
738        ]);
739    }
740
741    /**
742     * Render the reports dashboard
743     *
744     * GET /admin/:typeNum/events/reports
745     *
746     * Overview of event performance and analytics.
747     *
748     * @param string $typeNum Store type number
749     */
750    public function reports(string $typeNum): void
751    {
752        if (!$this->checkReportsPermission()) {
753            return;
754        }
755
756        $context = $this->getStoreContext($typeNum);
757        if (!$context) {
758            return;
759        }
760
761        $app = $this->_app;
762        $store = $context['store'];
763        $db = $context['db'];
764
765        // Get summary statistics
766        $stats = $this->getEventStatistics($db);
767
768        // Get completed and active events for reporting
769        $eventService = new EventService($db, $app->user->id);
770        $activeEvents = $eventService->list(['status' => Event::STATUS_ACTIVE]);
771        $completedEvents = $eventService->list(['status' => Event::STATUS_COMPLETED]);
772
773        // Convert to arrays
774        $activeEventsArray = array_map(function ($event) {
775            return $event->toArray();
776        }, $activeEvents);
777
778        $completedEventsArray = array_map(function ($event) {
779            return $event->toArray();
780        }, $completedEvents);
781
782        // Get year-to-date event counts by type
783        $eventsByType = $this->getEventCountsByType($db);
784
785        $app->render('admin/event-management/reports.html', [
786            'page' => [
787                'title' => 'Event Reports',
788                'description' => 'Event performance and analytics',
789            ],
790            'store' => [
791                'typeNum' => $store->getTypeNum(),
792                'name' => $store->getCompanyName(),
793                'city' => $store->getCity(),
794                'storeType' => $context['storeDir'],
795            ],
796            'typeNum' => $typeNum,
797            'stats' => $stats,
798            'activeEvents' => $activeEventsArray,
799            'completedEvents' => $completedEventsArray,
800            'eventsByType' => $eventsByType,
801            'currentYear' => (int) date('Y'),
802            'csrf_token' => \NoCSRF::generate('csrf_token'),
803            'user' => $app->user,
804            'activePage' => 'reports',
805        ]);
806    }
807
808    /**
809     * Render single event report detail
810     *
811     * GET /admin/:typeNum/events/reports/:eventId
812     *
813     * Detailed analytics and performance metrics for a specific event.
814     *
815     * @param string $typeNum Store type number
816     * @param int $eventId Event ID
817     */
818    public function reportDetail(string $typeNum, int $eventId): void
819    {
820        if (!$this->checkReadPermission()) {
821            return;
822        }
823
824        // Also check reports permission for detailed analytics
825        if (!$this->_app->user->checkAccess('uri_events_reports')) {
826            // Fallback to basic read permission if no reports permission
827            // User can view event detail but not advanced analytics
828        }
829
830        $context = $this->getStoreContext($typeNum);
831        if (!$context) {
832            return;
833        }
834
835        $app = $this->_app;
836        $store = $context['store'];
837        $db = $context['db'];
838
839        // Get event
840        $eventService = new EventService($db, $app->user->id);
841        $event = $eventService->get($eventId);
842
843        if (!$event) {
844            $app->notFound();
845            return;
846        }
847
848        // Get integrations for this event
849        $integrations = $eventService->getIntegrations($eventId);
850
851        // Convert integrations to arrays and calculate summary stats
852        $integrationsArray = array_map(function ($integration) {
853            return $integration->toArray();
854        }, $integrations);
855
856        // Get integration summary by type
857        $integrationSummary = [];
858        foreach ($integrationsArray as $integration) {
859            $type = $integration['integrationType'];
860            if (!isset($integrationSummary[$type])) {
861                $integrationSummary[$type] = [
862                    'count' => 0,
863                    'active' => 0,
864                    'completed' => 0,
865                    'failed' => 0,
866                ];
867            }
868            $integrationSummary[$type]['count']++;
869            $status = $integration['status'] ?? 'pending';
870            if (isset($integrationSummary[$type][$status])) {
871                $integrationSummary[$type][$status]++;
872            }
873        }
874
875        // Get audit log entries
876        $auditLog = $this->getAuditLog($db, $eventId);
877
878        $app->render('admin/event-management/report-detail.html', [
879            'page' => [
880                'title' => 'Report: ' . $event->name,
881                'description' => 'Event performance report and analytics',
882            ],
883            'store' => [
884                'typeNum' => $store->getTypeNum(),
885                'name' => $store->getCompanyName(),
886                'city' => $store->getCity(),
887                'storeType' => $context['storeDir'],
888            ],
889            'typeNum' => $typeNum,
890            'event' => $event->toArray(),
891            'integrations' => $integrationsArray,
892            'integrationSummary' => $integrationSummary,
893            'auditLog' => $auditLog,
894            'csrf_token' => \NoCSRF::generate('csrf_token'),
895            'user' => $app->user,
896            'canManage' => $app->user->checkAccess('uri_events_manage'),
897            'activePage' => 'reports',
898        ]);
899    }
900
901    /**
902     * Render the archived events page
903     *
904     * GET /admin/:typeNum/events/archive
905     *
906     * View and manage archived events with restore functionality.
907     *
908     * @param string $typeNum Store type number
909     */
910    public function archive(string $typeNum): void
911    {
912        if (!$this->checkReadPermission()) {
913            return;
914        }
915
916        $context = $this->getStoreContext($typeNum);
917        if (!$context) {
918            return;
919        }
920
921        $app = $this->_app;
922        $store = $context['store'];
923        $db = $context['db'];
924
925        // Get archived events
926        $eventService = new EventService($db, $app->user->id);
927        $archivedEvents = $eventService->list([
928            'status' => Event::STATUS_ARCHIVED,
929            'includeArchived' => true,
930        ]);
931
932        // Convert to arrays
933        $eventsArray = array_map(function ($event) {
934            return $event->toArray();
935        }, $archivedEvents);
936
937        // Get archive statistics
938        $archiveStats = [
939            'total' => count($archivedEvents),
940            'byType' => [],
941            'byYear' => [],
942        ];
943
944        foreach ($archivedEvents as $event) {
945            // Count by type
946            $type = $event->eventType;
947            if (!isset($archiveStats['byType'][$type])) {
948                $archiveStats['byType'][$type] = 0;
949            }
950            $archiveStats['byType'][$type]++;
951
952            // Count by year
953            $year = $event->year;
954            if (!isset($archiveStats['byYear'][$year])) {
955                $archiveStats['byYear'][$year] = 0;
956            }
957            $archiveStats['byYear'][$year]++;
958        }
959
960        // Sort years descending
961        krsort($archiveStats['byYear']);
962
963        $app->render('admin/event-management/archive.html', [
964            'page' => [
965                'title' => 'Archived Events',
966                'description' => 'View and restore archived events',
967            ],
968            'store' => [
969                'typeNum' => $store->getTypeNum(),
970                'name' => $store->getCompanyName(),
971                'city' => $store->getCity(),
972                'storeType' => $context['storeDir'],
973            ],
974            'typeNum' => $typeNum,
975            'events' => $eventsArray,
976            'archiveStats' => $archiveStats,
977            'csrf_token' => \NoCSRF::generate('csrf_token'),
978            'user' => $app->user,
979            'canManage' => $app->user->checkAccess('uri_events_manage'),
980            'activePage' => 'archive',
981        ]);
982    }
983
984    // =========================================================================
985    // PRIVATE HELPER METHODS
986    // =========================================================================
987
988    /**
989     * Get audit log entries for an event
990     *
991     * @param \PDO $db Database connection
992     * @param int $eventId Event ID
993     * @return array Audit log entries
994     */
995    private function getAuditLog(\PDO $db, int $eventId): array
996    {
997        $auditLog = [];
998
999        try {
1000            // Check if audit log table exists
1001            $tableCheck = $db->query("SHOW TABLES LIKE 'event_audit_log'");
1002            if ($tableCheck->rowCount() === 0) {
1003                return $auditLog;
1004            }
1005
1006            $sql = "SELECT eal.*, e.display_name as employee_name
1007                    FROM event_audit_log eal
1008                    LEFT JOIN employees e ON eal.employee_id = e.id
1009                    WHERE eal.event_id = :eventId
1010                    ORDER BY eal.created_at DESC
1011                    LIMIT 50";
1012            $stmt = $db->prepare($sql);
1013            $stmt->execute([':eventId' => $eventId]);
1014
1015            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1016                $auditLog[] = [
1017                    'id' => (int) $row['id'],
1018                    'action' => $row['action'],
1019                    'details' => $row['details'] ? json_decode($row['details'], true) : null,
1020                    'employeeId' => $row['employee_id'] ? (int) $row['employee_id'] : null,
1021                    'employeeName' => $row['employee_name'] ?? 'System',
1022                    'createdAt' => $row['created_at'],
1023                ];
1024            }
1025        } catch (\Exception $e) {
1026            error_log("EventPageController::getAuditLog error: " . $e->getMessage());
1027        }
1028
1029        return $auditLog;
1030    }
1031
1032    /**
1033     * Get event counts by type for the current year
1034     *
1035     * @param \PDO $db Database connection
1036     * @return array Counts by event type
1037     */
1038    private function getEventCountsByType(\PDO $db): array
1039    {
1040        $counts = [
1041            'season' => 0,
1042            'holiday' => 0,
1043            'sale' => 0,
1044            'custom' => 0,
1045        ];
1046
1047        try {
1048            $currentYear = (int) date('Y');
1049            $sql = "SELECT eventType, COUNT(*) as cnt
1050                    FROM events
1051                    WHERE year = :year AND status != 'archived'
1052                    GROUP BY eventType";
1053            $stmt = $db->prepare($sql);
1054            $stmt->execute([':year' => $currentYear]);
1055
1056            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1057                $type = $row['eventType'];
1058                if (isset($counts[$type])) {
1059                    $counts[$type] = (int) $row['cnt'];
1060                }
1061            }
1062        } catch (\Exception $e) {
1063            error_log("EventPageController::getEventCountsByType error: " . $e->getMessage());
1064        }
1065
1066        return $counts;
1067    }
1068}