Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
WhenIWorkSchedule
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 11
930
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
6
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProviderName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getScheduleForDate
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getEmployeeShift
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 syncScheduleToCache
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 fetchShiftsFromAPI
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 formatShifts
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getLocalEmployee
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 formatDateTime
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 formatTimeOnly
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\Workbook;
4
5use Exception;
6
7/**
8 * WhenIWork Schedule Provider
9 *
10 * Retrieves and manages employee schedules from WhenIWork API.
11 * Caches data in Redis and syncs to database for persistence.
12 *
13 * @package BuyerKiosk\Workbook
14 */
15class WhenIWorkSchedule extends ScheduleProvider
16{
17    /**
18     * @var \Wheniwork WhenIWork API client
19     */
20    private $wiw;
21
22    /**
23     * Constructor
24     *
25     * @param \Store $store Store instance
26     */
27    public function __construct(\Store $store)
28    {
29        parent::__construct($store);
30        if ($this->isEnabled()) {
31            $this->wiw = new \Wheniwork($this->store->getWiwToken());
32        }
33    }
34
35    /**
36     * Check if WhenIWork is enabled for this store
37     *
38     * @return bool True if WhenIWork is enabled
39     */
40    public function isEnabled(): bool
41    {
42        return $this->store->getWiwEnable() > 0;
43    }
44
45    /**
46     * Get provider name
47     *
48     * @return string Provider identifier
49     */
50    public function getProviderName(): string
51    {
52        return 'wheniwork';
53    }
54
55    /**
56     * Get schedule for a specific date
57     *
58     * @param \DateTime $date Date to retrieve schedule for
59     * @return array Array of formatted shift data
60     */
61    public function getScheduleForDate(\DateTime $date): array
62    {
63        if (!$this->isEnabled()) {
64            return [];
65        }
66
67        // Check cache first
68        $cached = $this->getFromCache($date);
69        if ($cached !== null) {
70            return $cached;
71        }
72
73        try {
74            // Fetch from WhenIWork API
75            $shifts = $this->fetchShiftsFromAPI($date);
76            $formatted = $this->formatShifts($shifts);
77
78            // Save to cache
79            $this->saveToCache($date, $formatted);
80
81            return $formatted;
82        } catch (Exception $e) {
83            error_log("WhenIWorkSchedule::getScheduleForDate error: " . $e->getMessage());
84            return [];
85        }
86    }
87
88    /**
89     * Get a specific employee's shift for a date
90     *
91     * @param int $employeeId Local employee ID
92     * @param \DateTime $date Date to check
93     * @return array|null Shift data or null if not scheduled
94     */
95    public function getEmployeeShift(int $employeeId, \DateTime $date): ?array
96    {
97        $schedule = $this->getScheduleForDate($date);
98
99        foreach ($schedule as $shift) {
100            if ($shift['employeeId'] == $employeeId) {
101                return $shift;
102            }
103        }
104
105        return null;
106    }
107
108    /**
109     * Sync schedule data to database cache
110     *
111     * @param \DateTime $date Date to sync
112     * @return bool Success status
113     */
114    public function syncScheduleToCache(\DateTime $date): bool
115    {
116        if (!$this->isEnabled()) {
117            return false;
118        }
119
120        try {
121            // Get fresh data from API
122            $shifts = $this->fetchShiftsFromAPI($date);
123            $formatted = $this->formatShifts($shifts);
124
125            // Clear existing cache entries for this date
126            $stmt = $this->getDb()->prepare("
127                DELETE FROM workbook_schedule_cache
128                WHERE date = :date AND provider = 'wheniwork'
129            ");
130            $stmt->bindValue(':date', $date->format('Y-m-d'));
131            $stmt->execute();
132
133            // Insert new entries
134            $insertStmt = $this->getDb()->prepare("
135                INSERT INTO workbook_schedule_cache
136                (date, employeeId, providerEmployeeId, provider, shiftStart, shiftEnd, position, notes)
137                VALUES (:date, :employeeId, :providerEmployeeId, :provider, :shiftStart, :shiftEnd, :position, :notes)
138            ");
139
140            foreach ($formatted as $shift) {
141                $insertStmt->bindValue(':date', $date->format('Y-m-d'));
142                $insertStmt->bindValue(':employeeId', $shift['employeeId']);
143                $insertStmt->bindValue(':providerEmployeeId', $shift['providerEmployeeId']);
144                $insertStmt->bindValue(':provider', 'wheniwork');
145                $insertStmt->bindValue(':shiftStart', $shift['shiftStart']);
146                $insertStmt->bindValue(':shiftEnd', $shift['shiftEnd']);
147                $insertStmt->bindValue(':position', $shift['position'] ?? null);
148                $insertStmt->bindValue(':notes', $shift['notes'] ?? null);
149                $insertStmt->execute();
150            }
151
152            // Update Redis cache
153            $this->saveToCache($date, $formatted);
154
155            return true;
156        } catch (Exception $e) {
157            error_log("WhenIWorkSchedule::syncScheduleToCache error: " . $e->getMessage());
158            return false;
159        }
160    }
161
162    /**
163     * Fetch shifts from WhenIWork API
164     *
165     * @param \DateTime $date Date to fetch shifts for
166     * @return array Raw shift data from API
167     * @throws Exception If API call fails
168     */
169    private function fetchShiftsFromAPI(\DateTime $date): array
170    {
171        // Set up date range for the entire day
172        $start = clone $date;
173        $start->setTime(0, 0, 0);
174
175        $end = clone $date;
176        $end->setTime(23, 59, 59);
177
178        /** @var object|false $result */
179        $result = $this->wiw->get("shifts", [
180            "location_id" => $this->store->getWiwLocationID(),
181            "start" => $start->format("Y-m-d H:i:s"),
182            "end" => $end->format("Y-m-d H:i:s")
183        ]);
184
185        if (!$result || !is_object($result) || !isset($result->shifts)) {
186            throw new Exception("Invalid response from WhenIWork API");
187        }
188
189        // WhenIWork API returns shifts as array within object
190        return is_array($result->shifts) ? $result->shifts : [];
191    }
192
193    /**
194     * Format shifts from WhenIWork API format to our internal format
195     * Maps WhenIWork user IDs to local employee IDs
196     *
197     * @param array $shifts Raw shifts from API
198     * @return array Formatted shifts with local employee IDs and names
199     */
200    private function formatShifts(array $shifts): array
201    {
202        $formatted = [];
203
204        foreach ($shifts as $shift) {
205            // Look up local employee by WhenIWork user ID
206            $employee = $this->getLocalEmployee($shift->user_id);
207
208            if (!$employee) {
209                continue;
210            }
211
212            $formatted[] = [
213                'employeeId' => $employee['employeeID'],
214                'providerEmployeeId' => (string)$shift->user_id,
215                'firstName' => $employee['employeeFirstName'],
216                'lastName' => $employee['employeeLastName'],
217                'shiftStart' => $this->formatDateTime($shift->start_time),
218                'shiftEnd' => $this->formatDateTime($shift->end_time),
219                'startTime' => $this->formatTimeOnly($shift->start_time),
220                'endTime' => $this->formatTimeOnly($shift->end_time),
221                'position' => $shift->position_name ?? null,
222                'notes' => $shift->notes ?? null,
223                'provider' => 'wheniwork'
224            ];
225        }
226
227        return $formatted;
228    }
229
230    /**
231     * Get local employee data from WhenIWork user ID
232     *
233     * @param int $wiwUserId WhenIWork user ID
234     * @return array|null Employee data or null if not found
235     */
236    private function getLocalEmployee(int $wiwUserId): ?array
237    {
238        // Try new employee system first (externalId field)
239        $stmt = $this->getDb()->prepare("
240            SELECT employeeID, employeeFirstName, employeeLastName, externalId FROM employees
241            WHERE source = 'wheniwork'
242            AND externalId = :wiwUserId
243            AND active = 1
244            LIMIT 1
245        ");
246        $stmt->bindValue(':wiwUserId', (string)$wiwUserId);
247        $stmt->execute();
248        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
249
250        if ($result) {
251            return $result;
252        }
253
254        // Fall back to legacy employeeID column (where employeeID IS the WiW user ID)
255        $stmt = $this->getDb()->prepare("
256            SELECT employeeID, employeeFirstName, employeeLastName FROM employees
257            WHERE employeeID = :wiwUserId
258            AND active = 1
259            LIMIT 1
260        ");
261        $stmt->bindValue(':wiwUserId', $wiwUserId, \PDO::PARAM_INT);
262        $stmt->execute();
263        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
264
265        return $result ?: null;
266    }
267
268    /**
269     * Format WhenIWork datetime string to MySQL datetime format
270     *
271     * @param string $wiwDateTime WhenIWork datetime string
272     * @return string MySQL datetime format
273     */
274    private function formatDateTime(string $wiwDateTime): string
275    {
276        // WhenIWork format: "Day, DD Mon YYYY HH:MM:SS O"
277        $dt = \DateTime::createFromFormat("D, d M Y H:i:s O", $wiwDateTime);
278
279        if (!$dt) {
280            error_log("WhenIWorkSchedule: Failed to parse datetime: " . $wiwDateTime);
281            return date('Y-m-d H:i:s');
282        }
283
284        return $dt->format('Y-m-d H:i:s');
285    }
286
287    /**
288     * Format WhenIWork datetime string to time-only format for display
289     *
290     * @param string $wiwDateTime WhenIWork datetime string
291     * @return string Time in h:ia format (e.g., "9:00am")
292     */
293    private function formatTimeOnly(string $wiwDateTime): string
294    {
295        // WhenIWork format: "Day, DD Mon YYYY HH:MM:SS O"
296        $dt = \DateTime::createFromFormat("D, d M Y H:i:s O", $wiwDateTime);
297
298        if (!$dt) {
299            return '';
300        }
301
302        return $dt->format('g:ia');
303    }
304}