Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 362
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
BackstockPanelController
0.00% covered (danger)
0.00%
0 / 362
0.00% covered (danger)
0.00%
0 / 12
3540
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 binToArray
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 getBinsToPull
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 1
182
 getTodayActivity
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
30
 getBinDetails
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
30
 quickAction
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
156
 searchBin
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 getCategories
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getLocations
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 updateBin
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
72
 hideBin
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 getActiveOrPreparingEvents
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\Workbook\Controllers;
4
5use BuyerKiosk\Backstock\BackstockFactory;
6use BuyerKiosk\Backstock\Bin;
7use BuyerKiosk\Backstock\Action;
8use BuyerKiosk\Backstock\Event;
9use BuyerKiosk\Backstock\EventService;
10use Exception;
11
12/**
13 * Backstock Panel API Controller for Workbook
14 *
15 * Provides endpoints for the workbook's backstock panel to display bins to pull,
16 * today's activity, and handle quick actions on bins.
17 *
18 * @package BuyerKiosk\Workbook
19 */
20class BackstockPanelController
21{
22    /**
23     * @var \Slim\Slim Slim application instance
24     */
25    private $app;
26
27    /**
28     * @var \Store Store object
29     */
30    private $store;
31
32    /**
33     * @var \PDO Database connection
34     */
35    private $db;
36
37    /**
38     * @var BackstockFactory Backstock factory
39     */
40    private $bsFactory;
41
42    /**
43     * Constructor
44     *
45     * @param \Slim\Slim $app Slim application instance
46     * @param \Store $store Store object
47     */
48    public function __construct($app, \Store $store)
49    {
50        $this->app = $app;
51        $this->store = $store;
52        $this->db = dbConnectByName($store->getDbName());
53        $this->bsFactory = new BackstockFactory($store);
54    }
55
56    /**
57     * Convert a Bin object to an array for JSON response
58     */
59    private function binToArray(Bin $binObj): array
60    {
61        return [
62            'id' => $binObj->id,
63            'name' => $binObj->name,
64            'uuid' => $binObj->uuid,
65            'mainCategory' => $binObj->mainCategory,
66            'location' => $binObj->location,
67            'locationID' => $binObj->locationID ?? null,
68            'onSite' => $binObj->onSite ?? false,
69            'age' => $binObj->age,
70            'ageColor' => $binObj->ageColor,
71            'dateCreatedReadable' => $binObj->dateCreatedReadable,
72            'actionStringDate' => $binObj->actionStringDate ?? null,
73            'actionStringEmployee' => $binObj->actionStringEmployee ?? null,
74            'actionStringAction' => $binObj->actionStringAction ?? null,
75            'actionStringCategory' => $binObj->actionStringCategory ?? null
76        ];
77    }
78
79    /**
80     * GET /api/:typeNum/workbook/backstock/bins-to-pull
81     *
82     * Get bins that need to be pulled from storage based on event status:
83     * - Active event: Returns bins linked to that event's categories that haven't been pulled
84     * - Preparing event: Returns calculated daily target based on remaining days
85     * - No event: Returns configurable amount (default 10) of oldest bins
86     *
87     * Always separates results into onSite and offSite arrays and includes event info if applicable
88     */
89    public function getBinsToPull()
90    {
91        try {
92            $limit = $this->app->request->get('limit') ?: 10;
93            $eventService = new EventService($this->store);
94
95            // Check for active events
96            $activeEvents = $this->getActiveOrPreparingEvents();
97
98            $result = [
99                'success' => true,
100                'onSite' => [],
101                'offSite' => [],
102                'event' => null,
103                'mode' => 'default'
104            ];
105
106            if (!empty($activeEvents)) {
107                $event = $activeEvents[0]; // Use first active/preparing event
108                $today = new \DateTime();
109                $startDate = new \DateTime($event['startDate']);
110                $buildUpStart = new \DateTime($event['startDate']);
111                $buildUpStart->modify("-{$event['buildUpDays']} days");
112
113                // Determine if active or preparing
114                if ($today >= $startDate) {
115                    // Active event - get bins linked to event categories that haven't been pulled
116                    $result['mode'] = 'active_event';
117                    $result['event'] = [
118                        'id' => $event['id'],
119                        'name' => $event['name'],
120                        'startDate' => $event['startDate'],
121                        'endDate' => $event['endDate'],
122                        'phase' => 'active'
123                    ];
124
125                    $bins = $eventService->getBinsToPull();
126
127                } elseif ($today >= $buildUpStart && $today < $startDate) {
128                    // Preparing event - calculate daily target
129                    $result['mode'] = 'preparing_event';
130                    $result['event'] = [
131                        'id' => $event['id'],
132                        'name' => $event['name'],
133                        'startDate' => $event['startDate'],
134                        'endDate' => $event['endDate'],
135                        'phase' => 'build_up',
136                        'buildUpDays' => $event['buildUpDays']
137                    ];
138
139                    // Calculate daily target
140                    $allEventBins = $eventService->getBinsToPull();
141                    $offSiteBins = array_filter($allEventBins, function($bin) {
142                        return !$bin['onSite'];
143                    });
144
145                    $totalBins = count($offSiteBins);
146                    $remainingDays = $today->diff($startDate)->days;
147                    if ($remainingDays > 0) {
148                        $dailyTarget = ceil($totalBins / $remainingDays);
149                    } else {
150                        $dailyTarget = $totalBins;
151                    }
152
153                    $result['event']['dailyTarget'] = $dailyTarget;
154                    $result['event']['remainingDays'] = $remainingDays;
155                    $result['event']['totalBins'] = $totalBins;
156
157                    // Return daily target amount, sorted by oldest
158                    usort($offSiteBins, function($a, $b) {
159                        return $b['age'] - $a['age'];
160                    });
161
162                    $bins = array_slice($offSiteBins, 0, $dailyTarget);
163                }
164            } else {
165                // No event - return default amount of oldest bins
166                $result['mode'] = 'default';
167
168                $stmt = $this->db->prepare("
169                    SELECT DISTINCT b.*, DATE(b.ageDate) as dateNoTime
170                    FROM bsBins b
171                    LEFT JOIN bsLocations l ON b.location = l.id
172                    WHERE b.deleted = 0
173                    AND b.active = 1
174                    ORDER BY b.ageDate ASC
175                    LIMIT :limit
176                ");
177                $stmt->bindValue(":limit", (int)$limit, \PDO::PARAM_INT);
178                $stmt->execute();
179
180                $this->bsFactory->prepareLookupArrays();
181
182                $bins = [];
183                while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
184                    $bin = new Bin($this->store);
185                    $bin->createFromRow($row);
186                    $binObj = $this->bsFactory->makeBinReadable($bin);
187                    $bins[] = $this->binToArray($binObj);
188                }
189            }
190
191            // Separate into onSite and offSite
192            if (isset($bins)) {
193                foreach ($bins as $bin) {
194                    if (isset($bin['onSite']) && $bin['onSite']) {
195                        $result['onSite'][] = $bin;
196                    } else {
197                        $result['offSite'][] = $bin;
198                    }
199                }
200            }
201
202            $this->app->response->headers->set('Content-Type', 'application/json');
203            $this->app->response->setBody(json_encode($result));
204
205        } catch (\Throwable $e) {
206            error_log("BackstockPanelController::getBinsToPull error: " . $e->getMessage() . " at " . $e->getFile() . ":" . $e->getLine());
207            $this->app->halt(500, json_encode([
208                'error' => 'Failed to retrieve bins to pull',
209                'message' => $e->getMessage()
210            ]));
211        }
212    }
213
214    /**
215     * GET /api/:typeNum/workbook/backstock/today-activity
216     *
217     * Get bins with actions performed today
218     */
219    public function getTodayActivity()
220    {
221        try {
222            // Get bins that had actions today with their latest action info
223            $stmt = $this->db->prepare("
224                SELECT DISTINCT b.*, DATE(b.ageDate) as dateNoTime,
225                       (SELECT COUNT(*) FROM bsActions WHERE binID = b.id AND DATE(timePerformed) = CURDATE()) as actionCount
226                FROM bsBins b
227                INNER JOIN bsActions a ON b.id = a.binID
228                WHERE b.deleted = 0
229                AND DATE(a.timePerformed) = CURDATE()
230                ORDER BY (SELECT MAX(timePerformed) FROM bsActions WHERE binID = b.id AND DATE(timePerformed) = CURDATE()) DESC
231            ");
232            $stmt->execute();
233
234            $this->bsFactory->prepareLookupArrays();
235
236            $bins = [];
237            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
238                $bin = new Bin($this->store);
239                $bin->createFromRow($row);
240                $binObj = $this->bsFactory->makeBinReadable($bin);
241
242                // Convert Bin object to array for JSON response
243                $binData = $this->binToArray($binObj);
244                $binData['actionCount'] = (int)$row['actionCount'];
245
246                // Get today's actions for this bin (optimized - no heavy operations)
247                $actionsStmt = $this->db->prepare("
248                    SELECT a.id, a.binID, a.action, a.categoryID, a.timePerformed,
249                           CONCAT(e.employeeFirstName, ' ', e.employeeLastName) as employeeName
250                    FROM bsActions a
251                    LEFT JOIN employees e ON a.employeeID = e.employeeID
252                    WHERE a.binID = :binID
253                    AND DATE(a.timePerformed) = CURDATE()
254                    ORDER BY a.timePerformed DESC
255                    LIMIT 5
256                ");
257                $actionsStmt->bindValue(":binID", $bin->id);
258                $actionsStmt->execute();
259
260                $actions = [];
261                while ($actionRow = $actionsStmt->fetch(\PDO::FETCH_ASSOC)) {
262                    // Format time in store timezone
263                    $date = new \DateTime($actionRow['timePerformed'], new \DateTimeZone("UTC"));
264                    $date->setTimezone(new \DateTimeZone($this->store->getTimeZone()));
265
266                    $actions[] = [
267                        'id' => $actionRow['id'],
268                        'action' => (int)$actionRow['action'],
269                        'categoryID' => $actionRow['categoryID'],
270                        'employeeName' => $actionRow['employeeName'] ?: 'Unknown',
271                        'timeStamp' => $date->format("Y-m-d H:i:s"),
272                        'dateReadable' => $date->format("M jS, Y g:i a"),
273                        'dateReadableShort' => $date->format("g:i a")
274                    ];
275                }
276
277                $binData['todayActions'] = $actions;
278                $bins[] = $binData;
279            }
280
281            $this->app->response->headers->set('Content-Type', 'application/json');
282            $this->app->response->setBody(json_encode([
283                'success' => true,
284                'bins' => $bins,
285                'count' => count($bins)
286            ]));
287
288        } catch (\Throwable $e) {
289            error_log("BackstockPanelController::getTodayActivity error: " . $e->getMessage() . " at " . $e->getFile() . ":" . $e->getLine());
290            $this->app->halt(500, json_encode([
291                'error' => 'Failed to retrieve today\'s activity',
292                'message' => $e->getMessage()
293            ]));
294        }
295    }
296
297    /**
298     * GET /api/:typeNum/workbook/backstock/bin/:binId
299     *
300     * Get full bin details including categories, recent actions, and location info
301     */
302    public function getBinDetails($binId)
303    {
304        try {
305            $bin = new Bin($this->store);
306            if (!$bin->readByID($binId)) {
307                $this->app->halt(404, json_encode([
308                    'error' => 'Bin not found'
309                ]));
310                return;
311            }
312
313            $this->bsFactory->prepareLookupArrays();
314            $binObj = $this->bsFactory->makeBinReadable($bin);
315            $binData = $this->binToArray($binObj);
316
317            // Get categories for this bin
318            $binData['categories'] = $bin->getCategories();
319
320            // Get recent actions (last 20) - optimized query
321            $stmt = $this->db->prepare("
322                SELECT a.id, a.binID, a.action, a.categoryID, a.timePerformed,
323                       CONCAT(e.employeeFirstName, ' ', e.employeeLastName) as employeeName,
324                       c.name as categoryName, c.color as categoryColor
325                FROM bsActions a
326                LEFT JOIN employees e ON a.employeeID = e.employeeID
327                LEFT JOIN bsCategories c ON a.categoryID = c.id
328                WHERE a.binID = :binID
329                ORDER BY a.timePerformed DESC, a.id DESC
330                LIMIT 20
331            ");
332            $stmt->bindValue(":binID", $binId);
333            $stmt->execute();
334
335            $actions = [];
336            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
337                // Format time in store timezone
338                $date = new \DateTime($row['timePerformed'], new \DateTimeZone("UTC"));
339                $date->setTimezone(new \DateTimeZone($this->store->getTimeZone()));
340
341                $actions[] = [
342                    'id' => $row['id'],
343                    'action' => (int)$row['action'],
344                    'categoryID' => $row['categoryID'],
345                    'categoryName' => $row['categoryName'],
346                    'categoryColor' => $row['categoryColor'],
347                    'employeeName' => $row['employeeName'] ?: 'Unknown',
348                    'timeStamp' => $date->format("Y-m-d H:i:s"),
349                    'dateReadable' => $date->format("M jS, Y g:i a"),
350                    'dateReadableShort' => $date->format("g:i a")
351                ];
352            }
353
354            $binData['recentActions'] = $actions;
355
356            $this->app->response->headers->set('Content-Type', 'application/json');
357            $this->app->response->setBody(json_encode([
358                'success' => true,
359                'bin' => $binData
360            ]));
361
362        } catch (\Throwable $e) {
363            error_log("BackstockPanelController::getBinDetails error: " . $e->getMessage() . " at " . $e->getFile() . ":" . $e->getLine());
364            $this->app->halt(500, json_encode([
365                'error' => 'Failed to retrieve bin details',
366                'message' => $e->getMessage()
367            ]));
368        }
369    }
370
371    /**
372     * POST /api/:typeNum/workbook/backstock/bin/:binId/quick-action
373     *
374     * Handle quick actions on bins
375     * Supports: 'empty' (action=0), 'add' (action=1), 'remove_some' (action=2), 'remove_all' (action=3)
376     *
377     * Request body:
378     * {
379     *   "action": "empty|add|remove_some|remove_all",
380     *   "categoryId": 123,  // Required for add/remove actions
381     *   "employeeId": 456   // Optional, uses app user if not provided
382     * }
383     */
384    public function quickAction($binId)
385    {
386        try {
387            $data = json_decode($this->app->request->getBody(), true);
388
389            if (!isset($data['action'])) {
390                $this->app->halt(400, json_encode([
391                    'error' => 'Action is required'
392                ]));
393                return;
394            }
395
396            $bin = new Bin($this->store);
397            if (!$bin->readByID($binId)) {
398                $this->app->halt(404, json_encode([
399                    'error' => 'Bin not found'
400                ]));
401                return;
402            }
403
404            // Map action string to action ID
405            $actionMap = [
406                'empty' => 0,
407                'add' => 1,
408                'remove_some' => 2,
409                'remove_all' => 3
410            ];
411
412            if (!isset($actionMap[$data['action']])) {
413                $this->app->halt(400, json_encode([
414                    'error' => 'Invalid action type'
415                ]));
416                return;
417            }
418
419            $actionId = $actionMap[$data['action']];
420
421            // Validate categoryId for add/remove actions
422            if (in_array($actionId, [1, 2, 3]) && !isset($data['categoryId'])) {
423                $this->app->halt(400, json_encode([
424                    'error' => 'Category ID is required for this action'
425                ]));
426                return;
427            }
428
429            // Get employee ID
430            $employeeId = isset($data['employeeId']) ? (int)$data['employeeId'] : ($this->app->user->id ?? 0);
431
432            // Create action record
433            $action = new Action($this->store);
434            $action->binID = $binId;
435            $action->action = $actionId;
436            $action->employeeID = $employeeId;
437            $action->categoryID = isset($data['categoryId']) ? (int)$data['categoryId'] : null;
438
439            if (!$action->create()) {
440                $this->app->halt(500, json_encode([
441                    'error' => 'Failed to create action'
442                ]));
443                return;
444            }
445
446            // Mark bin as used if this is the first action
447            if (!$bin->hasBeenUsed) {
448                $bin->markAsUsed();
449            }
450
451            // If action is "empty" (0), clear categories and optionally hide the bin
452            if ($actionId === 0) {
453                $bin->mainCategory = null;
454                $bin->editSubCategories([]);
455                $bin->update();
456
457                // Note: We don't automatically hide the bin in quick actions
458                // The bin can be hidden manually later if needed
459            }
460
461            // Reload bin with updated data
462            $bin->readByID($binId);
463            $this->bsFactory->prepareLookupArrays();
464            $binData = $this->bsFactory->makeBinReadable($bin);
465
466            $this->app->response->headers->set('Content-Type', 'application/json');
467            $this->app->response->setBody(json_encode([
468                'success' => true,
469                'message' => 'Action completed successfully',
470                'bin' => $binData
471            ]));
472
473        } catch (Exception $e) {
474            error_log("BackstockPanelController::quickAction error: " . $e->getMessage());
475            $this->app->halt(500, json_encode([
476                'error' => 'Failed to perform action',
477                'message' => $e->getMessage()
478            ]));
479        }
480    }
481
482    /**
483     * GET /api/:typeNum/workbook/backstock/search?q=query
484     *
485     * Search for bins by name, UUID, or category name
486     */
487    public function searchBin()
488    {
489        try {
490            $query = $this->app->request->get('q');
491
492            if (empty($query)) {
493                $this->app->halt(400, json_encode([
494                    'error' => 'Search query is required'
495                ]));
496                return;
497            }
498
499            // Search bins by name, UUID, or category name (main category or subcategories)
500            $stmt = $this->db->prepare("
501                SELECT DISTINCT b.*, DATE(b.ageDate) as dateNoTime
502                FROM bsBins b
503                LEFT JOIN bsCategories mc ON b.mainCategory = mc.id
504                LEFT JOIN bsCategories sc1 ON b.subCat1 = sc1.id
505                LEFT JOIN bsCategories sc2 ON b.subCat2 = sc2.id
506                LEFT JOIN bsCategories sc3 ON b.subCat3 = sc3.id
507                WHERE b.deleted = 0
508                AND b.active = 1
509                AND (
510                    b.name LIKE :query
511                    OR b.uuid LIKE :query
512                    OR mc.name LIKE :query
513                    OR sc1.name LIKE :query
514                    OR sc2.name LIKE :query
515                    OR sc3.name LIKE :query
516                )
517                ORDER BY b.ageDate DESC
518                LIMIT 20
519            ");
520            $stmt->bindValue(":query", "%{$query}%");
521            $stmt->execute();
522
523            $this->bsFactory->prepareLookupArrays();
524
525            $bins = [];
526            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
527                $bin = new Bin($this->store);
528                $bin->createFromRow($row);
529                $binObj = $this->bsFactory->makeBinReadable($bin);
530                $bins[] = $this->binToArray($binObj);
531            }
532
533            $this->app->response->headers->set('Content-Type', 'application/json');
534            $this->app->response->setBody(json_encode([
535                'success' => true,
536                'bins' => $bins,
537                'count' => count($bins),
538                'query' => $query
539            ]));
540
541        } catch (Exception $e) {
542            error_log("BackstockPanelController::searchBin error: " . $e->getMessage());
543            $this->app->halt(500, json_encode([
544                'error' => 'Failed to search bins'
545            ]));
546        }
547    }
548
549    /**
550     * GET /api/:typeNum/workbook/backstock/categories
551     *
552     * Get all categories for dropdown selections
553     */
554    public function getCategories()
555    {
556        try {
557            $stmt = $this->db->prepare("
558                SELECT id, name, color
559                FROM bsCategories
560                ORDER BY name ASC
561            ");
562            $stmt->execute();
563
564            $categories = $stmt->fetchAll(\PDO::FETCH_ASSOC);
565
566            $this->app->response->headers->set('Content-Type', 'application/json');
567            $this->app->response->setBody(json_encode([
568                'success' => true,
569                'categories' => $categories
570            ]));
571
572        } catch (Exception $e) {
573            error_log("BackstockPanelController::getCategories error: " . $e->getMessage());
574            $this->app->halt(500, json_encode([
575                'error' => 'Failed to retrieve categories'
576            ]));
577        }
578    }
579
580    /**
581     * GET /api/:typeNum/workbook/backstock/locations
582     *
583     * Get all locations for dropdown selections
584     */
585    public function getLocations()
586    {
587        try {
588            $stmt = $this->db->prepare("
589                SELECT id, name, onSite
590                FROM bsLocations
591                ORDER BY name ASC
592            ");
593            $stmt->execute();
594
595            $locations = $stmt->fetchAll(\PDO::FETCH_ASSOC);
596
597            $this->app->response->headers->set('Content-Type', 'application/json');
598            $this->app->response->setBody(json_encode([
599                'success' => true,
600                'locations' => $locations
601            ]));
602
603        } catch (Exception $e) {
604            error_log("BackstockPanelController::getLocations error: " . $e->getMessage());
605            $this->app->halt(500, json_encode([
606                'error' => 'Failed to retrieve locations'
607            ]));
608        }
609    }
610
611    /**
612     * POST /api/:typeNum/workbook/backstock/bin/:binId/update
613     *
614     * Update bin details (location, categories, notes)
615     *
616     * Request body:
617     * {
618     *   "location": 123,      // Location ID
619     *   "categories": [1,2],  // Array of category IDs
620     *   "notes": "string"     // Notes text
621     * }
622     */
623    public function updateBin($binId)
624    {
625        try {
626            $data = json_decode($this->app->request->getBody(), true);
627
628            $bin = new Bin($this->store);
629            if (!$bin->readByID($binId)) {
630                $this->app->halt(404, json_encode([
631                    'error' => 'Bin not found'
632                ]));
633                return;
634            }
635
636            // Update location if provided
637            if (isset($data['location'])) {
638                $bin->location = (int)$data['location'];
639
640                // Update onSite status based on location
641                $stmt = $this->db->prepare("SELECT onSite FROM bsLocations WHERE id = :id");
642                $stmt->bindValue(":id", $data['location']);
643                $stmt->execute();
644                $locationRow = $stmt->fetch(\PDO::FETCH_ASSOC);
645                if ($locationRow) {
646                    $bin->onSite = (int)$locationRow['onSite'];
647                }
648            }
649
650            // Update notes if provided
651            if (isset($data['notes'])) {
652                $bin->notes = $data['notes'];
653            }
654
655            // Update bin
656            $bin->update();
657
658            // Update categories if provided
659            if (isset($data['categories']) && is_array($data['categories'])) {
660                $bin->editSubCategories($data['categories']);
661            }
662
663            // Reload and return updated bin
664            $bin->readByID($binId);
665            $this->bsFactory->prepareLookupArrays();
666            $binData = $this->bsFactory->makeBinReadable($bin);
667
668            $this->app->response->headers->set('Content-Type', 'application/json');
669            $this->app->response->setBody(json_encode([
670                'success' => true,
671                'message' => 'Bin updated successfully',
672                'bin' => $binData
673            ]));
674
675        } catch (Exception $e) {
676            error_log("BackstockPanelController::updateBin error: " . $e->getMessage());
677            $this->app->halt(500, json_encode([
678                'error' => 'Failed to update bin',
679                'message' => $e->getMessage()
680            ]));
681        }
682    }
683
684    /**
685     * POST /api/:typeNum/workbook/backstock/bin/:binId/hide
686     *
687     * Hide a bin (for bins that have been emptied)
688     */
689    public function hideBin($binId)
690    {
691        try {
692            $bin = new Bin($this->store);
693            if (!$bin->readByID($binId)) {
694                $this->app->halt(404, json_encode([
695                    'error' => 'Bin not found'
696                ]));
697                return;
698            }
699
700            if (!$bin->canBeHidden()) {
701                $this->app->halt(400, json_encode([
702                    'error' => 'Bin cannot be hidden until it has been used'
703                ]));
704                return;
705            }
706
707            $bin->hide();
708
709            $this->app->response->headers->set('Content-Type', 'application/json');
710            $this->app->response->setBody(json_encode([
711                'success' => true,
712                'message' => 'Bin hidden successfully'
713            ]));
714
715        } catch (Exception $e) {
716            error_log("BackstockPanelController::hideBin error: " . $e->getMessage());
717            $this->app->halt(500, json_encode([
718                'error' => 'Failed to hide bin'
719            ]));
720        }
721    }
722
723    /**
724     * Helper method to get active or preparing events
725     *
726     * @return array Array of active or preparing events
727     */
728    private function getActiveOrPreparingEvents()
729    {
730        try {
731            $stmt = $this->db->prepare("
732                SELECT id, name, startDate, endDate, buildUpDays, windDownDays, color, icon
733                FROM bsEvents
734                WHERE isActive = 1
735                AND (
736                    -- Active phase: between start and end date
737                    (startDate <= CURDATE() AND endDate >= CURDATE())
738                    OR
739                    -- Build-up phase: between build-up start and event start
740                    (DATE_SUB(startDate, INTERVAL buildUpDays DAY) <= CURDATE() AND startDate > CURDATE())
741                )
742                ORDER BY startDate ASC
743            ");
744            $stmt->execute();
745
746            return $stmt->fetchAll(\PDO::FETCH_ASSOC);
747
748        } catch (Exception $e) {
749            error_log("BackstockPanelController::getActiveOrPreparingEvents error: " . $e->getMessage());
750            return [];
751        }
752    }
753}