Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 290
0.00% covered (danger)
0.00%
0 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventController
0.00% covered (danger)
0.00%
0 / 290
0.00% covered (danger)
0.00%
0 / 22
6480
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getEvents
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
90
 getEvent
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 createEvent
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
380
 updateEvent
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 deleteEvent
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getTemplates
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 createFromTemplate
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 getBinCountsByCategories
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 getBinsToPull
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getBinsToStore
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getBinsForEvent
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 updateProgress
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getReadiness
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getAlerts
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 acknowledgeAlert
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 generateAlerts
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getDashboard
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getTimeline
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getRequestJson
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 outputJson
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 outputError
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BuyerKiosk\Backstock\Controllers;
4
5
6use BuyerKiosk\Backstock\EventService;
7/**
8 * EventController - API endpoints for seasonal events management
9 *
10 * Handles all HTTP endpoints for the seasonal events system including
11 * event CRUD operations, bin operations, progress tracking, alerts,
12 * templates, and dashboard data.
13 *
14 * @package BuyerKiosk\Backstock
15 */
16class EventController extends \BuyerKiosk\Core\Controllers\BaseController
17{
18    /**
19     * @var \Store Store object
20     */
21    private $store;
22
23    /**
24     * @var EventService Event service instance
25     */
26    private $eventService;
27
28    /**
29     * Constructor
30     *
31     * @param \Slim\Slim $app Slim application instance
32     * @param \Store $store Store object
33     */
34    public function __construct($app, \Store $store)
35    {
36        parent::__construct($app);
37        $this->store = $store;
38        $this->eventService = new EventService($store);
39    }
40
41    // ==================== EVENT MANAGEMENT ENDPOINTS ====================
42
43    /**
44     * GET /api/:typeNum/backstock/events
45     * Get all events with summary data
46     *
47     * Query params:
48     * - year: Filter by year (optional)
49     * - active: Filter by active status (true/false) (optional)
50     */
51    public function getEvents()
52    {
53        try {
54            $request = $this->_app->request;
55            $year = $request->get('year');
56            $active = $request->get('active');
57            $status = $request->get('status');
58
59            $events = $this->eventService->getAllEvents();
60
61            // Apply filters if provided
62            if ($year !== null) {
63                $events = array_filter($events, function($event) use ($year) {
64                    return $event['year'] == $year;
65                });
66            }
67
68            if ($active !== null) {
69                $activeFilter = filter_var($active, FILTER_VALIDATE_BOOLEAN);
70                $events = array_filter($events, function($event) use ($activeFilter) {
71                    return $event['is_active'] == $activeFilter;
72                });
73            }
74
75            // Filter by status (currentPhase)
76            if ($status !== null) {
77                $events = array_filter($events, function($event) use ($status) {
78                    $phase = $event['currentPhase'] ?? '';
79
80                    if ($status === 'active') {
81                        // Active includes: build_up, active, wind_down phases
82                        return in_array($phase, ['build_up', 'active', 'wind_down']);
83                    } elseif ($status === 'upcoming') {
84                        // Upcoming events haven't started yet
85                        return $phase === 'upcoming';
86                    } elseif ($status === 'completed') {
87                        return $phase === 'completed';
88                    } elseif ($status === 'cancelled') {
89                        return $phase === 'cancelled';
90                    }
91
92                    // Direct phase match
93                    return $phase === $status;
94                });
95            }
96
97            // Re-index array after filtering
98            $events = array_values($events);
99
100            $this->outputJson([
101                'success' => true,
102                'events' => $events,
103                'count' => count($events)
104            ]);
105        } catch (\Exception $e) {
106            error_log("EventController::getEvents error: " . $e->getMessage());
107            $this->outputError('Failed to retrieve events', 500);
108        }
109    }
110
111    /**
112     * GET /api/:typeNum/backstock/events/:eventId
113     * Get single event with full details
114     *
115     * @param int $eventId Event ID
116     */
117    public function getEvent($eventId)
118    {
119        try {
120            $event = $this->eventService->getEventById($eventId);
121
122            if (!$event) {
123                $this->outputError('Event not found', 404);
124                return;
125            }
126
127            $this->outputJson([
128                'success' => true,
129                'event' => $event
130            ]);
131        } catch (\Exception $e) {
132            error_log("EventController::getEvent error: " . $e->getMessage());
133            $this->outputError('Failed to retrieve event', 500);
134        }
135    }
136
137    /**
138     * POST /api/:typeNum/backstock/events
139     * Create new event
140     *
141     * Body: {
142     *   name, startDate, endDate,
143     *   eventType (optional), year (optional - derived from startDate),
144     *   buildUpDays/prepDays, windDownDays, color, icon, notes,
145     *   isRecurring, categoryIds, categoryPriorities, priority, targetBins
146     * }
147     */
148    public function createEvent()
149    {
150        try {
151            $data = $this->getRequestJson();
152
153            // Validate required fields
154            $required = ['name', 'startDate', 'endDate'];
155            foreach ($required as $field) {
156                if (!isset($data[$field]) || empty($data[$field])) {
157                    $this->outputError("Missing required field: $field", 400);
158                    return;
159                }
160            }
161
162            // Derive year from startDate if not provided
163            if (!isset($data['year']) || empty($data['year'])) {
164                $startDate = new \DateTime($data['startDate']);
165                $data['year'] = (int)$startDate->format('Y');
166            }
167
168            // Default eventType if not provided (must match enum: season, holiday, sale, custom)
169            if (!isset($data['eventType']) || empty($data['eventType'])) {
170                $data['eventType'] = 'season';
171            }
172
173            // Map prepDays to buildUpDays if needed
174            if (isset($data['prepDays']) && !isset($data['buildUpDays'])) {
175                $data['buildUpDays'] = $data['prepDays'];
176            }
177
178            // Convert categoryIds from comma-separated string to array if needed
179            if (isset($data['categoryIds']) && is_string($data['categoryIds']) && !empty($data['categoryIds'])) {
180                $data['categoryIds'] = array_map('intval', explode(',', $data['categoryIds']));
181            } elseif (!isset($data['categoryIds']) || empty($data['categoryIds'])) {
182                $data['categoryIds'] = [];
183            }
184
185            // Map description to notes
186            if (isset($data['description']) && !isset($data['notes'])) {
187                $data['notes'] = $data['description'];
188            }
189
190            $event = $this->eventService->createEvent($data);
191
192            if (!$event) {
193                $this->outputError('Failed to create event', 500);
194                return;
195            }
196
197            // Get full event details to return
198            $eventData = $this->eventService->getEventById($event->getId());
199
200            $this->outputJson([
201                'success' => true,
202                'message' => 'Event created successfully',
203                'event' => $eventData
204            ], 201);
205        } catch (\Exception $e) {
206            error_log("EventController::createEvent error: " . $e->getMessage());
207            $this->outputError('Failed to create event: ' . $e->getMessage(), 500);
208        }
209    }
210
211    /**
212     * POST /api/:typeNum/backstock/events/:eventId
213     * Update existing event
214     *
215     * @param int $eventId Event ID
216     */
217    public function updateEvent($eventId)
218    {
219        try {
220            $data = $this->getRequestJson();
221
222            $event = $this->eventService->updateEvent($eventId, $data);
223
224            if (!$event) {
225                $this->outputError('Event not found or update failed', 404);
226                return;
227            }
228
229            // Get full event details to return
230            $eventData = $this->eventService->getEventById($event->getId());
231
232            $this->outputJson([
233                'success' => true,
234                'message' => 'Event updated successfully',
235                'event' => $eventData
236            ]);
237        } catch (\Exception $e) {
238            error_log("EventController::updateEvent error: " . $e->getMessage());
239            $this->outputError('Failed to update event: ' . $e->getMessage(), 500);
240        }
241    }
242
243    /**
244     * DELETE /api/:typeNum/backstock/events/:eventId
245     * Delete event
246     *
247     * @param int $eventId Event ID
248     */
249    public function deleteEvent($eventId)
250    {
251        try {
252            $success = $this->eventService->deleteEvent($eventId);
253
254            if (!$success) {
255                $this->outputError('Event not found or delete failed', 404);
256                return;
257            }
258
259            $this->outputJson([
260                'success' => true,
261                'message' => 'Event deleted successfully'
262            ]);
263        } catch (\Exception $e) {
264            error_log("EventController::deleteEvent error: " . $e->getMessage());
265            $this->outputError('Failed to delete event: ' . $e->getMessage(), 500);
266        }
267    }
268
269    // ==================== TEMPLATE ENDPOINTS ====================
270
271    /**
272     * GET /api/:typeNum/backstock/events/templates
273     * Get all available event templates
274     */
275    public function getTemplates()
276    {
277        try {
278            $templates = $this->eventService->getTemplates();
279
280            $this->outputJson([
281                'success' => true,
282                'templates' => $templates,
283                'count' => count($templates)
284            ]);
285        } catch (\Exception $e) {
286            error_log("EventController::getTemplates error: " . $e->getMessage());
287            $this->outputError('Failed to retrieve templates', 500);
288        }
289    }
290
291    /**
292     * POST /api/:typeNum/backstock/events/from-template
293     * Create event from template
294     *
295     * Body: { templateId, year, startDate, endDate }
296     */
297    public function createFromTemplate()
298    {
299        try {
300            $data = $this->getRequestJson();
301
302            // Validate required fields
303            $required = ['templateId', 'year', 'startDate', 'endDate'];
304            foreach ($required as $field) {
305                if (!isset($data[$field]) || empty($data[$field])) {
306                    $this->outputError("Missing required field: $field", 400);
307                    return;
308                }
309            }
310
311            $event = $this->eventService->createFromTemplate(
312                $data['templateId'],
313                $data['year'],
314                $data['startDate'],
315                $data['endDate']
316            );
317
318            if (!$event) {
319                $this->outputError('Failed to create event from template', 500);
320                return;
321            }
322
323            // Get full event details to return
324            $eventData = $this->eventService->getEventById($event->getId());
325
326            $this->outputJson([
327                'success' => true,
328                'message' => 'Event created from template successfully',
329                'event' => $eventData
330            ], 201);
331        } catch (\Exception $e) {
332            error_log("EventController::createFromTemplate error: " . $e->getMessage());
333            $this->outputError('Failed to create event from template: ' . $e->getMessage(), 500);
334        }
335    }
336
337    // ==================== BIN OPERATIONS ENDPOINTS ====================
338
339    /**
340     * GET /api/:typeNum/backstock/events/bins/count
341     * Get bin counts for a set of category IDs (used for preview when creating events)
342     *
343     * Query params:
344     * - categoryIds: Comma-separated list of category IDs
345     */
346    public function getBinCountsByCategories()
347    {
348        try {
349            $request = $this->_app->request;
350            $categoryIdsParam = $request->get('categoryIds');
351
352            if (empty($categoryIdsParam)) {
353                $this->outputJson([
354                    'success' => true,
355                    'total' => 0,
356                    'onsite' => 0,
357                    'offsite' => 0
358                ]);
359                return;
360            }
361
362            $categoryIds = array_map('intval', explode(',', $categoryIdsParam));
363            $counts = $this->eventService->getBinCountsByCategoryIds($categoryIds);
364
365            $this->outputJson([
366                'success' => true,
367                'total' => $counts['total'],
368                'onsite' => $counts['onsite'],
369                'offsite' => $counts['offsite'],
370                'categoryIds' => $categoryIds
371            ]);
372        } catch (\Exception $e) {
373            error_log("EventController::getBinCountsByCategories error: " . $e->getMessage());
374            $this->outputError('Failed to retrieve bin counts', 500);
375        }
376    }
377
378    /**
379     * GET /api/:typeNum/backstock/events/bins/pull
380     * Get bins that need to be pulled from storage
381     *
382     * Query params:
383     * - days: Number of days to look ahead (default 14)
384     */
385    public function getBinsToPull()
386    {
387        try {
388            $request = $this->_app->request;
389            $days = $request->get('days') ?? 14;
390
391            $bins = $this->eventService->getBinsToPull((int)$days);
392
393            $this->outputJson([
394                'success' => true,
395                'bins' => $bins,
396                'count' => count($bins),
397                'daysAhead' => (int)$days
398            ]);
399        } catch (\Exception $e) {
400            error_log("EventController::getBinsToPull error: " . $e->getMessage());
401            $this->outputError('Failed to retrieve bins to pull', 500);
402        }
403    }
404
405    /**
406     * GET /api/:typeNum/backstock/events/bins/store
407     * Get bins that need to be returned to storage
408     *
409     * Query params:
410     * - days: Number of days to look ahead (default 14)
411     */
412    public function getBinsToStore()
413    {
414        try {
415            $request = $this->_app->request;
416            $days = $request->get('days') ?? 14;
417
418            $bins = $this->eventService->getBinsToStore((int)$days);
419
420            $this->outputJson([
421                'success' => true,
422                'bins' => $bins,
423                'count' => count($bins),
424                'daysAhead' => (int)$days
425            ]);
426        } catch (\Exception $e) {
427            error_log("EventController::getBinsToStore error: " . $e->getMessage());
428            $this->outputError('Failed to retrieve bins to store', 500);
429        }
430    }
431
432    /**
433     * GET /api/:typeNum/backstock/events/:eventId/bins
434     * Get bins linked to event's categories
435     *
436     * @param int $eventId Event ID
437     */
438    public function getBinsForEvent($eventId)
439    {
440        try {
441            // Get event details first to show linked categories
442            $event = $this->eventService->getEventById($eventId);
443            $bins = $this->eventService->getBinsForEvent($eventId);
444
445            $this->outputJson([
446                'success' => true,
447                'eventId' => $eventId,
448                'eventName' => $event ? $event['name'] : null,
449                'linkedCategories' => $event ? $event['categories'] : [],
450                'linkedCategoryCount' => $event ? $event['categoryCount'] : 0,
451                'bins' => $bins,
452                'count' => count($bins)
453            ]);
454        } catch (\Exception $e) {
455            error_log("EventController::getBinsForEvent error: " . $e->getMessage());
456            $this->outputError('Failed to retrieve bins for event', 500);
457        }
458    }
459
460    // ==================== PROGRESS TRACKING ENDPOINTS ====================
461
462    /**
463     * POST /api/:typeNum/backstock/events/:eventId/progress
464     * Update event progress based on actual bin locations
465     *
466     * @param int $eventId Event ID
467     */
468    public function updateProgress($eventId)
469    {
470        try {
471            $progress = $this->eventService->updateEventProgress($eventId);
472
473            if (!$progress) {
474                $this->outputError('Event not found or progress update failed', 404);
475                return;
476            }
477
478            $this->outputJson([
479                'success' => true,
480                'message' => 'Progress updated successfully',
481                'progress' => $progress->toArray()
482            ]);
483        } catch (\Exception $e) {
484            error_log("EventController::updateProgress error: " . $e->getMessage());
485            $this->outputError('Failed to update progress: ' . $e->getMessage(), 500);
486        }
487    }
488
489    /**
490     * GET /api/:typeNum/backstock/events/readiness
491     * Get seasonal readiness summary for all upcoming events
492     */
493    public function getReadiness()
494    {
495        try {
496            $readiness = $this->eventService->getSeasonalReadiness();
497
498            $this->outputJson([
499                'success' => true,
500                'readiness' => $readiness,
501                'count' => count($readiness)
502            ]);
503        } catch (\Exception $e) {
504            error_log("EventController::getReadiness error: " . $e->getMessage());
505            $this->outputError('Failed to retrieve readiness data', 500);
506        }
507    }
508
509    // ==================== ALERT ENDPOINTS ====================
510
511    /**
512     * GET /api/:typeNum/backstock/events/alerts
513     * Get unacknowledged alerts
514     */
515    public function getAlerts()
516    {
517        try {
518            $alerts = $this->eventService->getUnacknowledgedAlerts();
519
520            $this->outputJson([
521                'success' => true,
522                'alerts' => $alerts,
523                'count' => count($alerts)
524            ]);
525        } catch (\Exception $e) {
526            error_log("EventController::getAlerts error: " . $e->getMessage());
527            $this->outputError('Failed to retrieve alerts', 500);
528        }
529    }
530
531    /**
532     * POST /api/:typeNum/backstock/events/alerts/:alertId/acknowledge
533     * Mark alert as acknowledged
534     *
535     * Body: { employeeId }
536     *
537     * @param int $alertId Alert ID
538     */
539    public function acknowledgeAlert($alertId)
540    {
541        try {
542            $data = $this->getRequestJson();
543
544            if (!isset($data['employeeId']) || empty($data['employeeId'])) {
545                $this->outputError('Missing required field: employeeId', 400);
546                return;
547            }
548
549            $success = $this->eventService->acknowledgeAlert($alertId, $data['employeeId']);
550
551            if (!$success) {
552                $this->outputError('Alert not found or acknowledgment failed', 404);
553                return;
554            }
555
556            $this->outputJson([
557                'success' => true,
558                'message' => 'Alert acknowledged successfully'
559            ]);
560        } catch (\Exception $e) {
561            error_log("EventController::acknowledgeAlert error: " . $e->getMessage());
562            $this->outputError('Failed to acknowledge alert: ' . $e->getMessage(), 500);
563        }
564    }
565
566    /**
567     * POST /api/:typeNum/backstock/events/generate-alerts
568     * Trigger alert generation for all active events
569     */
570    public function generateAlerts()
571    {
572        try {
573            $alerts = $this->eventService->generateAlerts();
574
575            $this->outputJson([
576                'success' => true,
577                'message' => 'Alerts generated successfully',
578                'alerts' => $alerts,
579                'count' => count($alerts)
580            ]);
581        } catch (\Exception $e) {
582            error_log("EventController::generateAlerts error: " . $e->getMessage());
583            $this->outputError('Failed to generate alerts: ' . $e->getMessage(), 500);
584        }
585    }
586
587    // ==================== DASHBOARD ENDPOINTS ====================
588
589    /**
590     * GET /api/:typeNum/backstock/events/dashboard
591     * Get dashboard summary data
592     */
593    public function getDashboard()
594    {
595        try {
596            $dashboard = $this->eventService->getDashboardSummary();
597
598            $this->outputJson([
599                'success' => true,
600                'dashboard' => $dashboard
601            ]);
602        } catch (\Exception $e) {
603            error_log("EventController::getDashboard error: " . $e->getMessage());
604            $this->outputError('Failed to retrieve dashboard data', 500);
605        }
606    }
607
608    /**
609     * GET /api/:typeNum/backstock/events/timeline
610     * Get timeline data for calendar view
611     *
612     * Query params:
613     * - months: Number of months to include (default 6)
614     */
615    public function getTimeline()
616    {
617        try {
618            $request = $this->_app->request;
619            $months = $request->get('months') ?? 6;
620
621            $timeline = $this->eventService->getEventTimeline((int)$months);
622
623            $this->outputJson([
624                'success' => true,
625                'timeline' => $timeline,
626                'count' => count($timeline),
627                'months' => (int)$months
628            ]);
629        } catch (\Exception $e) {
630            error_log("EventController::getTimeline error: " . $e->getMessage());
631            $this->outputError('Failed to retrieve timeline data', 500);
632        }
633    }
634
635    // ==================== HELPER METHODS ====================
636
637    /**
638     * Get and parse JSON from request body
639     *
640     * @return array Parsed JSON data
641     */
642    private function getRequestJson()
643    {
644        $body = $this->_app->request->getBody();
645        $data = json_decode($body, true);
646
647        if (json_last_error() !== JSON_ERROR_NONE) {
648            $this->outputError('Invalid JSON in request body', 400);
649            exit;
650        }
651
652        return $data ?? [];
653    }
654
655    /**
656     * Output JSON response with proper headers
657     *
658     * @param array $data Response data
659     * @param int $statusCode HTTP status code (default 200)
660     */
661    private function outputJson($data, $statusCode = 200)
662    {
663        $this->_app->response->headers->set('Content-Type', 'application/json');
664        $this->_app->response->setStatus($statusCode);
665        $this->_app->response->setBody(json_encode($data));
666    }
667
668    /**
669     * Output error response
670     *
671     * @param string $message Error message
672     * @param int $code HTTP status code (default 400)
673     */
674    private function outputError($message, $code = 400)
675    {
676        $this->_app->response->headers->set('Content-Type', 'application/json');
677        $this->_app->response->setStatus($code);
678        $this->_app->response->setBody(json_encode([
679            'success' => false,
680            'error' => $message
681        ]));
682    }
683}