Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 436
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
TasksApiController
0.00% covered (danger)
0.00%
0 / 436
0.00% covered (danger)
0.00%
0 / 15
6480
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
 getTodayLists
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getActiveList
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getSchedule
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 updateSchedule
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
72
 getListById
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 updateTaskStatus
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
210
 addTaskComment
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
42
 getTaskComments
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 createTaskList
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 updateTaskList
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
132
 assignTask
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
42
 getCompletedTasks
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
90
 getTaskCompletion
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
42
 invalidateTaskCache
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\Workbook\Controllers;
4
5use BuyerKiosk\Workbook\TaskListManager;
6use BuyerKiosk\Workbook\TaskCompletion;
7use BuyerKiosk\Workbook\TaskComment;
8use BuyerKiosk\Workbook\TaskAssignment;
9use BuyerKiosk\Workbook\WorkbookAbly;
10use Exception;
11
12/**
13 * Task System API Controller
14 *
15 * Handles REST API endpoints for the Workbook Task System including
16 * task list management, completion tracking, comments, and assignments.
17 *
18 * This controller uses the unified users table (kiosk_users.users) with store assignments
19 * (kiosk_users.userStoreAssignments) instead of per-store employee tables.
20 *
21 * @package BuyerKiosk\Workbook
22 */
23class TasksApiController
24{
25    /**
26     * @var \Slim\Slim Slim application instance
27     */
28    private $app;
29
30    /**
31     * @var \Store Store object
32     */
33    private $store;
34
35    /**
36     * @var \PDO Store database connection
37     */
38    private $db;
39
40    /**
41     * @var \PDO Central database connection for unified users
42     */
43    private $centralDb;
44
45    /**
46     * @var string Store identifier
47     */
48    private $typeNum;
49
50    /**
51     * Constructor
52     *
53     * @param \Slim\Slim $app Slim application instance
54     * @param \Store $store Store object
55     */
56    public function __construct($app, \Store $store)
57    {
58        $this->app = $app;
59        $this->store = $store;
60        $this->db = dbConnectByName($store->getDbName());
61        $this->centralDb = \dbConnectByName('kiosk_users');
62        $this->typeNum = $store->getTypeNum();
63    }
64
65    /**
66     * GET /api/:typeNum/workbook/tasks/lists/
67     * Get all task lists for today
68     */
69    public function getTodayLists()
70    {
71        try {
72            $manager = new TaskListManager($this->store);
73            $lists = $manager->getTodayLists();
74
75            $this->app->response->headers->set('Content-Type', 'application/json');
76            $this->app->response->setBody(json_encode([
77                'success' => true,
78                'lists' => $lists
79            ]));
80        } catch (Exception $e) {
81            error_log("TasksApiController::getTodayLists error: " . $e->getMessage());
82            $this->app->halt(500, json_encode([
83                'error' => 'Failed to retrieve task lists'
84            ]));
85        }
86    }
87
88    /**
89     * GET /api/:typeNum/workbook/tasks/lists/active/
90     * Get the currently active task list based on time with carryover tasks
91     *
92     * Response includes:
93     * - activeList: The currently active task list with its tasks
94     * - carryoverTasks: Incomplete tasks from earlier lists (shown in red)
95     * - nextListTime: When the next list starts (for polling)
96     * - nextListName: Name of the next upcoming list
97     */
98    public function getActiveList()
99    {
100        try {
101            $manager = new TaskListManager($this->store);
102            $result = $manager->getActiveListWithCarryover();
103
104            $this->app->response->headers->set('Content-Type', 'application/json');
105            $this->app->response->setBody(json_encode([
106                'success' => true,
107                'activeList' => $result['activeList'],
108                'carryoverTasks' => $result['carryoverTasks'],
109                'nextListTime' => $result['nextListTime'],
110                'nextListName' => $result['nextListName'] ?? null
111            ]));
112        } catch (Exception $e) {
113            error_log("TasksApiController::getActiveList error: " . $e->getMessage());
114            $this->app->halt(500, json_encode([
115                'error' => 'Failed to retrieve active task list'
116            ]));
117        }
118    }
119
120    /**
121     * GET /api/:typeNum/workbook/tasks/schedule/
122     * Get full schedule configuration for all task lists
123     */
124    public function getSchedule()
125    {
126        try {
127            $manager = new TaskListManager($this->store);
128            $schedule = $manager->getFullSchedule();
129
130            $this->app->response->headers->set('Content-Type', 'application/json');
131            $this->app->response->setBody(json_encode([
132                'success' => true,
133                'schedule' => $schedule
134            ]));
135        } catch (Exception $e) {
136            error_log("TasksApiController::getSchedule error: " . $e->getMessage());
137            $this->app->halt(500, json_encode([
138                'error' => 'Failed to retrieve schedule'
139            ]));
140        }
141    }
142
143    /**
144     * PUT /api/:typeNum/workbook/tasks/schedule/:listId/
145     * Update schedule for a task list
146     *
147     * Request body: {"days": {"SUN": "08:00:00", "MON": {"startTime": "09:00:00", "isActive": true}, ...}}
148     *
149     * @param string $typeNum Store identifier
150     * @param int $listId Task list ID
151     */
152    public function updateSchedule($typeNum, $listId)
153    {
154        // Check admin permission
155        if (!$this->app->user->checkAccess('workbook_manage_tasks')) {
156            $this->app->halt(403, json_encode([
157                'error' => 'Access denied. Requires workbook_manage_tasks permission'
158            ]));
159            return;
160        }
161
162        try {
163            $data = json_decode($this->app->request->getBody(), true);
164
165            if (empty($data['days']) || !is_array($data['days'])) {
166                $this->app->halt(400, json_encode([
167                    'error' => 'days object is required'
168                ]));
169                return;
170            }
171
172            // Validate days
173            $validDays = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
174            foreach (array_keys($data['days']) as $day) {
175                if (!in_array($day, $validDays)) {
176                    $this->app->halt(400, json_encode([
177                        'error' => 'Invalid day: ' . $day
178                    ]));
179                    return;
180                }
181            }
182
183            $manager = new TaskListManager($this->store);
184            $success = $manager->updateScheduleBatch((int) $listId, $data['days']);
185
186            if ($success) {
187                $this->app->response->headers->set('Content-Type', 'application/json');
188                $this->app->response->setBody(json_encode([
189                    'success' => true
190                ]));
191            } else {
192                $this->app->halt(500, json_encode([
193                    'error' => 'Failed to update schedule'
194                ]));
195            }
196        } catch (Exception $e) {
197            error_log("TasksApiController::updateSchedule error: " . $e->getMessage());
198            $this->app->halt(500, json_encode([
199                'error' => 'Failed to update schedule'
200            ]));
201        }
202    }
203
204    /**
205     * GET /api/:typeNum/workbook/tasks/lists/:listId/
206     * Get a specific task list with its tasks
207     *
208     * @param int $listId Task list ID
209     */
210    public function getListById($listId)
211    {
212        try {
213            $stmt = $this->db->prepare(
214                "SELECT dtl.*, tg.groupName
215                 FROM workbook_task_lists dtl
216                 JOIN taskGroups tg ON dtl.groupId = tg.id
217                 WHERE dtl.id = :listId"
218            );
219            $stmt->bindValue(':listId', $listId, \PDO::PARAM_INT);
220            $stmt->execute();
221
222            $list = $stmt->fetch(\PDO::FETCH_ASSOC);
223
224            if (!$list) {
225                $this->app->halt(404, json_encode([
226                    'error' => 'Task list not found'
227                ]));
228                return;
229            }
230
231            $manager = new TaskListManager($this->store);
232            $list['tasks'] = $manager->getTasksForList($list['groupId']);
233            $list['completionPercentage'] = $manager->calculateCompletionPercentage($list['tasks']);
234
235            $this->app->response->headers->set('Content-Type', 'application/json');
236            $this->app->response->setBody(json_encode([
237                'success' => true,
238                'list' => $list
239            ]));
240        } catch (Exception $e) {
241            error_log("TasksApiController::getListById error: " . $e->getMessage());
242            $this->app->halt(500, json_encode([
243                'error' => 'Failed to retrieve task list'
244            ]));
245        }
246    }
247
248    /**
249     * POST /api/:typeNum/workbook/tasks/:taskId/status/
250     * Update task completion status
251     *
252     * Request body: {"status": 0|1|2, "date": "Y-m-d", "employeeId": int, "notes": string}
253     *
254     * @param string $typeNum Store identifier
255     * @param int $taskId Task ID
256     */
257    public function updateTaskStatus($typeNum, $taskId)
258    {
259        try {
260            $data = json_decode($this->app->request->getBody(), true);
261
262            // Debug logging
263            error_log("TasksApiController::updateTaskStatus - taskId: $taskId, typeNum: $typeNum, data: " . json_encode($data));
264
265            // Validate input
266            if (!isset($data['status']) || !in_array($data['status'], [0, 1, 2], true)) {
267                $this->app->halt(400, json_encode([
268                    'error' => 'Invalid status. Must be 0 (Not Started), 1 (In Progress), or 2 (Completed)'
269                ]));
270                return;
271            }
272
273            // Verify task exists
274            $stmt = $this->db->prepare("SELECT id FROM tasks WHERE id = :taskId");
275            $stmt->bindValue(':taskId', $taskId, \PDO::PARAM_INT);
276            $stmt->execute();
277            if (!$stmt->fetch()) {
278                $this->app->halt(404, json_encode([
279                    'error' => 'Task not found'
280                ]));
281                return;
282            }
283
284            $completion = TaskCompletion::getForTaskAndDate($this->db, $taskId, $data['date'] ?? date('Y-m-d'));
285            if (!$completion) {
286                $completion = new TaskCompletion($this->db);
287                $completion->taskId = $taskId;
288                $completion->date = $data['date'] ?? date('Y-m-d');
289            }
290
291            $completion->status = $data['status'];
292            $completion->completedBy = $data['employeeId'] ?? null;
293            $completion->notes = $data['notes'] ?? null;
294
295            // Set completedAt timestamp
296            if ($data['status'] == 2) {
297                // Allow custom completedAt time (for editing), otherwise use now
298                if (isset($data['completedAt']) && !empty($data['completedAt'])) {
299                    $completion->completedAt = $data['completedAt'];
300                } else {
301                    $completion->completedAt = date('Y-m-d H:i:s');
302                }
303            } else {
304                $completion->completedAt = null;
305            }
306
307            $saveResult = $completion->save();
308            error_log("TasksApiController::updateTaskStatus - save result: " . ($saveResult ? 'true' : 'false') . ", completionId: " . ($completion->id ?? 'null'));
309
310            if ($saveResult) {
311                // Invalidate cache
312                $this->invalidateTaskCache($completion->date);
313
314                // Publish to Ably
315                try {
316                    $ably = new WorkbookAbly($typeNum);
317                    if ($data['status'] == 2) {
318                        $ably->taskCompleted($taskId, $data['employeeId'] ?? null, $completion->date);
319                    } elseif ($data['status'] == 1) {
320                        $ably->publish('workbook:task:progress', [
321                            'taskId' => $taskId,
322                            'employeeId' => $data['employeeId'] ?? null,
323                            'date' => $completion->date
324                        ]);
325                    } else {
326                        $ably->publish('workbook:task:uncomplete', [
327                            'taskId' => $taskId,
328                            'date' => $completion->date
329                        ]);
330                    }
331                } catch (Exception $e) {
332                    // Log but don't fail on Ably error
333                    error_log("TasksApiController::updateTaskStatus Ably error: " . $e->getMessage());
334                }
335
336                $this->app->response->headers->set('Content-Type', 'application/json');
337                $this->app->response->setBody(json_encode([
338                    'success' => true,
339                    'debug' => [
340                        'taskId' => $taskId,
341                        'date' => $completion->date,
342                        'status' => $completion->status,
343                        'completionId' => $completion->id
344                    ]
345                ]));
346            } else {
347                error_log("TasksApiController::updateTaskStatus - save failed for task $taskId");
348                $this->app->halt(500, json_encode([
349                    'error' => 'Failed to update task status'
350                ]));
351            }
352        } catch (Exception $e) {
353            error_log("TasksApiController::updateTaskStatus error: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
354            $this->app->halt(500, json_encode([
355                'error' => 'Failed to update task status',
356                'debug' => $e->getMessage()
357            ]));
358        }
359    }
360
361    /**
362     * POST /api/:typeNum/workbook/tasks/:taskId/comment/
363     * Add comment to task
364     *
365     * Request body: {"comment": string, "date": "Y-m-d", "employeeId": int}
366     *
367     * @param string $typeNum Store identifier
368     * @param int $taskId Task ID
369     */
370    public function addTaskComment($typeNum, $taskId)
371    {
372        try {
373            $data = json_decode($this->app->request->getBody(), true);
374
375            if (empty($data['comment'])) {
376                $this->app->halt(400, json_encode([
377                    'error' => 'Comment is required'
378                ]));
379                return;
380            }
381
382            if (strlen($data['comment']) > 1000) {
383                $this->app->halt(400, json_encode([
384                    'error' => 'Comment exceeds maximum length of 1000 characters'
385                ]));
386                return;
387            }
388
389            // Verify task exists
390            $stmt = $this->db->prepare("SELECT id FROM tasks WHERE id = :taskId");
391            $stmt->bindValue(':taskId', $taskId, \PDO::PARAM_INT);
392            $stmt->execute();
393            if (!$stmt->fetch()) {
394                $this->app->halt(404, json_encode([
395                    'error' => 'Task not found'
396                ]));
397                return;
398            }
399
400            $comment = new TaskComment($this->db);
401            $comment->taskId = $taskId;
402            $comment->date = $data['date'] ?? date('Y-m-d');
403            $comment->employeeId = $data['employeeId'];
404            $comment->comment = trim($data['comment']);
405
406            $commentId = $comment->save();
407
408            // Publish to Ably
409            try {
410                $ably = new WorkbookAbly($typeNum);
411                $ably->taskComment($taskId, $commentId, $data['employeeId'], $data['comment']);
412            } catch (Exception $e) {
413                // Log but don't fail on Ably error
414                error_log("TasksApiController::addTaskComment Ably error: " . $e->getMessage());
415            }
416
417            $this->app->response->headers->set('Content-Type', 'application/json');
418            $this->app->response->setBody(json_encode([
419                'success' => true,
420                'commentId' => $commentId
421            ]));
422        } catch (Exception $e) {
423            error_log("TasksApiController::addTaskComment error: " . $e->getMessage());
424            $this->app->halt(500, json_encode([
425                'error' => 'Failed to add comment'
426            ]));
427        }
428    }
429
430    /**
431     * GET /api/:typeNum/workbook/tasks/:taskId/comments/
432     * Get comments for a task on a specific date
433     *
434     * Query params: date (default: today)
435     *
436     * @param string $typeNum Store identifier
437     * @param int $taskId Task ID
438     */
439    public function getTaskComments($typeNum, $taskId)
440    {
441        try {
442            $date = $this->app->request->get('date') ?? date('Y-m-d');
443
444            // Verify task exists
445            $stmt = $this->db->prepare("SELECT id FROM tasks WHERE id = :taskId");
446            $stmt->bindValue(':taskId', $taskId, \PDO::PARAM_INT);
447            $stmt->execute();
448            if (!$stmt->fetch()) {
449                $this->app->halt(404, json_encode([
450                    'error' => 'Task not found'
451                ]));
452                return;
453            }
454
455            $comments = TaskComment::getForTaskAndDate($this->db, $taskId, $date);
456
457            $this->app->response->headers->set('Content-Type', 'application/json');
458            $this->app->response->setBody(json_encode([
459                'success' => true,
460                'comments' => $comments
461            ]));
462        } catch (Exception $e) {
463            error_log("TasksApiController::getTaskComments error: " . $e->getMessage());
464            $this->app->halt(500, json_encode([
465                'error' => 'Failed to retrieve comments'
466            ]));
467        }
468    }
469
470    /**
471     * POST /api/:typeNum/workbook/tasks/lists/
472     * Create new task list configuration (admin)
473     *
474     * Request body: {"groupId": int, "displayName": string, "scheduledDays": string, "startTime": string, "sortOrder": int}
475     *
476     * @param string $typeNum Store identifier
477     */
478    public function createTaskList($typeNum)
479    {
480        // Check admin permission
481        if (!$this->app->user->checkAccess('workbook_manage_tasks')) {
482            $this->app->halt(403, json_encode([
483                'error' => 'Access denied. Requires workbook_manage_tasks permission'
484            ]));
485            return;
486        }
487
488        try {
489            $data = json_decode($this->app->request->getBody(), true);
490
491            if (empty($data['groupId'])) {
492                $this->app->halt(400, json_encode([
493                    'error' => 'groupId is required'
494                ]));
495                return;
496            }
497
498            // Verify task group exists
499            $stmt = $this->db->prepare("SELECT id FROM taskGroups WHERE id = :groupId");
500            $stmt->bindValue(':groupId', $data['groupId'], \PDO::PARAM_INT);
501            $stmt->execute();
502            if (!$stmt->fetch()) {
503                $this->app->halt(404, json_encode([
504                    'error' => 'Task group not found'
505                ]));
506                return;
507            }
508
509            $manager = new TaskListManager($this->store);
510            $listId = $manager->createTaskList($data);
511
512            $this->app->response->setStatus(201);
513            $this->app->response->headers->set('Content-Type', 'application/json');
514            $this->app->response->setBody(json_encode([
515                'success' => true,
516                'listId' => $listId
517            ]));
518        } catch (Exception $e) {
519            error_log("TasksApiController::createTaskList error: " . $e->getMessage());
520            $this->app->halt(500, json_encode([
521                'error' => 'Failed to create task list'
522            ]));
523        }
524    }
525
526    /**
527     * PUT /api/:typeNum/workbook/tasks/lists/:listId/
528     * Update task list configuration (admin)
529     *
530     * Request body: {"displayName": string, "scheduledDays": string, "startTime": string, "sortOrder": int, "isActive": bool}
531     *
532     * @param string $typeNum Store identifier
533     * @param int $listId Task list ID
534     */
535    public function updateTaskList($typeNum, $listId)
536    {
537        // Check admin permission
538        if (!$this->app->user->checkAccess('workbook_manage_tasks')) {
539            $this->app->halt(403, json_encode([
540                'error' => 'Access denied. Requires workbook_manage_tasks permission'
541            ]));
542            return;
543        }
544
545        try {
546            $data = json_decode($this->app->request->getBody(), true);
547
548            // Verify list exists
549            $stmt = $this->db->prepare("SELECT id FROM workbook_task_lists WHERE id = :listId");
550            $stmt->bindValue(':listId', $listId, \PDO::PARAM_INT);
551            $stmt->execute();
552            if (!$stmt->fetch()) {
553                $this->app->halt(404, json_encode([
554                    'error' => 'Task list not found'
555                ]));
556                return;
557            }
558
559            // Build update query
560            $updateFields = [];
561            $params = [':listId' => $listId];
562
563            if (isset($data['displayName'])) {
564                $updateFields[] = 'displayName = :displayName';
565                $params[':displayName'] = $data['displayName'];
566            }
567            if (isset($data['scheduledDays'])) {
568                $updateFields[] = 'scheduledDays = :scheduledDays';
569                $params[':scheduledDays'] = $data['scheduledDays'];
570            }
571            if (isset($data['startTime'])) {
572                $updateFields[] = 'startTime = :startTime';
573                $params[':startTime'] = $data['startTime'];
574            }
575            if (isset($data['sortOrder'])) {
576                $updateFields[] = 'sortOrder = :sortOrder';
577                $params[':sortOrder'] = $data['sortOrder'];
578            }
579            if (isset($data['isActive'])) {
580                $updateFields[] = 'isActive = :isActive';
581                $params[':isActive'] = $data['isActive'] ? 1 : 0;
582            }
583
584            if (empty($updateFields)) {
585                $this->app->halt(400, json_encode([
586                    'error' => 'No fields to update'
587                ]));
588                return;
589            }
590
591            $sql = "UPDATE workbook_task_lists SET " . implode(', ', $updateFields) . " WHERE id = :listId";
592            $stmt = $this->db->prepare($sql);
593            $stmt->execute($params);
594
595            // Invalidate cache
596            $this->invalidateTaskCache(date('Y-m-d'));
597
598            $this->app->response->headers->set('Content-Type', 'application/json');
599            $this->app->response->setBody(json_encode([
600                'success' => true
601            ]));
602        } catch (Exception $e) {
603            error_log("TasksApiController::updateTaskList error: " . $e->getMessage());
604            $this->app->halt(500, json_encode([
605                'error' => 'Failed to update task list'
606            ]));
607        }
608    }
609
610    /**
611     * POST /api/:typeNum/workbook/tasks/:taskId/assign/
612     * Assign task to employee (admin)
613     *
614     * Request body: {"employeeId": int|null, "dueDate": string, "dueTime": string}
615     *
616     * @param string $typeNum Store identifier
617     * @param int $taskId Task ID
618     */
619    public function assignTask($typeNum, $taskId)
620    {
621        // Check admin permission
622        if (!$this->app->user->checkAccess('workbook_manage_tasks')) {
623            $this->app->halt(403, json_encode([
624                'error' => 'Access denied. Requires workbook_manage_tasks permission'
625            ]));
626            return;
627        }
628
629        try {
630            $data = json_decode($this->app->request->getBody(), true);
631
632            // Verify task exists
633            $stmt = $this->db->prepare("SELECT id FROM tasks WHERE id = :taskId");
634            $stmt->bindValue(':taskId', $taskId, \PDO::PARAM_INT);
635            $stmt->execute();
636            if (!$stmt->fetch()) {
637                $this->app->halt(404, json_encode([
638                    'error' => 'Task not found'
639                ]));
640                return;
641            }
642
643            // Verify employee exists in unified users table (if provided)
644            if (!empty($data['employeeId'])) {
645                $stmt = $this->centralDb->prepare("
646                    SELECT u.id
647                    FROM users u
648                    INNER JOIN userStoreAssignments usa ON u.id = usa.userId
649                    WHERE u.id = :employeeId
650                      AND usa.typeNum = :typeNum
651                      AND usa.isActive = 1
652                      AND u.enabled = 1
653                ");
654                $stmt->execute([
655                    ':employeeId' => (int)$data['employeeId'],
656                    ':typeNum' => $this->typeNum
657                ]);
658                if (!$stmt->fetch()) {
659                    $this->app->halt(404, json_encode([
660                        'error' => 'Employee not found or inactive'
661                    ]));
662                    return;
663                }
664            }
665
666            $assignment = new TaskAssignment($this->db);
667            $assignment->taskId = $taskId;
668            $assignment->employeeId = $data['employeeId'] ?? null;
669            $assignment->dueDate = $data['dueDate'] ?? date('Y-m-d');
670            $assignment->dueTime = $data['dueTime'] ?? null;
671
672            $assignmentId = $assignment->save();
673
674            // Invalidate cache
675            $this->invalidateTaskCache($assignment->dueDate);
676
677            $this->app->response->setStatus(201);
678            $this->app->response->headers->set('Content-Type', 'application/json');
679            $this->app->response->setBody(json_encode([
680                'success' => true,
681                'assignmentId' => $assignmentId
682            ]));
683        } catch (Exception $e) {
684            error_log("TasksApiController::assignTask error: " . $e->getMessage());
685            $this->app->halt(500, json_encode([
686                'error' => 'Failed to assign task'
687            ]));
688        }
689    }
690
691    /**
692     * GET /api/:typeNum/workbook/tasks/completed/
693     * Get all completed tasks for a specific date
694     *
695     * @param string $typeNum Store identifier
696     */
697    public function getCompletedTasks($typeNum)
698    {
699        try {
700            $date = $this->app->request->get('date');
701            if (!$date) {
702                $timezone = new \DateTimeZone($this->store->timezone);
703                $now = new \DateTime('now', $timezone);
704                $date = $now->format('Y-m-d');
705            }
706
707            // Query completed tasks from store database (without employee join)
708            $stmt = $this->db->prepare("
709                SELECT
710                    t.id,
711                    t.taskName,
712                    t.comment,
713                    t.taskGroup,
714                    tg.groupName as listName,
715                    dtc.status,
716                    dtc.completedBy,
717                    dtc.completedAt,
718                    dtc.notes as completionNotes
719                FROM tasks t
720                INNER JOIN workbook_task_completions dtc ON t.id = dtc.taskId AND dtc.date = :date
721                LEFT JOIN taskGroups tg ON t.taskGroup = tg.id
722                WHERE dtc.status = 2
723                ORDER BY dtc.completedAt DESC
724            ");
725            $stmt->execute([':date' => $date]);
726            $tasks = $stmt->fetchAll(\PDO::FETCH_ASSOC);
727
728            // Enrich with employee names from unified users table
729            if (!empty($tasks)) {
730                $userIds = array_unique(array_filter(array_column($tasks, 'completedBy')));
731                $userNames = [];
732                if (!empty($userIds)) {
733                    $placeholders = implode(',', array_fill(0, count($userIds), '?'));
734                    $userStmt = $this->centralDb->prepare("
735                        SELECT id, firstName, lastName
736                        FROM users
737                        WHERE id IN ($placeholders)
738                    ");
739                    $userStmt->execute(array_values($userIds));
740                    $users = $userStmt->fetchAll(\PDO::FETCH_ASSOC);
741                    foreach ($users as $user) {
742                        $userNames[$user['id']] = [
743                            'firstName' => $user['firstName'],
744                            'lastName' => $user['lastName']
745                        ];
746                    }
747                }
748
749                // Add employee names to tasks
750                foreach ($tasks as &$task) {
751                    $userId = $task['completedBy'];
752                    if ($userId && isset($userNames[$userId])) {
753                        $task['completedByFirstName'] = $userNames[$userId]['firstName'];
754                        $task['completedByLastName'] = $userNames[$userId]['lastName'];
755                    } else {
756                        $task['completedByFirstName'] = null;
757                        $task['completedByLastName'] = null;
758                    }
759                }
760                unset($task);
761            }
762
763            $this->app->response->headers->set('Content-Type', 'application/json');
764            $this->app->response->setBody(json_encode([
765                'success' => true,
766                'tasks' => $tasks,
767                'date' => $date
768            ]));
769        } catch (Exception $e) {
770            error_log("TasksApiController::getCompletedTasks error: " . $e->getMessage());
771            $this->app->halt(500, json_encode([
772                'error' => 'Failed to retrieve completed tasks'
773            ]));
774        }
775    }
776
777    /**
778     * GET /api/:typeNum/workbook/tasks/:taskId/completion/
779     * Get completion details for a specific task
780     *
781     * @param string $typeNum Store identifier
782     * @param int $taskId Task ID
783     */
784    public function getTaskCompletion($typeNum, $taskId)
785    {
786        try {
787            $date = $this->app->request->get('date');
788            if (!$date) {
789                $timezone = new \DateTimeZone($this->store->timezone);
790                $now = new \DateTime('now', $timezone);
791                $date = $now->format('Y-m-d');
792            }
793
794            // Query task completion from store database (without employee join)
795            $stmt = $this->db->prepare("
796                SELECT
797                    t.id,
798                    t.taskName,
799                    t.comment,
800                    t.taskGroup,
801                    tg.groupName as listName,
802                    dtc.id as completionId,
803                    dtc.status,
804                    dtc.completedBy,
805                    dtc.completedAt,
806                    dtc.notes as completionNotes
807                FROM tasks t
808                LEFT JOIN workbook_task_completions dtc ON t.id = dtc.taskId AND dtc.date = :date
809                LEFT JOIN taskGroups tg ON t.taskGroup = tg.id
810                WHERE t.id = :taskId
811            ");
812            $stmt->execute([':taskId' => $taskId, ':date' => $date]);
813            $task = $stmt->fetch(\PDO::FETCH_ASSOC);
814
815            if (!$task) {
816                $this->app->halt(404, json_encode([
817                    'error' => 'Task not found'
818                ]));
819                return;
820            }
821
822            // Enrich with employee name from unified users table
823            $task['completedByFirstName'] = null;
824            $task['completedByLastName'] = null;
825            if (!empty($task['completedBy'])) {
826                $userStmt = $this->centralDb->prepare("
827                    SELECT firstName, lastName
828                    FROM users
829                    WHERE id = :userId
830                ");
831                $userStmt->execute([':userId' => $task['completedBy']]);
832                $user = $userStmt->fetch(\PDO::FETCH_ASSOC);
833                if ($user) {
834                    $task['completedByFirstName'] = $user['firstName'];
835                    $task['completedByLastName'] = $user['lastName'];
836                }
837            }
838
839            $this->app->response->headers->set('Content-Type', 'application/json');
840            $this->app->response->setBody(json_encode([
841                'success' => true,
842                'task' => $task,
843                'date' => $date
844            ]));
845        } catch (Exception $e) {
846            error_log("TasksApiController::getTaskCompletion error: " . $e->getMessage());
847            $this->app->halt(500, json_encode([
848                'error' => 'Failed to retrieve task completion'
849            ]));
850        }
851    }
852
853    /**
854     * Invalidate task cache for a specific date
855     *
856     * @param string $date Date in Y-m-d format
857     */
858    private function invalidateTaskCache($date)
859    {
860        try {
861            $predis = new \Predis\Client($_ENV['REDIS_URL']);
862            $cacheKey = $this->store->getTypeNum() . '_workbook_tasks_' . $date;
863            $predis->del($cacheKey);
864        } catch (Exception $e) {
865            error_log("TasksApiController::invalidateTaskCache error: " . $e->getMessage());
866            // Don't fail on cache error
867        }
868    }
869}