Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 187
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
NoteManager
0.00% covered (danger)
0.00%
0 / 187
0.00% covered (danger)
0.00%
0 / 11
2450
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
 getTodayNotes
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 getById
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 create
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getNotes
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
72
 getTotalCount
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 hydrate
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 getReactionCounts
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getReactionCountsBatch
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 getReactionsByEmployees
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getReactionDetailsBatch
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace BuyerKiosk\Workbook;
4
5use \Store;
6use \PDO;
7use \PDOException;
8
9/**
10 * NoteManager - Main class for managing workbook notes
11 *
12 * Handles note retrieval with visibility filtering, reaction counts,
13 * comment counts, and user reaction status for the Workbook dashboard.
14 */
15class NoteManager
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 notes visible for today
40     *
41     * Filters notes based on:
42     * - startDate <= today <= endDate (or endDate is NULL)
43     * - isManagerOnly flag (if includeManagerOnly is false)
44     * - Includes author info, reaction count, comment count
45     * - Includes whether current employee has reacted
46     *
47     * @param bool $includeManagerOnly Whether to include manager-only notes
48     * @param int|null $currentEmployeeId Current employee ID for reaction status
49     * @return array Array of Note objects
50     */
51    public function getTodayNotes(bool $includeManagerOnly = false, ?int $currentEmployeeId = null): array
52    {
53        $timezone = new \DateTimeZone($this->store->timezone);
54        $now = new \DateTime('now', $timezone);
55        $today = $now->format('Y-m-d');
56
57        try {
58            $sql = "
59                SELECT
60                    dn.*,
61                    e.employeeFirstName as authorFirstName,
62                    e.employeeLastName as authorLastName,
63                    COALESCE(rc.reactionCount, 0) as reactionCount,
64                    COALESCE(cc.commentCount, 0) as commentCount,
65                    CASE WHEN ur.id IS NOT NULL THEN 1 ELSE 0 END as userHasReacted
66                FROM workbook_notes dn
67                LEFT JOIN employees e ON dn.authorEmployeeId = e.employeeID
68                LEFT JOIN (
69                    SELECT noteId, COUNT(*) as reactionCount
70                    FROM workbook_note_reactions
71                    GROUP BY noteId
72                ) rc ON dn.id = rc.noteId
73                LEFT JOIN (
74                    SELECT noteId, COUNT(*) as commentCount
75                    FROM workbook_note_comments
76                    GROUP BY noteId
77                ) cc ON dn.id = cc.noteId
78            ";
79
80            // Left join for user reaction status
81            if ($currentEmployeeId !== null) {
82                $sql .= "
83                    LEFT JOIN workbook_note_reactions ur
84                        ON dn.id = ur.noteId AND ur.employeeId = :currentEmployeeId
85                ";
86            } else {
87                $sql .= "
88                    LEFT JOIN (SELECT NULL as id, NULL as noteId) ur ON 1=0
89                ";
90            }
91
92            $sql .= "
93                WHERE dn.deletedAt IS NULL
94                  AND dn.startDate <= :today
95                  AND (dn.endDate IS NULL OR dn.endDate >= :today)
96            ";
97
98            $params = [':today' => $today];
99
100            if ($currentEmployeeId !== null) {
101                $params[':currentEmployeeId'] = $currentEmployeeId;
102            }
103
104            if (!$includeManagerOnly) {
105                $sql .= " AND dn.isManagerOnly = 0";
106            }
107
108            $sql .= " ORDER BY dn.isPinned DESC, dn.createdAt DESC";
109
110            $stmt = $this->db->prepare($sql);
111            $stmt->execute($params);
112
113            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
114            $notes = [];
115
116            foreach ($rows as $row) {
117                $notes[] = $this->hydrate($row);
118            }
119
120            return $notes;
121        } catch (PDOException $e) {
122            error_log("NoteManager::getTodayNotes error: " . $e->getMessage());
123            return [];
124        }
125    }
126
127    /**
128     * Get a single note by ID with full details
129     *
130     * Includes author info, reaction count, comment count, and user reaction status
131     *
132     * @param int $id Note ID
133     * @param int|null $currentEmployeeId Current employee ID for reaction status
134     * @return Note|null Note object or null if not found
135     */
136    public function getById(int $id, ?int $currentEmployeeId = null): ?Note
137    {
138        try {
139            $sql = "
140                SELECT
141                    dn.*,
142                    e.employeeFirstName as authorFirstName,
143                    e.employeeLastName as authorLastName,
144                    COALESCE(rc.reactionCount, 0) as reactionCount,
145                    COALESCE(cc.commentCount, 0) as commentCount,
146                    CASE WHEN ur.id IS NOT NULL THEN 1 ELSE 0 END as userHasReacted
147                FROM workbook_notes dn
148                INNER JOIN employees e ON dn.authorEmployeeId = e.employeeID
149                LEFT JOIN (
150                    SELECT noteId, COUNT(*) as reactionCount
151                    FROM workbook_note_reactions
152                    GROUP BY noteId
153                ) rc ON dn.id = rc.noteId
154                LEFT JOIN (
155                    SELECT noteId, COUNT(*) as commentCount
156                    FROM workbook_note_comments
157                    GROUP BY noteId
158                ) cc ON dn.id = cc.noteId
159            ";
160
161            if ($currentEmployeeId !== null) {
162                $sql .= "
163                    LEFT JOIN workbook_note_reactions ur
164                        ON dn.id = ur.noteId AND ur.employeeId = :currentEmployeeId
165                ";
166            } else {
167                $sql .= "
168                    LEFT JOIN (SELECT NULL as id, NULL as noteId) ur ON 1=0
169                ";
170            }
171
172            $sql .= " WHERE dn.id = :id";
173
174            $stmt = $this->db->prepare($sql);
175            $params = [':id' => $id];
176
177            if ($currentEmployeeId !== null) {
178                $params[':currentEmployeeId'] = $currentEmployeeId;
179            }
180
181            $stmt->execute($params);
182
183            $row = $stmt->fetch(PDO::FETCH_ASSOC);
184            if (!$row) {
185                return null;
186            }
187
188            return $this->hydrate($row);
189        } catch (PDOException $e) {
190            error_log("NoteManager::getById error: " . $e->getMessage());
191            return null;
192        }
193    }
194
195    /**
196     * Create a new note
197     *
198     * @param array $data Note data (authorEmployeeId, title, content, etc.)
199     * @return int|null The ID of the created note, or null on failure
200     */
201    public function create(array $data): ?int
202    {
203        try {
204            $note = Note::fromArray($data);
205
206            if ($note->save($this->db)) {
207                return $note->id;
208            }
209
210            return null;
211        } catch (\Exception $e) {
212            error_log("NoteManager::create error: " . $e->getMessage());
213            return null;
214        }
215    }
216
217    /**
218     * Get paginated notes sorted by creation date (newest first)
219     *
220     * Returns all notes (not filtered by date visibility) for the infinite scroll feed.
221     * Pinned notes are always shown first.
222     *
223     * @param int $limit Number of notes to return
224     * @param int $offset Starting offset for pagination
225     * @param bool $includeManagerOnly Whether to include manager-only notes
226     * @param int|null $currentEmployeeId Current employee ID for reaction status
227     * @return array Array with 'notes' and 'hasMore' keys
228     */
229    public function getNotes(int $limit = 5, int $offset = 0, bool $includeManagerOnly = false, ?int $currentEmployeeId = null): array
230    {
231        try {
232            $sql = "
233                SELECT
234                    dn.*,
235                    e.employeeFirstName as authorFirstName,
236                    e.employeeLastName as authorLastName,
237                    COALESCE(rc.reactionCount, 0) as reactionCount,
238                    COALESCE(cc.commentCount, 0) as commentCount,
239                    CASE WHEN ur.id IS NOT NULL THEN 1 ELSE 0 END as userHasReacted
240                FROM workbook_notes dn
241                LEFT JOIN employees e ON dn.authorEmployeeId = e.employeeID
242                LEFT JOIN (
243                    SELECT noteId, COUNT(*) as reactionCount
244                    FROM workbook_note_reactions
245                    GROUP BY noteId
246                ) rc ON dn.id = rc.noteId
247                LEFT JOIN (
248                    SELECT noteId, COUNT(*) as commentCount
249                    FROM workbook_note_comments
250                    GROUP BY noteId
251                ) cc ON dn.id = cc.noteId
252            ";
253
254            // Left join for user reaction status
255            if ($currentEmployeeId !== null) {
256                $sql .= "
257                    LEFT JOIN workbook_note_reactions ur
258                        ON dn.id = ur.noteId AND ur.employeeId = :currentEmployeeId
259                ";
260            } else {
261                $sql .= "
262                    LEFT JOIN (SELECT NULL as id, NULL as noteId) ur ON 1=0
263                ";
264            }
265
266            $sql .= " WHERE dn.deletedAt IS NULL";
267
268            $params = [];
269
270            if ($currentEmployeeId !== null) {
271                $params[':currentEmployeeId'] = $currentEmployeeId;
272            }
273
274            if (!$includeManagerOnly) {
275                $sql .= " AND dn.isManagerOnly = 0";
276            }
277
278            // Order by pinned first, then by creation date descending
279            $sql .= " ORDER BY dn.isPinned DESC, dn.createdAt DESC";
280
281            // Add limit + 1 to check if there are more
282            $sql .= " LIMIT :limit OFFSET :offset";
283
284            $stmt = $this->db->prepare($sql);
285
286            foreach ($params as $key => $value) {
287                $stmt->bindValue($key, $value);
288            }
289            $stmt->bindValue(':limit', $limit + 1, PDO::PARAM_INT);
290            $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
291
292            $stmt->execute();
293
294            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
295
296            // Check if there are more results
297            $hasMore = count($rows) > $limit;
298            if ($hasMore) {
299                array_pop($rows); // Remove the extra row
300            }
301
302            $notes = [];
303            foreach ($rows as $row) {
304                $notes[] = $this->hydrate($row);
305            }
306
307            return [
308                'notes' => $notes,
309                'hasMore' => $hasMore
310            ];
311        } catch (PDOException $e) {
312            error_log("NoteManager::getNotes error: " . $e->getMessage());
313            return ['notes' => [], 'hasMore' => false];
314        }
315    }
316
317    /**
318     * Get total count of notes
319     *
320     * @param bool $includeManagerOnly Whether to include manager-only notes
321     * @return int Total count
322     */
323    public function getTotalCount(bool $includeManagerOnly = false): int
324    {
325        try {
326            $sql = "SELECT COUNT(*) FROM workbook_notes";
327
328            if (!$includeManagerOnly) {
329                $sql .= " WHERE isManagerOnly = 0";
330            }
331
332            $stmt = $this->db->query($sql);
333            return (int) $stmt->fetchColumn();
334        } catch (PDOException $e) {
335            error_log("NoteManager::getTotalCount error: " . $e->getMessage());
336            return 0;
337        }
338    }
339
340    /**
341     * Hydrate a Note object from a database row
342     *
343     * Converts database row array to Note object with all joined fields
344     *
345     * @param array $row Database row from query
346     * @return Note
347     */
348    public function hydrate(array $row): Note
349    {
350        $note = new Note();
351        $note->id = (int) $row['id'];
352        $note->authorEmployeeId = isset($row['authorEmployeeId']) ? (int) $row['authorEmployeeId'] : null;
353        $note->authorName = $row['authorName'] ?? null;
354        $note->title = $row['title'];
355        $note->content = $row['content'];
356        $note->contentHtml = $row['contentHtml'];
357        $note->startDate = $row['startDate'];
358        $note->endDate = $row['endDate'];
359        $note->isManagerOnly = (bool) $row['isManagerOnly'];
360        $note->isPinned = (bool) $row['isPinned'];
361        $note->createdAt = $row['createdAt'];
362        $note->updatedAt = $row['updatedAt'];
363
364        // Joined fields
365        $note->authorFirstName = $row['authorFirstName'] ?? null;
366        $note->authorLastName = $row['authorLastName'] ?? null;
367        $note->reactionCount = isset($row['reactionCount']) ? (int) $row['reactionCount'] : 0;
368        $note->commentCount = isset($row['commentCount']) ? (int) $row['commentCount'] : 0;
369        $note->userHasReacted = isset($row['userHasReacted']) ? (bool) $row['userHasReacted'] : false;
370
371        return $note;
372    }
373
374    /**
375     * Get reaction counts grouped by type for a note
376     *
377     * @param int $noteId Note ID
378     * @return array Associative array with reaction types as keys and counts as values
379     */
380    public function getReactionCounts(int $noteId): array
381    {
382        try {
383            $stmt = $this->db->prepare("
384                SELECT reactionType, COUNT(*) as count
385                FROM workbook_note_reactions
386                WHERE noteId = :noteId
387                GROUP BY reactionType
388            ");
389            $stmt->execute([':noteId' => $noteId]);
390
391            $counts = ['like' => 0, 'heart' => 0];
392            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
393                $counts[$row['reactionType']] = (int) $row['count'];
394            }
395
396            return $counts;
397        } catch (PDOException $e) {
398            error_log("NoteManager::getReactionCounts error: " . $e->getMessage());
399            return ['like' => 0, 'heart' => 0];
400        }
401    }
402
403    /**
404     * Get reaction counts for multiple notes at once (batch)
405     *
406     * @param array $noteIds Array of note IDs
407     * @return array Associative array with note IDs as keys, each containing reaction counts
408     */
409    public function getReactionCountsBatch(array $noteIds): array
410    {
411        if (empty($noteIds)) {
412            return [];
413        }
414
415        try {
416            $placeholders = implode(',', array_fill(0, count($noteIds), '?'));
417            $stmt = $this->db->prepare("
418                SELECT noteId, reactionType, COUNT(*) as count
419                FROM workbook_note_reactions
420                WHERE noteId IN ($placeholders)
421                GROUP BY noteId, reactionType
422            ");
423            $stmt->execute($noteIds);
424
425            // Initialize all notes with default counts
426            $result = [];
427            foreach ($noteIds as $id) {
428                $result[$id] = ['like' => 0, 'heart' => 0];
429            }
430
431            // Populate actual counts
432            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
433                $noteId = (int) $row['noteId'];
434                $result[$noteId][$row['reactionType']] = (int) $row['count'];
435            }
436
437            return $result;
438        } catch (PDOException $e) {
439            error_log("NoteManager::getReactionCountsBatch error: " . $e->getMessage());
440            return [];
441        }
442    }
443
444    /**
445     * Get list of employees who reacted to a note, grouped by reaction type
446     *
447     * @param int $noteId Note ID
448     * @return array Associative array with reaction types as keys, each containing array of employee names
449     */
450    public function getReactionsByEmployees(int $noteId): array
451    {
452        try {
453            $stmt = $this->db->prepare("
454                SELECT
455                    dnr.reactionType,
456                    dnr.employeeId,
457                    e.employeeFirstName,
458                    e.employeeLastName
459                FROM workbook_note_reactions dnr
460                LEFT JOIN employees e ON dnr.employeeId = e.employeeID
461                WHERE dnr.noteId = :noteId
462                ORDER BY dnr.createdAt DESC
463            ");
464            $stmt->execute([':noteId' => $noteId]);
465
466            $result = ['like' => [], 'heart' => []];
467
468            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
469                $name = trim(($row['employeeFirstName'] ?? '') . ' ' . ($row['employeeLastName'] ?? ''));
470                if (empty($name)) {
471                    $name = 'Anonymous';
472                }
473                $result[$row['reactionType']][] = [
474                    'employeeId' => (int) $row['employeeId'],
475                    'name' => $name
476                ];
477            }
478
479            return $result;
480        } catch (PDOException $e) {
481            error_log("NoteManager::getReactionsByEmployees error: " . $e->getMessage());
482            return ['like' => [], 'heart' => []];
483        }
484    }
485
486    /**
487     * Get reaction details for multiple notes at once (batch) - includes employee names
488     *
489     * @param array $noteIds Array of note IDs
490     * @return array Associative array with note IDs as keys, each containing reactions by type with employee names
491     */
492    public function getReactionDetailsBatch(array $noteIds): array
493    {
494        if (empty($noteIds)) {
495            return [];
496        }
497
498        try {
499            $placeholders = implode(',', array_fill(0, count($noteIds), '?'));
500            $stmt = $this->db->prepare("
501                SELECT
502                    dnr.noteId,
503                    dnr.reactionType,
504                    dnr.employeeId,
505                    e.employeeFirstName,
506                    e.employeeLastName
507                FROM workbook_note_reactions dnr
508                LEFT JOIN employees e ON dnr.employeeId = e.employeeID
509                WHERE dnr.noteId IN ($placeholders)
510                ORDER BY dnr.createdAt DESC
511            ");
512            $stmt->execute($noteIds);
513
514            // Initialize all notes with empty arrays
515            $result = [];
516            foreach ($noteIds as $id) {
517                $result[$id] = [
518                    'like' => ['count' => 0, 'names' => []],
519                    'heart' => ['count' => 0, 'names' => []]
520                ];
521            }
522
523            // Populate reactions
524            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
525                $noteId = (int) $row['noteId'];
526                $type = $row['reactionType'];
527                $name = trim(($row['employeeFirstName'] ?? '') . ' ' . ($row['employeeLastName'] ?? ''));
528                if (empty($name)) {
529                    $name = 'Anonymous';
530                }
531
532                $result[$noteId][$type]['count']++;
533                $result[$noteId][$type]['names'][] = $name;
534            }
535
536            return $result;
537        } catch (PDOException $e) {
538            error_log("NoteManager::getReactionDetailsBatch error: " . $e->getMessage());
539            return [];
540        }
541    }
542}