Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.80% covered (danger)
2.80%
3 / 107
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Note
2.80% covered (danger)
2.80%
3 / 107
0.00% covered (danger)
0.00%
0 / 4
1226.02
0.00% covered (danger)
0.00%
0 / 1
 save
7.32% covered (danger)
7.32%
3 / 41
0.00% covered (danger)
0.00%
0 / 1
170.05
 delete
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 toArray
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 fromArray
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2
3namespace BuyerKiosk\Workbook;
4
5use \PDO;
6use \PDOException;
7
8/**
9 * Note - Model for workbook notes and announcements
10 *
11 * Manages notes with time-based visibility, manager-only flags, pinning,
12 * and includes aggregated reaction/comment counts.
13 */
14class Note
15{
16    /**
17     * @var int|null Note ID
18     */
19    public $id;
20
21    /**
22     * @var int|null Employee ID of note author (can be null if authorName is provided)
23     */
24    public $authorEmployeeId;
25
26    /**
27     * @var string|null Author name (used when employee ID is not available)
28     */
29    public $authorName;
30
31    /**
32     * @var string|null Note title
33     */
34    public $title;
35
36    /**
37     * @var string Note content (plain text or markdown)
38     */
39    public $content;
40
41    /**
42     * @var string|null Rich text HTML content
43     */
44    public $contentHtml;
45
46    /**
47     * @var string Start date (Y-m-d format)
48     */
49    public $startDate;
50
51    /**
52     * @var string|null End date (Y-m-d format, NULL = indefinite)
53     */
54    public $endDate;
55
56    /**
57     * @var bool Manager-only visibility flag
58     */
59    public $isManagerOnly;
60
61    /**
62     * @var bool Pinned flag
63     */
64    public $isPinned;
65
66    /**
67     * @var string Created timestamp
68     */
69    public $createdAt;
70
71    /**
72     * @var string|null Updated timestamp
73     */
74    public $updatedAt;
75
76    /**
77     * @var string|null Soft delete timestamp
78     */
79    public $deletedAt;
80
81    /**
82     * @var string|null Author first name (joined from employees)
83     */
84    public $authorFirstName;
85
86    /**
87     * @var string|null Author last name (joined from employees)
88     */
89    public $authorLastName;
90
91    /**
92     * @var int Reaction count (aggregated)
93     */
94    public $reactionCount = 0;
95
96    /**
97     * @var int Comment count (aggregated)
98     */
99    public $commentCount = 0;
100
101    /**
102     * @var bool Whether current user has reacted to this note
103     */
104    public $userHasReacted = false;
105
106    /**
107     * Save note (insert or update)
108     *
109     * Uses INSERT or UPDATE based on whether ID is set
110     *
111     * @param PDO $db Database connection
112     * @return bool True if successful
113     */
114    public function save(PDO $db): bool
115    {
116        // Either authorEmployeeId or authorName is required
117        if ((!isset($this->authorEmployeeId) && !isset($this->authorName)) || !isset($this->content) || !isset($this->startDate)) {
118            error_log("Note::save error: authorEmployeeId or authorName, content, and startDate are required");
119            return false;
120        }
121
122        // Set defaults
123        if (!isset($this->isManagerOnly)) {
124            $this->isManagerOnly = false;
125        }
126        if (!isset($this->isPinned)) {
127            $this->isPinned = false;
128        }
129
130        try {
131            if ($this->id) {
132                // Update existing note
133                $stmt = $db->prepare("
134                    UPDATE workbook_notes
135                    SET authorEmployeeId = :authorEmployeeId,
136                        authorName = :authorName,
137                        title = :title,
138                        content = :content,
139                        contentHtml = :contentHtml,
140                        startDate = :startDate,
141                        endDate = :endDate,
142                        isManagerOnly = :isManagerOnly,
143                        isPinned = :isPinned
144                    WHERE id = :id
145                ");
146
147                return $stmt->execute([
148                    ':id' => $this->id,
149                    ':authorEmployeeId' => $this->authorEmployeeId,
150                    ':authorName' => $this->authorName,
151                    ':title' => $this->title,
152                    ':content' => $this->content,
153                    ':contentHtml' => $this->contentHtml,
154                    ':startDate' => $this->startDate,
155                    ':endDate' => $this->endDate,
156                    ':isManagerOnly' => $this->isManagerOnly ? 1 : 0,
157                    ':isPinned' => $this->isPinned ? 1 : 0
158                ]);
159            } else {
160                // Insert new note
161                $stmt = $db->prepare("
162                    INSERT INTO workbook_notes
163                    (authorEmployeeId, authorName, title, content, contentHtml, startDate, endDate, isManagerOnly, isPinned)
164                    VALUES
165                    (:authorEmployeeId, :authorName, :title, :content, :contentHtml, :startDate, :endDate, :isManagerOnly, :isPinned)
166                ");
167
168                $result = $stmt->execute([
169                    ':authorEmployeeId' => $this->authorEmployeeId,
170                    ':authorName' => $this->authorName,
171                    ':title' => $this->title,
172                    ':content' => $this->content,
173                    ':contentHtml' => $this->contentHtml,
174                    ':startDate' => $this->startDate,
175                    ':endDate' => $this->endDate,
176                    ':isManagerOnly' => $this->isManagerOnly ? 1 : 0,
177                    ':isPinned' => $this->isPinned ? 1 : 0
178                ]);
179
180                if ($result) {
181                    $this->id = (int) $db->lastInsertId();
182                }
183
184                return $result;
185            }
186        } catch (PDOException $e) {
187            error_log("Note::save error: " . $e->getMessage());
188            return false;
189        }
190    }
191
192    /**
193     * Delete note and all related reactions/comments
194     *
195     * @param PDO $db Database connection
196     * @return bool True if successful
197     */
198    public function delete(PDO $db): bool
199    {
200        if (!isset($this->id)) {
201            error_log("Note::delete error: id is required");
202            return false;
203        }
204
205        // Check if already in a transaction (e.g., from test context)
206        $ownTransaction = !$db->inTransaction();
207
208        try {
209            // Start transaction only if not already in one
210            if ($ownTransaction) {
211                $db->beginTransaction();
212            }
213
214            // Delete reactions
215            $stmt = $db->prepare("DELETE FROM workbook_note_reactions WHERE noteId = :noteId");
216            $stmt->execute([':noteId' => $this->id]);
217
218            // Delete comments
219            $stmt = $db->prepare("DELETE FROM workbook_note_comments WHERE noteId = :noteId");
220            $stmt->execute([':noteId' => $this->id]);
221
222            // Delete note
223            $stmt = $db->prepare("DELETE FROM workbook_notes WHERE id = :id");
224            $result = $stmt->execute([':id' => $this->id]);
225
226            if ($ownTransaction) {
227                $db->commit();
228            }
229            return $result;
230        } catch (PDOException $e) {
231            if ($ownTransaction && $db->inTransaction()) {
232                $db->rollBack();
233            }
234            error_log("Note::delete error: " . $e->getMessage());
235            return false;
236        }
237    }
238
239    /**
240     * Convert Note to array for API responses
241     *
242     * @return array Associative array representation
243     */
244    public function toArray(): array
245    {
246        // Build author name from available data
247        $authorName = $this->authorName;
248        if (!$authorName && ($this->authorFirstName || $this->authorLastName)) {
249            $authorName = trim(($this->authorFirstName ?? '') . ' ' . ($this->authorLastName ?? ''));
250        }
251
252        return [
253            'id' => $this->id,
254            'authorEmployeeId' => $this->authorEmployeeId,
255            'authorFirstName' => $this->authorFirstName ?? null,
256            'authorLastName' => $this->authorLastName ?? null,
257            'authorName' => $authorName ?: null,
258            'title' => $this->title,
259            'content' => $this->content,
260            'contentHtml' => $this->contentHtml,
261            'startDate' => $this->startDate,
262            'endDate' => $this->endDate,
263            'isManagerOnly' => (bool) $this->isManagerOnly,
264            'isPinned' => (bool) $this->isPinned,
265            'reactionCount' => $this->reactionCount,
266            'commentCount' => $this->commentCount,
267            'userHasReacted' => (bool) $this->userHasReacted,
268            'createdAt' => $this->createdAt,
269            'updatedAt' => $this->updatedAt
270        ];
271    }
272
273    /**
274     * Create a Note object from an array
275     *
276     * @param array $data Associative array of note data
277     * @return self
278     */
279    public static function fromArray(array $data): self
280    {
281        $note = new self();
282        $note->id = isset($data['id']) ? (int) $data['id'] : null;
283        $note->authorEmployeeId = isset($data['authorEmployeeId']) ? (int) $data['authorEmployeeId'] : null;
284        $note->authorName = $data['authorName'] ?? null;
285        $note->title = $data['title'] ?? null;
286        $note->content = $data['content'];
287        $note->contentHtml = $data['contentHtml'] ?? null;
288        $note->startDate = $data['startDate'];
289        $note->endDate = $data['endDate'] ?? null;
290        // Handle string "false"/"true" from POST data
291        $note->isManagerOnly = isset($data['isManagerOnly']) && filter_var($data['isManagerOnly'], FILTER_VALIDATE_BOOLEAN);
292        $note->isPinned = isset($data['isPinned']) && filter_var($data['isPinned'], FILTER_VALIDATE_BOOLEAN);
293        $note->createdAt = $data['createdAt'] ?? null;
294        $note->updatedAt = $data['updatedAt'] ?? null;
295
296        // Joined fields
297        if (isset($data['authorFirstName'])) {
298            $note->authorFirstName = $data['authorFirstName'];
299        }
300        if (isset($data['authorLastName'])) {
301            $note->authorLastName = $data['authorLastName'];
302        }
303        if (isset($data['reactionCount'])) {
304            $note->reactionCount = (int) $data['reactionCount'];
305        }
306        if (isset($data['commentCount'])) {
307            $note->commentCount = (int) $data['commentCount'];
308        }
309        if (isset($data['userHasReacted'])) {
310            $note->userHasReacted = (bool) $data['userHasReacted'];
311        }
312
313        return $note;
314    }
315}