Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 261
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
WhiteboardApiController
0.00% covered (danger)
0.00%
0 / 261
0.00% covered (danger)
0.00%
0 / 9
2256
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getCanvas
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 saveCanvas
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
42
 clearCanvas
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 getItems
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 addItem
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
90
 updateItem
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
72
 deleteItem
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
42
 getHistory
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace BuyerKiosk\Workbook\Controllers;
4
5use BuyerKiosk\Workbook\WhiteboardManager;
6use BuyerKiosk\Workbook\WorkbookAbly;
7use Exception;
8
9/**
10 * Whiteboard System API Controller
11 *
12 * Handles REST API endpoints for the Workbook Whiteboard System including
13 * canvas management, sticky notes, and whiteboard history.
14 *
15 * This controller uses the unified users table (kiosk_users.users) with store assignments
16 * (kiosk_users.userStoreAssignments) instead of per-store employee tables.
17 *
18 * @package BuyerKiosk\Workbook
19 */
20class WhiteboardApiController
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 Store database connection
34     */
35    private $db;
36
37    /**
38     * @var \PDO Central database connection for unified users
39     */
40    private $centralDb;
41
42    /**
43     * @var string Store identifier
44     */
45    private $typeNum;
46
47    /**
48     * Constructor
49     *
50     * @param \Slim\Slim $app Slim application instance
51     * @param \Store $store Store object
52     */
53    public function __construct($app, \Store $store)
54    {
55        $this->app = $app;
56        $this->store = $store;
57        $this->db = dbConnectByName($store->getDbName());
58        $this->centralDb = \dbConnectByName('kiosk_users');
59        $this->typeNum = $store->getTypeNum();
60    }
61
62    /**
63     * GET /api/:typeNum/workbook/whiteboard/canvas/
64     * Get today's canvas data
65     *
66     * Query params:
67     *   - employeeId: int (optional, for tracking views)
68     *
69     * @param string $typeNum Store identifier
70     */
71    public function getCanvas($typeNum)
72    {
73        try {
74            $employeeId = $this->app->request->get('employeeId');
75            $employeeId = $employeeId ? (int) $employeeId : null;
76
77            $manager = new WhiteboardManager($this->store);
78            $canvas = $manager->getTodayCanvas();
79
80            // Return empty object if no canvas exists
81            if ($canvas === null) {
82                $canvas = [
83                    'canvasData' => null,
84                    'thumbnail' => null,
85                    'lastModifiedBy' => null,
86                    'lastModifiedAt' => null,
87                    'lastModifierName' => null
88                ];
89            } else {
90                // Build modifier name from firstName and lastName
91                $firstName = $canvas['lastModifierFirstName'] ?? '';
92                $lastName = $canvas['lastModifierLastName'] ?? '';
93                $canvas['lastModifierName'] = trim($firstName . ' ' . $lastName);
94
95                // Remove individual name fields
96                unset($canvas['lastModifierFirstName']);
97                unset($canvas['lastModifierLastName']);
98            }
99
100            $this->app->response->headers->set('Content-Type', 'application/json');
101            $this->app->response->setBody(json_encode([
102                'success' => true,
103                'canvasData' => $canvas['canvasData'],
104                'thumbnail' => $canvas['thumbnail'],
105                'lastModifiedBy' => $canvas['lastModifiedBy'],
106                'lastModifiedAt' => $canvas['lastModifiedAt'],
107                'lastModifierName' => $canvas['lastModifierName']
108            ]));
109        } catch (Exception $e) {
110            error_log("WhiteboardApiController::getCanvas error: " . $e->getMessage());
111            $this->app->response->headers->set('Content-Type', 'application/json');
112            $this->app->halt(500, json_encode([
113                'error' => 'Failed to retrieve canvas'
114            ]));
115        }
116    }
117
118    /**
119     * POST /api/:typeNum/workbook/whiteboard/canvas/
120     * Save canvas state
121     *
122     * Request body: {
123     *   "canvasData": {...},
124     *   "thumbnail": "base64..."
125     * }
126     *
127     * Any authenticated user with store access can save
128     *
129     * @param string $typeNum Store identifier
130     */
131    public function saveCanvas($typeNum)
132    {
133        // Check store access (any authenticated user in this store group)
134        if (!$this->app->user->checkStoreGroup($typeNum)) {
135            $this->app->response->headers->set('Content-Type', 'application/json');
136            $this->app->halt(403, json_encode([
137                'error' => 'Access denied to this store'
138            ]));
139            return;
140        }
141
142        try {
143            $data = json_decode($this->app->request->getBody(), true);
144
145            // Validate required fields - only canvasData is required
146            if (!isset($data['canvasData'])) {
147                $this->app->response->headers->set('Content-Type', 'application/json');
148                $this->app->halt(400, json_encode([
149                    'error' => 'canvasData is required'
150                ]));
151                return;
152            }
153
154            $manager = new WhiteboardManager($this->store);
155            $canvasJson = json_encode($data['canvasData']);
156            $thumbnail = $data['thumbnail'] ?? null;
157
158            // Pass null for employeeId - canvas drawings are anonymous
159            $result = $manager->saveCanvas($canvasJson, $thumbnail, null);
160
161            if ($result) {
162                // Publish to Ably for real-time updates
163                try {
164                    $ably = new WorkbookAbly($typeNum);
165                    $ably->whiteboardSave($data['employeeId']);
166                } catch (Exception $e) {
167                    error_log("WhiteboardApiController::saveCanvas Ably error: " . $e->getMessage());
168                }
169
170                $this->app->response->headers->set('Content-Type', 'application/json');
171                $this->app->response->setBody(json_encode([
172                    'success' => true
173                ]));
174            } else {
175                $this->app->response->headers->set('Content-Type', 'application/json');
176                $this->app->halt(500, json_encode([
177                    'error' => 'Failed to save canvas'
178                ]));
179            }
180        } catch (Exception $e) {
181            error_log("WhiteboardApiController::saveCanvas error: " . $e->getMessage());
182            $this->app->response->headers->set('Content-Type', 'application/json');
183            $this->app->halt(500, json_encode([
184                'error' => 'Failed to save canvas'
185            ]));
186        }
187    }
188
189    /**
190     * DELETE /api/:typeNum/workbook/whiteboard/canvas/
191     * Clear today's canvas
192     *
193     * Any authenticated user with store access can clear
194     *
195     * @param string $typeNum Store identifier
196     */
197    public function clearCanvas($typeNum)
198    {
199        // Check store access (any authenticated user in this store group)
200        if (!$this->app->user->checkStoreGroup($typeNum)) {
201            $this->app->response->headers->set('Content-Type', 'application/json');
202            $this->app->halt(403, json_encode([
203                'error' => 'Access denied to this store'
204            ]));
205            return;
206        }
207
208        try {
209            $manager = new WhiteboardManager($this->store);
210            $result = $manager->clearCanvas(null);
211
212            if ($result) {
213                // Publish to Ably for real-time updates
214                try {
215                    $ably = new WorkbookAbly($typeNum);
216                    $ably->whiteboardClear($data['employeeId']);
217                } catch (Exception $e) {
218                    error_log("WhiteboardApiController::clearCanvas Ably error: " . $e->getMessage());
219                }
220
221                $this->app->response->headers->set('Content-Type', 'application/json');
222                $this->app->response->setBody(json_encode([
223                    'success' => true
224                ]));
225            } else {
226                $this->app->response->headers->set('Content-Type', 'application/json');
227                $this->app->halt(500, json_encode([
228                    'error' => 'Failed to clear canvas'
229                ]));
230            }
231        } catch (Exception $e) {
232            error_log("WhiteboardApiController::clearCanvas error: " . $e->getMessage());
233            $this->app->response->headers->set('Content-Type', 'application/json');
234            $this->app->halt(500, json_encode([
235                'error' => 'Failed to clear canvas'
236            ]));
237        }
238    }
239
240    /**
241     * GET /api/:typeNum/workbook/whiteboard/items/
242     * Get all non-expired overlay items
243     *
244     * @param string $typeNum Store identifier
245     */
246    public function getItems($typeNum)
247    {
248        try {
249            $manager = new WhiteboardManager($this->store);
250            $items = $manager->getItems();
251
252            $this->app->response->headers->set('Content-Type', 'application/json');
253            $this->app->response->setBody(json_encode([
254                'success' => true,
255                'items' => $items
256            ]));
257        } catch (Exception $e) {
258            error_log("WhiteboardApiController::getItems error: " . $e->getMessage());
259            $this->app->response->headers->set('Content-Type', 'application/json');
260            $this->app->halt(500, json_encode([
261                'error' => 'Failed to retrieve items'
262            ]));
263        }
264    }
265
266    /**
267     * POST /api/:typeNum/workbook/whiteboard/items/
268     * Add a new overlay item (sticky note)
269     *
270     * Request body: {
271     *   "employeeId": int,
272     *   "type": string (sticky, text, image),
273     *   "content": string,
274     *   "positionX": int,
275     *   "positionY": int,
276     *   "width": int,
277     *   "height": int,
278     *   "rotation": float,
279     *   "zIndex": int,
280     *   "backgroundColor": string,
281     *   "textColor": string,
282     *   "fontSize": int,
283     *   "expiresAt": datetime
284     * }
285     *
286     * @param string $typeNum Store identifier
287     */
288    public function addItem($typeNum)
289    {
290        // Check store access (any authenticated user in this store group)
291        if (!$this->app->user->checkStoreGroup($typeNum)) {
292            $this->app->response->headers->set('Content-Type', 'application/json');
293            $this->app->halt(403, json_encode([
294                'error' => 'Access denied to this store'
295            ]));
296            return;
297        }
298
299        try {
300            $data = json_decode($this->app->request->getBody(), true);
301
302            // employeeId is optional - sticky notes can be anonymous
303            // If provided, verify employee exists in unified users table
304            if (!empty($data['employeeId'])) {
305                $stmt = $this->centralDb->prepare("
306                    SELECT u.id
307                    FROM users u
308                    INNER JOIN userStoreAssignments usa ON u.id = usa.userId
309                    WHERE u.id = :employeeId
310                      AND usa.typeNum = :typeNum
311                      AND usa.isActive = 1
312                      AND u.enabled = 1
313                ");
314                $stmt->execute([
315                    ':employeeId' => (int)$data['employeeId'],
316                    ':typeNum' => $this->typeNum
317                ]);
318                if (!$stmt->fetch()) {
319                    // If employee not found, just set to null (anonymous)
320                    $data['employeeId'] = null;
321                }
322            } else {
323                $data['employeeId'] = null;
324            }
325
326            // Validate type if provided
327            if (isset($data['type'])) {
328                $validTypes = ['sticky', 'text', 'image'];
329                if (!in_array($data['type'], $validTypes)) {
330                    $this->app->response->headers->set('Content-Type', 'application/json');
331                    $this->app->halt(400, json_encode([
332                        'error' => 'Invalid type. Must be one of: sticky, text, image'
333                    ]));
334                    return;
335                }
336            }
337
338            $manager = new WhiteboardManager($this->store);
339            $itemId = $manager->addItem($data);
340
341            if ($itemId) {
342                // Get the created item
343                $item = $manager->getItem($itemId);
344
345                // Publish to Ably for real-time updates
346                try {
347                    $ably = new WorkbookAbly($typeNum);
348                    $ably->whiteboardStickyAdd($itemId, $item);
349                } catch (Exception $e) {
350                    error_log("WhiteboardApiController::addItem Ably error: " . $e->getMessage());
351                }
352
353                $this->app->response->setStatus(201);
354                $this->app->response->headers->set('Content-Type', 'application/json');
355                $this->app->response->setBody(json_encode([
356                    'success' => true,
357                    'item' => $item
358                ]));
359            } else {
360                $this->app->response->headers->set('Content-Type', 'application/json');
361                $this->app->halt(500, json_encode([
362                    'error' => 'Failed to add item'
363                ]));
364            }
365        } catch (Exception $e) {
366            error_log("WhiteboardApiController::addItem error: " . $e->getMessage());
367            $this->app->response->headers->set('Content-Type', 'application/json');
368            $this->app->halt(500, json_encode([
369                'error' => 'Failed to add item'
370            ]));
371        }
372    }
373
374    /**
375     * PUT /api/:typeNum/workbook/whiteboard/items/:itemId/
376     * Update an existing overlay item
377     *
378     * Request body: {
379     *   "content": string,
380     *   "positionX": int,
381     *   "positionY": int,
382     *   "width": int,
383     *   "height": int,
384     *   "rotation": float,
385     *   "zIndex": int,
386     *   "backgroundColor": string,
387     *   "textColor": string,
388     *   "fontSize": int
389     * }
390     *
391     * @param string $typeNum Store identifier
392     * @param int $itemId Item ID
393     */
394    public function updateItem($typeNum, $itemId)
395    {
396        try {
397            $data = json_decode($this->app->request->getBody(), true);
398
399            // Verify item exists
400            $manager = new WhiteboardManager($this->store);
401            $item = $manager->getItem($itemId);
402
403            if (!$item) {
404                $this->app->response->headers->set('Content-Type', 'application/json');
405                $this->app->halt(404, json_encode([
406                    'error' => 'Item not found'
407                ]));
408                return;
409            }
410
411            $result = $manager->updateItem($itemId, $data);
412
413            if ($result) {
414                // Get updated item
415                $updatedItem = $manager->getItem($itemId);
416
417                // Publish to Ably for real-time updates
418                try {
419                    $ably = new WorkbookAbly($typeNum);
420                    // Determine if this is a move or edit operation
421                    if ((isset($data['positionX']) || isset($data['positionY'])) && !isset($data['content'])) {
422                        $ably->whiteboardStickyMove(
423                            $itemId,
424                            $data['positionX'] ?? $item['positionX'],
425                            $data['positionY'] ?? $item['positionY']
426                        );
427                    } else {
428                        $ably->whiteboardStickyEdit($itemId, $updatedItem);
429                    }
430                } catch (Exception $e) {
431                    error_log("WhiteboardApiController::updateItem Ably error: " . $e->getMessage());
432                }
433
434                $this->app->response->headers->set('Content-Type', 'application/json');
435                $this->app->response->setBody(json_encode([
436                    'success' => true,
437                    'item' => $updatedItem
438                ]));
439            } else {
440                $this->app->response->headers->set('Content-Type', 'application/json');
441                $this->app->halt(500, json_encode([
442                    'error' => 'Failed to update item'
443                ]));
444            }
445        } catch (Exception $e) {
446            error_log("WhiteboardApiController::updateItem error: " . $e->getMessage());
447            $this->app->response->headers->set('Content-Type', 'application/json');
448            $this->app->halt(500, json_encode([
449                'error' => 'Failed to update item'
450            ]));
451        }
452    }
453
454    /**
455     * DELETE /api/:typeNum/workbook/whiteboard/items/:itemId/
456     * Delete an overlay item
457     *
458     * Request body: {
459     *   "employeeId": int
460     * }
461     *
462     * @param string $typeNum Store identifier
463     * @param int $itemId Item ID
464     */
465    public function deleteItem($typeNum, $itemId)
466    {
467        // Check store access (any authenticated user in this store group)
468        if (!$this->app->user->checkStoreGroup($typeNum)) {
469            $this->app->response->headers->set('Content-Type', 'application/json');
470            $this->app->halt(403, json_encode([
471                'error' => 'Access denied to this store'
472            ]));
473            return;
474        }
475
476        try {
477            // Verify item exists
478            $manager = new WhiteboardManager($this->store);
479            $item = $manager->getItem($itemId);
480
481            if (!$item) {
482                $this->app->response->headers->set('Content-Type', 'application/json');
483                $this->app->halt(404, json_encode([
484                    'error' => 'Item not found'
485                ]));
486                return;
487            }
488
489            $result = $manager->deleteItem($itemId, null);
490
491            if ($result) {
492                // Publish to Ably for real-time updates
493                try {
494                    $ably = new WorkbookAbly($typeNum);
495                    $ably->whiteboardStickyDelete($itemId);
496                } catch (Exception $e) {
497                    error_log("WhiteboardApiController::deleteItem Ably error: " . $e->getMessage());
498                }
499
500                $this->app->response->headers->set('Content-Type', 'application/json');
501                $this->app->response->setBody(json_encode([
502                    'success' => true
503                ]));
504            } else {
505                $this->app->response->headers->set('Content-Type', 'application/json');
506                $this->app->halt(500, json_encode([
507                    'error' => 'Failed to delete item'
508                ]));
509            }
510        } catch (Exception $e) {
511            error_log("WhiteboardApiController::deleteItem error: " . $e->getMessage());
512            $this->app->response->headers->set('Content-Type', 'application/json');
513            $this->app->halt(500, json_encode([
514                'error' => 'Failed to delete item'
515            ]));
516        }
517    }
518
519    /**
520     * GET /api/:typeNum/workbook/whiteboard/history/
521     * Get whiteboard action history for a specific date
522     *
523     * Query params:
524     *   - date: string (Y-m-d format, optional, defaults to today)
525     *
526     * Requires uri_store_settings permission
527     *
528     * @param string $typeNum Store identifier
529     */
530    public function getHistory($typeNum)
531    {
532        // Check permission
533        if (!$this->app->user->checkAccess('uri_store_settings')) {
534            $this->app->response->headers->set('Content-Type', 'application/json');
535            $this->app->halt(403, json_encode([
536                'error' => 'Access denied. Requires admin permission'
537            ]));
538            return;
539        }
540
541        try {
542            $date = $this->app->request->get('date');
543
544            // Validate date format if provided
545            if ($date) {
546                $dateObj = \DateTime::createFromFormat('Y-m-d', $date);
547                if (!$dateObj || $dateObj->format('Y-m-d') !== $date) {
548                    $this->app->response->headers->set('Content-Type', 'application/json');
549                    $this->app->halt(400, json_encode([
550                        'error' => 'Invalid date format. Use Y-m-d'
551                    ]));
552                    return;
553                }
554            }
555
556            $manager = new WhiteboardManager($this->store);
557            $history = $manager->getHistory($date);
558
559            $this->app->response->headers->set('Content-Type', 'application/json');
560            $this->app->response->setBody(json_encode([
561                'success' => true,
562                'history' => $history
563            ]));
564        } catch (Exception $e) {
565            error_log("WhiteboardApiController::getHistory error: " . $e->getMessage());
566            $this->app->response->headers->set('Content-Type', 'application/json');
567            $this->app->halt(500, json_encode([
568                'error' => 'Failed to retrieve history'
569            ]));
570        }
571    }
572}