Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 230
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
WhiteboardManager
0.00% covered (danger)
0.00%
0 / 230
0.00% covered (danger)
0.00%
0 / 12
1806
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
 getTodayCanvas
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 saveCanvas
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 clearCanvas
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getItems
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
12
 addItem
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 getItem
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 updateItem
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 deleteItem
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 cleanupExpired
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 logAction
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getHistory
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace BuyerKiosk\Workbook;
4
5use \Store;
6use \PDO;
7use \PDOException;
8
9/**
10 * WhiteboardManager - Main class for managing the workbook whiteboard system
11 *
12 * Handles canvas drawing operations and overlay items (sticky notes) for
13 * the collaborative whiteboard feature in the Workbook dashboard.
14 */
15class WhiteboardManager
16{
17    /**
18     * @var Store Store object
19     */
20    private $store;
21
22    /**
23     * @var PDO Database connection
24     */
25    private $db;
26
27    /**
28     * Constructor
29     *
30     * @param Store $store Store object with configuration
31     */
32    public function __construct(Store $store)
33    {
34        $this->store = $store;
35        $this->db = dbConnectByName($store->getDbName());
36    }
37
38    /**
39     * Get canvas for today
40     *
41     * Returns the canvas data for today's date based on store timezone.
42     * Includes canvas data (JSON), thumbnail, last modifier info.
43     *
44     * @return array|null Canvas data or null if no canvas exists for today
45     */
46    public function getTodayCanvas(): ?array
47    {
48        $timezone = new \DateTimeZone($this->store->timezone);
49        $now = new \DateTime('now', $timezone);
50        $today = $now->format('Y-m-d');
51
52        try {
53            $sql = "
54                SELECT
55                    c.canvasData,
56                    c.thumbnail,
57                    c.lastModifiedBy,
58                    c.lastModifiedAt,
59                    e.employeeFirstName,
60                    e.employeeLastName
61                FROM workbook_whiteboard_canvas c
62                LEFT JOIN employees e ON c.lastModifiedBy = e.employeeID
63                WHERE c.date = :today
64            ";
65
66            $stmt = $this->db->prepare($sql);
67            $stmt->execute([':today' => $today]);
68
69            $row = $stmt->fetch(PDO::FETCH_ASSOC);
70            if (!$row) {
71                return null;
72            }
73
74            return [
75                'canvasData' => json_decode($row['canvasData'], true),
76                'thumbnail' => $row['thumbnail'],
77                'lastModifiedBy' => $row['lastModifiedBy'],
78                'lastModifiedAt' => $row['lastModifiedAt'],
79                'lastModifierFirstName' => $row['employeeFirstName'],
80                'lastModifierLastName' => $row['employeeLastName']
81            ];
82        } catch (PDOException $e) {
83            error_log("WhiteboardManager::getTodayCanvas error: " . $e->getMessage());
84            return null;
85        }
86    }
87
88    /**
89     * Save canvas for today
90     *
91     * Uses INSERT ... ON DUPLICATE KEY UPDATE to save or update today's canvas.
92     * Logs the save action to history.
93     *
94     * @param string $canvasJson Canvas data as JSON string
95     * @param string|null $thumbnail Thumbnail data (base64 encoded image)
96     * @param int|null $employeeId Employee ID who is saving (null for anonymous)
97     * @return bool Success status
98     */
99    public function saveCanvas(string $canvasJson, ?string $thumbnail, ?int $employeeId): bool
100    {
101        $timezone = new \DateTimeZone($this->store->timezone);
102        $now = new \DateTime('now', $timezone);
103        $today = $now->format('Y-m-d');
104
105        try {
106            // Validate JSON
107            $decoded = json_decode($canvasJson, true);
108            if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
109                error_log("WhiteboardManager::saveCanvas error: Invalid JSON");
110                return false;
111            }
112
113            $sql = "
114                INSERT INTO workbook_whiteboard_canvas
115                    (date, canvasData, thumbnail, lastModifiedBy, lastModifiedAt)
116                VALUES
117                    (:date, :canvasData, :thumbnail, :employeeId, NOW())
118                ON DUPLICATE KEY UPDATE
119                    canvasData = VALUES(canvasData),
120                    thumbnail = VALUES(thumbnail),
121                    lastModifiedBy = VALUES(lastModifiedBy),
122                    lastModifiedAt = NOW()
123            ";
124
125            $stmt = $this->db->prepare($sql);
126            $result = $stmt->execute([
127                ':date' => $today,
128                ':canvasData' => $canvasJson,
129                ':thumbnail' => $thumbnail,
130                ':employeeId' => $employeeId
131            ]);
132
133            if ($result && $employeeId) {
134                // Log the action only if we have an employee
135                $this->logAction('save', $employeeId, [
136                    'date' => $today,
137                    'hasThumbnail' => $thumbnail !== null
138                ]);
139            }
140
141            return $result;
142        } catch (PDOException $e) {
143            error_log("WhiteboardManager::saveCanvas error: " . $e->getMessage());
144            return false;
145        }
146    }
147
148    /**
149     * Clear canvas for today
150     *
151     * Clears the canvas by saving an empty canvas object.
152     * Logs the clear action to history.
153     *
154     * @param int|null $employeeId Employee ID who is clearing (null for anonymous)
155     * @return bool Success status
156     */
157    public function clearCanvas(?int $employeeId): bool
158    {
159        $timezone = new \DateTimeZone($this->store->timezone);
160        $now = new \DateTime('now', $timezone);
161        $today = $now->format('Y-m-d');
162
163        try {
164            // Log the action before clearing (only if we have an employee)
165            if ($employeeId) {
166                $this->logAction('clear', $employeeId, ['date' => $today]);
167            }
168
169            // Save empty canvas
170            return $this->saveCanvas('{"objects":[]}', null, $employeeId);
171        } catch (\Exception $e) {
172            error_log("WhiteboardManager::clearCanvas error: " . $e->getMessage());
173            return false;
174        }
175    }
176
177    /**
178     * Get all active overlay items (sticky notes)
179     *
180     * Returns all non-expired items ordered by zIndex.
181     * Includes creator information from employees table.
182     *
183     * @return array Array of item data
184     */
185    public function getItems(): array
186    {
187        try {
188            $sql = "
189                SELECT
190                    i.*,
191                    e.employeeFirstName as creatorFirstName,
192                    e.employeeLastName as creatorLastName
193                FROM workbook_whiteboard_items i
194                LEFT JOIN employees e ON i.employeeId = e.employeeID
195                WHERE i.expiresAt IS NULL OR i.expiresAt > NOW()
196                ORDER BY i.zIndex ASC, i.createdAt ASC
197            ";
198
199            $stmt = $this->db->prepare($sql);
200            $stmt->execute();
201
202            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
203            $items = [];
204
205            foreach ($rows as $row) {
206                $items[] = [
207                    'id' => (int) $row['id'],
208                    'employeeId' => (int) $row['employeeId'],
209                    'type' => $row['type'],
210                    'content' => $row['content'],
211                    'positionX' => (float) $row['positionX'],
212                    'positionY' => (float) $row['positionY'],
213                    'width' => (int) $row['width'],
214                    'height' => (int) $row['height'],
215                    'rotation' => (float) $row['rotation'],
216                    'zIndex' => (int) $row['zIndex'],
217                    'backgroundColor' => $row['backgroundColor'],
218                    'textColor' => $row['textColor'],
219                    'fontSize' => (int) $row['fontSize'],
220                    'expiresAt' => $row['expiresAt'],
221                    'createdAt' => $row['createdAt'],
222                    'updatedAt' => $row['updatedAt'],
223                    'creatorFirstName' => $row['creatorFirstName'],
224                    'creatorLastName' => $row['creatorLastName']
225                ];
226            }
227
228            return $items;
229        } catch (PDOException $e) {
230            error_log("WhiteboardManager::getItems error: " . $e->getMessage());
231            return [];
232        }
233    }
234
235    /**
236     * Add a new overlay item (sticky note)
237     *
238     * Creates a new sticky note or other overlay item on the whiteboard.
239     * Logs the action to history.
240     *
241     * @param array $data Item data (employeeId, type, content, position, styling, etc.)
242     * @return int|null The ID of the created item, or null on failure
243     */
244    public function addItem(array $data): ?int
245    {
246        try {
247            $sql = "
248                INSERT INTO workbook_whiteboard_items
249                    (employeeId, type, content, positionX, positionY, width, height,
250                     rotation, zIndex, backgroundColor, textColor, fontSize, expiresAt)
251                VALUES
252                    (:employeeId, :type, :content, :positionX, :positionY, :width, :height,
253                     :rotation, :zIndex, :backgroundColor, :textColor, :fontSize, :expiresAt)
254            ";
255
256            $stmt = $this->db->prepare($sql);
257            $result = $stmt->execute([
258                ':employeeId' => $data['employeeId'],
259                ':type' => $data['type'] ?? 'sticky',
260                ':content' => $data['content'] ?? '',
261                ':positionX' => $data['positionX'] ?? 0,
262                ':positionY' => $data['positionY'] ?? 0,
263                ':width' => $data['width'] ?? 200,
264                ':height' => $data['height'] ?? 150,
265                ':rotation' => $data['rotation'] ?? 0,
266                ':zIndex' => $data['zIndex'] ?? 0,
267                ':backgroundColor' => $data['backgroundColor'] ?? '#FFFF88',
268                ':textColor' => $data['textColor'] ?? '#000000',
269                ':fontSize' => $data['fontSize'] ?? 14,
270                ':expiresAt' => $data['expiresAt'] ?? null
271            ]);
272
273            if ($result) {
274                $itemId = (int) $this->db->lastInsertId();
275
276                // Log the action (only if we have an employee)
277                if ($data['employeeId']) {
278                    $this->logAction('add_item', $data['employeeId'], [
279                        'itemId' => $itemId,
280                        'type' => $data['type'] ?? 'sticky'
281                    ]);
282                }
283
284                return $itemId;
285            }
286
287            return null;
288        } catch (PDOException $e) {
289            error_log("WhiteboardManager::addItem error: " . $e->getMessage());
290            return null;
291        }
292    }
293
294    /**
295     * Get a single item by ID
296     *
297     * Returns item data with creator information.
298     *
299     * @param int $itemId Item ID
300     * @return array|null Item data or null if not found
301     */
302    public function getItem(int $itemId): ?array
303    {
304        try {
305            $sql = "
306                SELECT
307                    i.*,
308                    e.employeeFirstName as creatorFirstName,
309                    e.employeeLastName as creatorLastName
310                FROM workbook_whiteboard_items i
311                LEFT JOIN employees e ON i.employeeId = e.employeeID
312                WHERE i.id = :itemId
313            ";
314
315            $stmt = $this->db->prepare($sql);
316            $stmt->execute([':itemId' => $itemId]);
317
318            $row = $stmt->fetch(PDO::FETCH_ASSOC);
319            if (!$row) {
320                return null;
321            }
322
323            return [
324                'id' => (int) $row['id'],
325                'employeeId' => (int) $row['employeeId'],
326                'type' => $row['type'],
327                'content' => $row['content'],
328                'positionX' => (float) $row['positionX'],
329                'positionY' => (float) $row['positionY'],
330                'width' => (int) $row['width'],
331                'height' => (int) $row['height'],
332                'rotation' => (float) $row['rotation'],
333                'zIndex' => (int) $row['zIndex'],
334                'backgroundColor' => $row['backgroundColor'],
335                'textColor' => $row['textColor'],
336                'fontSize' => (int) $row['fontSize'],
337                'expiresAt' => $row['expiresAt'],
338                'createdAt' => $row['createdAt'],
339                'updatedAt' => $row['updatedAt'],
340                'creatorFirstName' => $row['creatorFirstName'],
341                'creatorLastName' => $row['creatorLastName']
342            ];
343        } catch (PDOException $e) {
344            error_log("WhiteboardManager::getItem error: " . $e->getMessage());
345            return null;
346        }
347    }
348
349    /**
350     * Update an existing item
351     *
352     * Updates only the specified fields (whitelisted for security).
353     * Automatically updates the updatedAt timestamp.
354     *
355     * @param int $itemId Item ID
356     * @param array $data Fields to update (content, position, styling, etc.)
357     * @return bool Success status
358     */
359    public function updateItem(int $itemId, array $data): bool
360    {
361        try {
362            // Whitelist of allowed update fields
363            $allowedFields = [
364                'content', 'positionX', 'positionY', 'width', 'height',
365                'rotation', 'zIndex', 'backgroundColor', 'textColor', 'fontSize'
366            ];
367
368            $updateFields = [];
369            $params = [':itemId' => $itemId];
370
371            foreach ($allowedFields as $field) {
372                if (array_key_exists($field, $data)) {
373                    $updateFields[] = "$field = :$field";
374                    $params[":$field"] = $data[$field];
375                }
376            }
377
378            if (empty($updateFields)) {
379                // No valid fields to update
380                return false;
381            }
382
383            // Always update the updatedAt timestamp
384            $updateFields[] = "updatedAt = NOW()";
385
386            $sql = "
387                UPDATE workbook_whiteboard_items
388                SET " . implode(', ', $updateFields) . "
389                WHERE id = :itemId
390            ";
391
392            $stmt = $this->db->prepare($sql);
393            return $stmt->execute($params);
394        } catch (PDOException $e) {
395            error_log("WhiteboardManager::updateItem error: " . $e->getMessage());
396            return false;
397        }
398    }
399
400    /**
401     * Delete an item
402     *
403     * Removes an item from the whiteboard and logs the action.
404     *
405     * @param int $itemId Item ID to delete
406     * @param int $employeeId Employee ID who is deleting
407     * @return bool Success status
408     */
409    public function deleteItem(int $itemId, ?int $employeeId): bool
410    {
411        try {
412            // Get item info before deleting for logging
413            $item = $this->getItem($itemId);
414            if (!$item) {
415                return false;
416            }
417
418            // Log the action before deleting (only if we have an employee)
419            if ($employeeId) {
420                $this->logAction('delete_item', $employeeId, [
421                    'itemId' => $itemId,
422                    'type' => $item['type']
423                ]);
424            }
425
426            $sql = "DELETE FROM workbook_whiteboard_items WHERE id = :itemId";
427            $stmt = $this->db->prepare($sql);
428            return $stmt->execute([':itemId' => $itemId]);
429        } catch (PDOException $e) {
430            error_log("WhiteboardManager::deleteItem error: " . $e->getMessage());
431            return false;
432        }
433    }
434
435    /**
436     * Clean up expired items
437     *
438     * Deletes items that have passed their expiration date.
439     * Should be called periodically (e.g., daily cron job).
440     *
441     * @return int Number of items deleted
442     */
443    public function cleanupExpired(): int
444    {
445        try {
446            $sql = "
447                DELETE FROM workbook_whiteboard_items
448                WHERE expiresAt IS NOT NULL AND expiresAt < NOW()
449            ";
450
451            $stmt = $this->db->prepare($sql);
452            $stmt->execute();
453
454            return $stmt->rowCount();
455        } catch (PDOException $e) {
456            error_log("WhiteboardManager::cleanupExpired error: " . $e->getMessage());
457            return 0;
458        }
459    }
460
461    /**
462     * Log a whiteboard action to history
463     *
464     * Records actions for audit trail and activity tracking.
465     * Actions include: save, clear, add_item, update_item, delete_item.
466     *
467     * @param string $action Action type
468     * @param int $employeeId Employee ID who performed the action
469     * @param array|null $data Additional action data (optional)
470     * @return void
471     */
472    public function logAction(string $action, int $employeeId, ?array $data = null): void
473    {
474        $timezone = new \DateTimeZone($this->store->timezone);
475        $now = new \DateTime('now', $timezone);
476        $today = $now->format('Y-m-d');
477
478        try {
479            $sql = "
480                INSERT INTO workbook_whiteboard_history
481                    (date, employeeId, action, actionData)
482                VALUES
483                    (:date, :employeeId, :action, :actionData)
484            ";
485
486            $stmt = $this->db->prepare($sql);
487            $stmt->execute([
488                ':date' => $today,
489                ':employeeId' => $employeeId,
490                ':action' => $action,
491                ':actionData' => $data ? json_encode($data) : null
492            ]);
493        } catch (PDOException $e) {
494            error_log("WhiteboardManager::logAction error: " . $e->getMessage());
495            // Don't throw - logging failures shouldn't break the main operation
496        }
497    }
498
499    /**
500     * Get history for a specific date
501     *
502     * Returns all actions performed on the whiteboard for the given date.
503     * Defaults to today if no date provided.
504     *
505     * @param string|null $date Date in Y-m-d format (defaults to today)
506     * @return array Array of history entries
507     */
508    public function getHistory(?string $date = null): array
509    {
510        if ($date === null) {
511            $timezone = new \DateTimeZone($this->store->timezone);
512            $now = new \DateTime('now', $timezone);
513            $date = $now->format('Y-m-d');
514        }
515
516        try {
517            $sql = "
518                SELECT
519                    h.*,
520                    e.employeeFirstName,
521                    e.employeeLastName
522                FROM workbook_whiteboard_history h
523                INNER JOIN employees e ON h.employeeId = e.employeeID
524                WHERE h.date = :date
525                ORDER BY h.createdAt DESC
526            ";
527
528            $stmt = $this->db->prepare($sql);
529            $stmt->execute([':date' => $date]);
530
531            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
532            $history = [];
533
534            foreach ($rows as $row) {
535                $history[] = [
536                    'id' => (int) $row['id'],
537                    'date' => $row['date'],
538                    'employeeId' => (int) $row['employeeId'],
539                    'action' => $row['action'],
540                    'actionData' => $row['actionData'] ? json_decode($row['actionData'], true) : null,
541                    'createdAt' => $row['createdAt'],
542                    'employeeFirstName' => $row['employeeFirstName'],
543                    'employeeLastName' => $row['employeeLastName']
544                ];
545            }
546
547            return $history;
548        } catch (PDOException $e) {
549            error_log("WhiteboardManager::getHistory error: " . $e->getMessage());
550            return [];
551        }
552    }
553}