Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 120
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ScheduleManager
0.00% covered (danger)
0.00%
0 / 120
0.00% covered (danger)
0.00%
0 / 10
1406
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 resolveProvider
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
90
 getTodaySchedule
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getScheduleForDate
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 refreshSchedule
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 enrichWithClockStatus
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getClockedInEmployeeIds
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 getBuyerKioskClockedInIds
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getProvider
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BuyerKiosk\Workbook;
4
5use BuyerKiosk\Scheduling\BuyerKioskSchedule;
6use BuyerKiosk\WhenIWork\FinancialsController;
7use Exception;
8
9/**
10 * Schedule Manager
11 *
12 * Manages employee schedules across multiple providers (WhenIWork, Homebase).
13 * Handles provider resolution, schedule retrieval, and clock-in status enrichment.
14 *
15 * @package BuyerKiosk\Workbook
16 */
17class ScheduleManager
18{
19    /**
20     * @var \Store Store instance
21     */
22    private $store;
23
24    /**
25     * @var ScheduleProvider|null Active schedule provider
26     */
27    private $provider;
28
29    /**
30     * @var \Slim\Slim Slim application instance
31     */
32    private $app;
33
34    /**
35     * Constructor
36     *
37     * @param \Slim\Slim $app Slim application instance
38     * @param \Store $store Store instance
39     */
40    public function __construct($app, \Store $store)
41    {
42        $this->app = $app;
43        $this->store = $store;
44        $this->provider = $this->resolveProvider();
45    }
46
47    /**
48     * Resolve which schedule provider to use
49     *
50     * Priority order:
51     * 1. Explicit schedulingProvider setting (buyerkiosk, wheniwork, homebase)
52     * 2. Legacy detection (WhenIWork enabled, Homebase enabled)
53     *
54     * @return ScheduleProvider|null Active provider or null if none enabled
55     */
56    public function resolveProvider(): ?ScheduleProvider
57    {
58        // Check explicit schedulingProvider setting first
59        $explicitProvider = $this->store->getSchedulingProvider();
60
61        if ($explicitProvider === 'buyerkiosk') {
62            $bksProvider = new BuyerKioskSchedule($this->store);
63            if ($bksProvider->isEnabled()) {
64                return $bksProvider;
65            }
66        }
67
68        if ($explicitProvider === 'wheniwork') {
69            $wiwProvider = new WhenIWorkSchedule($this->store);
70            if ($wiwProvider->isEnabled()) {
71                return $wiwProvider;
72            }
73        }
74
75        if ($explicitProvider === 'homebase') {
76            $homebaseProvider = new HomebaseSchedule($this->store);
77            if ($homebaseProvider->isEnabled()) {
78                return $homebaseProvider;
79            }
80        }
81
82        // Fall back to legacy detection for stores without explicit setting
83        // Try WhenIWork first (legacy behavior)
84        $wiwProvider = new WhenIWorkSchedule($this->store);
85        if ($wiwProvider->isEnabled()) {
86            return $wiwProvider;
87        }
88
89        // Try Homebase
90        $homebaseProvider = new HomebaseSchedule($this->store);
91        if ($homebaseProvider->isEnabled()) {
92            return $homebaseProvider;
93        }
94
95        // No provider enabled
96        return null;
97    }
98
99    /**
100     * Get today's schedule in store timezone
101     *
102     * @return array Schedule data with metadata
103     */
104    public function getTodaySchedule(): array
105    {
106        $date = new \DateTime('now', new \DateTimeZone($this->store->getTimeZone()));
107        return $this->getScheduleForDate($date);
108    }
109
110    /**
111     * Get schedule for a specific date
112     *
113     * @param \DateTime $date Date to retrieve schedule for
114     * @return array Schedule data with enabled status, provider, date, and shifts
115     */
116    public function getScheduleForDate(\DateTime $date): array
117    {
118        $response = [
119            'enabled' => false,
120            'provider' => null,
121            'date' => $date->format('Y-m-d'),
122            'shifts' => []
123        ];
124
125        if (!$this->provider) {
126            return $response;
127        }
128
129        try {
130            $shifts = $this->provider->getScheduleForDate($date);
131            $enrichedShifts = $this->enrichWithClockStatus($shifts);
132
133            $response['enabled'] = true;
134            $response['provider'] = $this->provider->getProviderName();
135            $response['shifts'] = $enrichedShifts;
136
137            return $response;
138        } catch (Exception $e) {
139            error_log("ScheduleManager::getScheduleForDate error: " . $e->getMessage());
140            return $response;
141        }
142    }
143
144    /**
145     * Refresh schedule data (clear cache and re-sync)
146     *
147     * @return array Result with success status and message
148     */
149    public function refreshSchedule(): array
150    {
151        if (!$this->provider) {
152            return [
153                'success' => false,
154                'message' => 'No schedule provider enabled'
155            ];
156        }
157
158        try {
159            $date = new \DateTime('now', new \DateTimeZone($this->store->getTimeZone()));
160
161            // Clear cache by syncing fresh data
162            $success = $this->provider->syncScheduleToCache($date);
163
164            if ($success) {
165                return [
166                    'success' => true,
167                    'message' => 'Schedule refreshed successfully',
168                    'provider' => $this->provider->getProviderName()
169                ];
170            } else {
171                return [
172                    'success' => false,
173                    'message' => 'Failed to refresh schedule'
174                ];
175            }
176        } catch (Exception $e) {
177            error_log("ScheduleManager::refreshSchedule error: " . $e->getMessage());
178            return [
179                'success' => false,
180                'message' => 'Error refreshing schedule: ' . $e->getMessage()
181            ];
182        }
183    }
184
185    /**
186     * Enrich shifts with clock-in status
187     * Adds 'isClockedIn' flag to each shift
188     *
189     * @param array $shifts Array of shift data
190     * @return array Enriched shifts with clock status
191     */
192    public function enrichWithClockStatus(array $shifts): array
193    {
194        if (empty($shifts)) {
195            return $shifts;
196        }
197
198        try {
199            // Get clocked-in employee IDs from FinancialsController
200            $clockedInIds = $this->getClockedInEmployeeIds();
201
202            // Map employee IDs to WiW user IDs for lookup
203            foreach ($shifts as &$shift) {
204                $shift['isClockedIn'] = in_array($shift['employeeId'], $clockedInIds);
205            }
206
207            return $shifts;
208        } catch (Exception $e) {
209            error_log("ScheduleManager::enrichWithClockStatus error: " . $e->getMessage());
210            // Return shifts without clock status on error
211            foreach ($shifts as &$shift) {
212                $shift['isClockedIn'] = false;
213            }
214            return $shifts;
215        }
216    }
217
218    /**
219     * Get array of employee IDs that are currently clocked in
220     *
221     * For BuyerKiosk provider: queries scheduleTimePunches table directly
222     * For WhenIWork provider: maps WiW user IDs to store employees.employeeID
223     *   using a single IN query (handles both externalId and legacy employeeID matching)
224     *
225     * @return array Array of store employee IDs (employees.employeeID)
226     */
227    private function getClockedInEmployeeIds(): array
228    {
229        if (!$this->provider) {
230            return [];
231        }
232
233        // Handle BuyerKiosk provider - query native time punches
234        if ($this->provider->getProviderName() === 'buyerkiosk') {
235            return $this->getBuyerKioskClockedInIds();
236        }
237
238        // Handle WhenIWork provider
239        if ($this->provider->getProviderName() !== 'wheniwork') {
240            return [];
241        }
242
243        try {
244            $fc = new FinancialsController($this->app, $this->store);
245            $laborData = $fc->getLaborTotals();
246
247            if (!isset($laborData['employeeData']) || !is_array($laborData['employeeData'])) {
248                return [];
249            }
250
251            // employeeData keys are WiW user IDs
252            // Shifts contain employees.employeeID, so we need to map WiW IDs to store employee IDs
253            $wiwUserIds = array_keys($laborData['employeeData']);
254
255            if (empty($wiwUserIds)) {
256                return [];
257            }
258
259            $storeDb = \dbConnectByName($this->store->getDbName());
260
261            // Build placeholders for IN clause
262            $placeholders = implode(',', array_fill(0, count($wiwUserIds), '?'));
263
264            // Query store employees table - match by externalId (WiW user ID) or employeeID (legacy)
265            $stmt = $storeDb->prepare("
266                SELECT employeeID
267                FROM employees
268                WHERE active = 1
269                  AND (
270                    (source = 'wheniwork' AND externalId IN ($placeholders))
271                    OR employeeID IN ($placeholders)
272                  )
273            ");
274
275            // Bind WiW user IDs twice (once for externalId, once for employeeID legacy lookup)
276            $params = array_merge(
277                array_map('strval', $wiwUserIds),
278                array_map('intval', $wiwUserIds)
279            );
280            $stmt->execute($params);
281
282            $employeeIds = [];
283            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
284                $employeeIds[] = (int)$row['employeeID'];
285            }
286
287            return $employeeIds;
288        } catch (Exception $e) {
289            error_log("ScheduleManager::getClockedInEmployeeIds error: " . $e->getMessage());
290            return [];
291        }
292    }
293
294    /**
295     * Get clocked-in employee IDs for BuyerKiosk provider
296     *
297     * Queries the scheduleTimePunches table for employees with active clock-in sessions
298     * (clockIn is set, clockOut is null).
299     *
300     * @return array Array of employee IDs
301     */
302    private function getBuyerKioskClockedInIds(): array
303    {
304        try {
305            $storeDb = \dbConnectByName($this->store->getDbName());
306
307            // Find employees who are currently clocked in (have a punch with no clock-out)
308            $stmt = $storeDb->prepare("
309                SELECT DISTINCT employeeId
310                FROM scheduleTimePunches
311                WHERE clockIn IS NOT NULL
312                  AND clockOut IS NULL
313                  AND deleted_at IS NULL
314            ");
315            $stmt->execute();
316
317            $employeeIds = [];
318            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
319                $employeeIds[] = (int)$row['employeeId'];
320            }
321
322            return $employeeIds;
323        } catch (Exception $e) {
324            error_log("ScheduleManager::getBuyerKioskClockedInIds error: " . $e->getMessage());
325            return [];
326        }
327    }
328
329    /**
330     * Get the active provider instance
331     *
332     * @return ScheduleProvider|null
333     */
334    public function getProvider(): ?ScheduleProvider
335    {
336        return $this->provider;
337    }
338
339    /**
340     * Check if any schedule provider is enabled
341     *
342     * @return bool
343     */
344    public function isEnabled(): bool
345    {
346        return $this->provider !== null;
347    }
348}