Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 211
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
BuyerKiosk\ServiceQueue\Controllers\gen_uid
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
ServiceQueueController
0.00% covered (danger)
0.00%
0 / 208
0.00% covered (danger)
0.00%
0 / 13
1190
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addToQueue
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
110
 startService
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 completeService
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 sendAblyUpdate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 removeService
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 saveNotes
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getCurrentQueue
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getQueueStats
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueuePosition
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getNextDailyNumber
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getCompletedToday
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 getCompletedByDate
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\ServiceQueue\Controllers;
4
5use Ably\AblyRest;
6use BuyerKiosk\ServiceQueue\ServiceQueue;
7use BuyerKiosk\ServiceQueue\ServiceQueueItem;
8
9if (!function_exists('gen_uid')) {
10    function gen_uid($length = 10) {
11        $randomStr = bin2hex(random_bytes($length));
12        return substr($randomStr, 0, $length);
13    }
14}
15
16class ServiceQueueController extends \BuyerKiosk\Core\Controllers\BaseController
17{
18    /**
19     * @var \Store
20     */
21    private $store;
22
23    /**
24     * @var \PDO
25     */
26    private $storeDB;
27
28    /**
29     * @var \BuyerKiosk\CustomerController
30     */
31    private $customerController;
32
33    /**
34     * @var \BuyerKiosk\ServiceQueue\ServiceQueue
35     */
36    private $serviceQueue;
37
38    /**
39     * @var \BuyerKiosk\ServiceQueue\ServiceQueueItem
40     */
41    private $queueItem;
42
43    /**
44     * ServiceQueueController constructor.
45     *
46     * @param \Slim\Slim $app
47     * @param \Store      $store
48     */
49    public function __construct($app, \Store $store)
50    {
51        parent::__construct($app);
52
53        $this->store = $store;
54        $this->storeDB = dbConnectByName($store->getDbName());
55        $this->customerController = new \BuyerKiosk\CustomerController($app, $store);
56        $this->serviceQueue = new ServiceQueue($store);
57        // Used for direct item-level operations (start, complete, etc.)
58        $this->queueItem = new ServiceQueueItem($store);
59    }
60
61
62    /**
63     * Add a new item to the queue (creates a new customer if needed).
64     *
65     * @param array $data
66     * @return array
67     */
68    public function addToQueue($data)
69    {
70        try {
71            // Begin transaction
72            $this->storeDB->beginTransaction();
73
74            // Check if customer exists by phone
75            $stmt = $this->storeDB->prepare("SELECT customerID, onEmail FROM customers WHERE phone = :phone LIMIT 1");
76            $stmt->execute([':phone' => $data['phone']]);
77            $customer = $stmt->fetch(\PDO::FETCH_ASSOC);
78
79            $customerID = null;
80            if (!$customer) {
81                // Create new customer
82                $customerData = [
83                    'phone'     => $data['phone'],
84                    'firstName' => $data['firstName'],
85                    'lastName'  => $data['lastName'],
86                    'email'     => $data['email'],
87                    'onEmail'   => $data['emailOptIn'] ?? 0
88                ];
89                $customerID = $this->customerController->createCustomer($customerData);
90                if (!$customerID) {
91                    throw new \Exception("Failed to create customer");
92                }
93            } else {
94                $customerID = $customer['customerID'];
95                // Update onEmail if emailOptIn is provided
96                if (isset($data['emailOptIn']) && $data['emailOptIn'] == 1 && $customer['onEmail'] == 0) {
97                    $this->customerController->updateEmailOptIn($customerID, 1);
98                }
99            }
100
101            // Get next daily number
102            $dailyNum = $this->getNextDailyNumber();
103
104            // Create queue item
105            $queueData = [
106                'dailyNum'   => $dailyNum,
107                'customerID' => $customerID,
108                'numItems'   => $data['numItems'] ?? 0,
109                'textMe'     => $data['textMe'] ?? 0,
110                'notes'      => $data['notes'] ?? null
111            ];
112            $queueId = $this->queueItem->create($queueData);
113
114            if (!$queueId) {
115                throw new \Exception("Failed to create queue item");
116            }
117
118            // Verify queue item creation
119            $createdItem = $this->queueItem->getById($queueId);
120            if (!$createdItem) {
121                throw new \Exception("Queue item creation verification failed");
122            }
123
124            $this->storeDB->commit();
125
126            // After success, send Ably event to let others know an item was inserted
127            $this->sendAblyUpdate(
128                'insert',
129                $this->store->getTypeNum(),
130                $data  // minimal payload; expand as needed
131            );
132            //Send another event to the android side
133            $androidData = array(
134                'buyID' => $queueId, //Due to my lazy partner I need to use the same variable name we use on the buy side
135                'action' => 'newService',
136                'category' => $this->store->getTypeNum()
137            );
138            $this->sendAblyUpdate(
139                'newService',
140                $this->store->getTypeNum(),
141                $androidData  // minimal payload; expand as needed
142            );
143
144            return [
145                'success' => true,
146                'message' => 'Successfully added to queue',
147                'data' => [
148                    'queueId'   => $queueId,
149                    'dailyNum'  => $dailyNum,
150                    'customerID'=> $customerID,
151                    'customerPhone' => $data['phone']
152                ]
153            ];
154        } catch (\Exception $e) {
155            if ($this->storeDB->inTransaction()) {
156                $this->storeDB->rollBack();
157            }
158
159            return [
160                'success' => false,
161                'message' => 'Operation failed',
162                'error'   => $e->getMessage(),
163                'debug'   => [
164                    'trace' => $e->getTraceAsString()
165                ]
166            ];
167        }
168    }
169
170    /**
171     * Start service for a queue item.
172     *
173     * @param int $id
174     * @return array
175     */
176    public function startService($id)
177    {
178        try {
179            $employeeID = $this->app->user->id ?? null;
180            $updateResult = $this->queueItem->startService($id, $employeeID);
181
182            if (!$updateResult) {
183                return [ 'success' => false, 'error' => 'Could not start service' ];
184            }
185            // Publish a 'start' event
186            $this->sendAblyUpdate(
187                'start',
188                $this->store->getTypeNum(),
189                [ 'id' => $id, 'employeeID' => $employeeID ]
190            );
191            return [ 'success' => true ];
192        } catch (\Exception $e) {
193            return [ 'success' => false, 'error' => $e->getMessage() ];
194        }
195    }
196
197    /**
198     * Complete service for a queue item.
199     *
200     * @param int $id
201     * @return array
202     */
203    public function completeService($id)
204    {
205        try {
206            $this->queueItem->getById($id);
207            $updateResult = $this->queueItem->completeService($id);
208            if (!$updateResult) {
209                return [ 'success' => false, 'error' => 'Could not complete service' ];
210            }
211            //Send SMS if we need to
212            if($this->queueItem->getTextMe() == 1 && $this->store->postBuySMS == 1){
213                $result = $this->store->textMessageService->sendServiceText($this->customerController->getCustomerById($this->queueItem->getCustomerId()));
214            }
215
216            // Publish a 'complete' event
217            $this->sendAblyUpdate(
218                'complete',
219                $this->store->getTypeNum(),
220                [ 'id' => $id ]
221            );
222            return [ 'success' => true ];
223        } catch (\Exception $e) {
224            return [ 'success' => false, 'error' => $e->getMessage() ];
225        }
226    }
227    private function sendAblyUpdate($action, $storeNum, array $payload = [])
228    {
229        // Use your existing Ably credentials
230        $ably = new AblyRest($_ENV['ABLY_KEY']);
231        $channel = $ably->channel($storeNum);
232
233        // category holds the channel name
234        $data = [
235            'action'   => $action,
236            'category' => $storeNum,
237            'payload'  => $payload,
238            'id'       => gen_uid(10) // random unique ID
239        ];
240        // Publish
241        $channel->publish($action, $data);
242    }
243
244    /**
245     * Remove (delete) a queue item.
246     *
247     * @param int $id
248     * @return array
249     */
250    public function removeService($id)
251    {
252        try {
253            $deleteResult = $this->queueItem->delete($id);
254            if (!$deleteResult) {
255                return [ 'success' => false, 'error' => 'Could not remove item' ];
256            }
257            // Publish a 'remove' event
258            $this->sendAblyUpdate(
259                'remove',
260                $this->store->getTypeNum(),
261                [ 'id' => $id ]
262            );
263            return [ 'success' => true ];
264        } catch (\Exception $e) {
265            return [ 'success' => false, 'error' => $e->getMessage() ];
266        }
267    }
268
269    /**
270     * Save notes for a queue item.
271     *
272     * @param int    $id
273     * @param string $notes
274     * @return array
275     */
276    public function saveNotes($id, $notes, $uuid = '')
277    {
278        try {
279            $updateResult = $this->queueItem->saveNotes($id, $notes);
280            if (!$updateResult) {
281                return [ 'success' => false, 'error' => 'Could not save notes' ];
282            }
283            // Publish a 'save-notes' event
284            $this->sendAblyUpdate(
285                'save-notes',
286                $this->store->getTypeNum(),
287                [
288                    'id' => $id,
289                    'notes' => $notes,
290                    'uuid' => $uuid
291                ]
292            );
293            return [ 'success' => true ];
294        } catch (\Exception $e) {
295            return [ 'success' => false, 'error' => $e->getMessage() ];
296        }
297    }
298
299    /**
300     * Fetch the current (unfinished) queue.
301     *
302     * @return array
303     * @throws \Exception
304     */
305    public function getCurrentQueue()
306    {
307        try {
308            $queueItems = $this->serviceQueue->getActiveQueue();
309            return $queueItems;
310        } catch (\Exception $e) {
311            error_log("[GET_CURRENT_QUEUE] Error: " . $e->getMessage());
312            throw $e;
313        }
314    }
315
316    /**
317     * Get daily queue stats (wait time, service time, etc.).
318     *
319     * @return array
320     */
321    public function getQueueStats()
322    {
323        return $this->serviceQueue->getQueueStats();
324    }
325
326    /**
327     * Get the queue position for a newly created item.
328     *
329     * @param int $queueId
330     * @return int|string
331     */
332    private function getQueuePosition($queueId)
333    {
334        $sql = "SELECT COUNT(*) as position
335                FROM serviceQueue
336                WHERE isFinished = 0
337                  AND timeStarted IS NULL
338                  AND id <= :queueId
339                  AND DATE(timeEntered) = CURDATE()";
340        $stmt = $this->storeDB->prepare($sql);
341        $stmt->execute([':queueId' => $queueId]);
342        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
343
344        return $row['position'] ?? 0;
345    }
346
347    /**
348     * Generate the next daily number.
349     *
350     * @return int
351     */
352    private function getNextDailyNumber()
353    {
354        $timeZone = $this->store->getTimeZone();
355
356        $sql = "SELECT COALESCE(MAX(dailyNum), 0) + 1 as nextNum
357            FROM serviceQueue
358            WHERE DATE(timeEntered) = DATE(NOW())";
359        $stmt = $this->storeDB->prepare($sql);
360        $stmt->execute([':tz' => $timeZone]);
361        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
362
363        return (int)($row['nextNum'] ?? 1);
364    }
365    public function getCompletedToday() {
366
367        // Get store timezone
368        $storeTimezone = $this->store->getTimeZone();
369
370        $dateRange = getCurrentDateRange();
371        $startDate = $dateRange['dateStart']->format('Y-m-d H:i:s');
372        $endDate = $dateRange['dateEnd']->format('Y-m-d H:i:s');
373
374        $sql = "SELECT sq.*, 
375                c.firstName as firstName, 
376                c.lastName as lastName,
377                c.phone as phone
378                FROM serviceQueue sq
379                LEFT JOIN customers c ON sq.customerID = c.customerID
380                WHERE sq.isFinished = 1 
381                AND sq.timeCompleted BETWEEN :startDate AND :endDate
382                ORDER BY sq.timeCompleted DESC";
383        $stmt = $this->storeDB->prepare($sql);
384        $stmt->execute([
385            ':startDate' => $startDate,
386            ':endDate' => $endDate
387        ]);
388
389        // Query the database for completed items
390
391        $completedItems = $stmt->fetchAll(\PDO::FETCH_ASSOC);
392
393        // Format the data for frontend consumption
394        return array_map(function($item) {
395            return [
396                'id' => $item['id'],
397                'customerFirstName' => $item['firstName'],
398                'customerLastName' => $item['lastName'],
399                'customerPhone' => $item['phone'],
400                'dailyNum' => $item['dailyNum'],
401                'timeEntered' => $item['timeEntered'],
402                'timeCompleted' => $item['timeCompleted'],
403                'textMe' => $item['textMe']
404            ];
405        }, $completedItems);
406    }
407    public function getCompletedByDate($date) {
408        $storeTimezone = $this->store->getTimeZone();
409
410        if($_ENV['DEV']) {
411            $sql = "SELECT sq.*, 
412            c.firstName, 
413            c.lastName,
414            c.phone
415            FROM serviceQueue sq
416            LEFT JOIN customers c ON sq.customerID = c.customerID
417            WHERE sq.isFinished = 1 
418            AND DATE(sq.timeCompleted) = :date
419            ORDER BY sq.timeCompleted ASC";
420            $stmt = $this->storeDB->prepare($sql);
421            $stmt->execute([
422                ':date' => $date
423            ]);
424        } else {
425            $sql = "SELECT sq.*, 
426            c.firstName, 
427            c.lastName,
428            c.phone
429            FROM serviceQueue sq
430            LEFT JOIN customers c ON sq.customerID = c.customerID
431            WHERE sq.isFinished = 1 
432            AND DATE(CONVERT_TZ(sq.timeCompleted, 'UTC', :timezone)) = :date
433            ORDER BY sq.timeCompleted ASC";
434
435            $stmt = $this->storeDB->prepare($sql);
436            $stmt->execute([
437                ':timezone' => $storeTimezone,
438                ':date' => $date
439            ]);
440        }
441        $items = $stmt->fetchAll(\PDO::FETCH_ASSOC);
442
443        return array_map(function($item) {
444            return [
445                'id' => $item['id'],
446                'customerFirstName' => $item['firstName'],
447                'customerLastName' => $item['lastName'],
448                'customerPhone' => $item['phone'],
449                'dailyNum' => $item['dailyNum'],
450                'timeEntered' => $item['timeEntered'],
451                'timeCompleted' => $item['timeCompleted'],
452                'textMe' => $item['textMe']
453            ];
454        }, $items);
455    }
456
457}