Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
EmployeeManager
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 15
1406
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
 loadStore
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 resolveProvider
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 getActiveEmployees
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmployee
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createEmployee
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 updateEmployee
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deactivateEmployee
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 syncFromProvider
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getEmployeesForSelector
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 setEmployeePin
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
110
 setEmployeePhoto
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getEmployeePhotoUrl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 clearEmployeePhoto
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 setUseUnifiedUsers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BuyerKiosk\Employee;
4
5use BuyerKiosk\StoreController;
6use BuyerKiosk\Auth\Models\StoreAssignment;
7use PDO;
8use Exception;
9
10/**
11 * Central manager for employee operations
12 *
13 * Routes to appropriate provider based on store configuration.
14 * This facade provides a unified interface for employee management
15 * regardless of whether employees are managed internally (homegrown),
16 * synced from WhenIWork, or synced from Homebase.
17 *
18 * @package BuyerKiosk\Employee
19 */
20class EmployeeManager
21{
22    /**
23     * @var PDO Database connection to store database
24     */
25    private $db;
26
27    /**
28     * @var string Store identifier (e.g., 'ou00', 'pa00')
29     */
30    private $typeNum;
31
32    /**
33     * @var \Store Store configuration object
34     */
35    private $store;
36
37    /**
38     * @var EmployeeProviderInterface Resolved employee provider
39     */
40    private $provider;
41
42    /**
43     * @var PDO Central database connection (kiosk_users)
44     */
45    private $centralDb;
46
47    /**
48     * @var bool Whether to use unified users table
49     */
50    private $useUnifiedUsers = true;
51
52    /**
53     * Initialize the EmployeeManager for a specific store
54     *
55     * @param string $typeNum Store identifier
56     * @throws Exception If store cannot be loaded
57     */
58    public function __construct(string $typeNum)
59    {
60        $this->typeNum = $typeNum;
61        $this->store = $this->loadStore($typeNum);
62        $this->db = dbConnectByName($this->store->getDbName());
63        $this->centralDb = dbConnectByName('kiosk_users');
64        $this->provider = $this->resolveProvider();
65    }
66
67    /**
68     * Load store configuration
69     *
70     * @param string $typeNum Store identifier
71     * @return \Store Store object
72     * @throws Exception If store cannot be loaded
73     */
74    private function loadStore(string $typeNum)
75    {
76        $storeController = new StoreController($typeNum);
77        $store = $storeController->getStore();
78
79        if (!$store) {
80            throw new Exception("Store not found: {$typeNum}");
81        }
82
83        return $store;
84    }
85
86    /**
87     * Resolve the appropriate provider based on store configuration
88     *
89     * @return EmployeeProviderInterface The resolved provider
90     */
91    private function resolveProvider(): EmployeeProviderInterface
92    {
93        // Check if store has getEmployeeSource method, otherwise default to homegrown
94        $source = method_exists($this->store, 'getEmployeeSource')
95            ? $this->store->getEmployeeSource()
96            : 'homegrown';
97
98        // If null or empty, default to homegrown
99        if (empty($source)) {
100            $source = 'homegrown';
101        }
102
103        switch ($source) {
104            case 'wheniwork':
105                return new WhenIWorkProvider($this->store, $this->db);
106
107            case 'homebase':
108                return new HomebaseProvider($this->store, $this->db);
109
110            default:
111                return new HomegrownProvider($this->db);
112        }
113    }
114
115    /**
116     * Get all active employees
117     *
118     * @return Employee[] Array of active Employee objects
119     */
120    public function getActiveEmployees(): array
121    {
122        return $this->provider->getActiveEmployees();
123    }
124
125    /**
126     * Get single employee by ID
127     *
128     * @param int $employeeId Employee ID
129     * @return Employee|null Employee object or null if not found
130     */
131    public function getEmployee(int $employeeId): ?Employee
132    {
133        return $this->provider->getEmployee($employeeId);
134    }
135
136    /**
137     * Create new employee
138     *
139     * Only supported for homegrown provider. External providers (WhenIWork, Homebase)
140     * require employees to be created in their systems and then synced.
141     *
142     * @param array $data Employee data
143     * @return Employee Created employee object
144     * @throws Exception If current provider does not support creation
145     */
146    public function createEmployee(array $data): Employee
147    {
148        if (!$this->provider->supportsCreate()) {
149            throw new Exception('Cannot create employees with current provider. Use external system.');
150        }
151
152        return $this->provider->createEmployee($data);
153    }
154
155    /**
156     * Update employee data
157     *
158     * For external providers, only local-only fields can be updated
159     * (e.g., emergency contact, DRS employee ID, daily email preferences).
160     * Fields synced from external systems will be overwritten on next sync.
161     *
162     * @param int $employeeId Employee ID
163     * @param array $data Fields to update
164     * @return Employee Updated employee object
165     */
166    public function updateEmployee(int $employeeId, array $data): Employee
167    {
168        return $this->provider->updateEmployee($employeeId, $data);
169    }
170
171    /**
172     * Deactivate employee
173     *
174     * For external providers, this only deactivates locally and may be
175     * reconciled on the next sync.
176     *
177     * @param int $employeeId Employee ID
178     * @param string|null $reason Optional reason for deactivation
179     * @return bool Success status
180     */
181    public function deactivateEmployee(int $employeeId, ?string $reason = null): bool
182    {
183        return $this->provider->deactivateEmployee($employeeId, $reason);
184    }
185
186    /**
187     * Sync employees from external provider
188     *
189     * Only supported for WhenIWork and Homebase providers.
190     * Fetches current employee list from external API and updates local database.
191     *
192     * @return SyncResult Sync operation results
193     * @throws Exception If current provider does not support sync
194     */
195    public function syncFromProvider(): SyncResult
196    {
197        if (!$this->provider->supportsSync()) {
198            throw new Exception('Current provider does not support sync');
199        }
200
201        return $this->provider->syncEmployees();
202    }
203
204    /**
205     * Get employees formatted for UI selection dropdowns
206     *
207     * Returns a simplified array format suitable for use in HTML select elements,
208     * Handlebars templates, and other UI components.
209     *
210     * @return array[] Array of employee data formatted for selectors
211     */
212    public function getEmployeesForSelector(): array
213    {
214        $employees = $this->getActiveEmployees();
215
216        return array_map(function($emp) {
217            return [
218                'id' => $emp->getId(),
219                'firstName' => $emp->getFirstName(),
220                'lastName' => $emp->getLastName(),
221                'fullName' => $emp->getFullName(),
222                'photoUrl' => $emp->getPhotoUrl(),
223                'position' => $emp->getPosition(),
224            ];
225        }, $employees);
226    }
227
228    /**
229     * Set or update employee clock PIN for time punch kiosk
230     *
231     * @param int $employeeId Employee ID (user ID in unified system)
232     * @param string|null $pin PIN to set (4-6 digits) or null to remove
233     * @return bool Success status
234     * @throws Exception If employee not found or invalid PIN
235     */
236    public function setEmployeePin(int $employeeId, ?string $pin): bool
237    {
238        // Verify employee exists
239        $employee = $this->getEmployee($employeeId);
240        if (!$employee) {
241            throw new Exception("Employee not found: {$employeeId}");
242        }
243
244        // Validate PIN format if provided
245        if ($pin !== null && $pin !== '') {
246            if (!preg_match('/^\d{4,6}$/', $pin)) {
247                throw new Exception("PIN must be 4-6 digits");
248            }
249        }
250
251        if ($this->useUnifiedUsers) {
252            // Update PIN in userStoreAssignments (unified system)
253            $assignment = StoreAssignment::findByUserAndStore($this->centralDb, $employeeId, $this->typeNum);
254
255            if (!$assignment) {
256                // Create assignment if it doesn't exist
257                $assignment = StoreAssignment::create($this->centralDb, $employeeId, $this->typeNum, [
258                    'clockPin' => $pin ?: null
259                ]);
260                return true;
261            }
262
263            $assignment->setClockPin($pin ?: null);
264            return $assignment->save();
265        }
266
267        // Legacy: Update PIN in store employees table
268        $stmt = $this->db->prepare("
269            UPDATE employees
270            SET clockPin = :pin,
271                updatedAt = NOW()
272            WHERE employeeID = :id
273        ");
274
275        return $stmt->execute([
276            ':pin' => $pin ?: null,
277            ':id' => $employeeId
278        ]);
279    }
280
281    /**
282     * Upload and set custom photo for employee
283     *
284     * @param int $userId User ID
285     * @param string $localUrl Local URL to the photo
286     * @return bool Success status
287     */
288    public function setEmployeePhoto(int $userId, string $localUrl): bool
289    {
290        if ($this->useUnifiedUsers) {
291            $stmt = $this->centralDb->prepare("
292                UPDATE users
293                SET photoUrl = :photoUrl, avatarOverride = 1, updatedAt = NOW()
294                WHERE id = :id
295            ");
296            return $stmt->execute([
297                'photoUrl' => $localUrl,
298                'id' => $userId
299            ]);
300        }
301
302        // Legacy
303        $stmt = $this->db->prepare("
304            UPDATE employees
305            SET photoUrl = :photoUrl, avatarOverride = 1
306            WHERE employeeID = :id
307        ");
308        return $stmt->execute([
309            ':photoUrl' => $localUrl,
310            ':id' => $userId
311        ]);
312    }
313
314    /**
315     * Get current photo URL for employee
316     *
317     * @param int $userId User ID
318     * @return string|null Photo URL or null
319     */
320    public function getEmployeePhotoUrl(int $userId): ?string
321    {
322        if ($this->useUnifiedUsers) {
323            $stmt = $this->centralDb->prepare("SELECT photoUrl FROM users WHERE id = :id");
324            $stmt->execute(['id' => $userId]);
325            $row = $stmt->fetch(PDO::FETCH_ASSOC);
326            return $row ? $row['photoUrl'] : null;
327        }
328
329        // Legacy
330        $stmt = $this->db->prepare("SELECT photoUrl FROM employees WHERE employeeID = :id");
331        $stmt->execute([':id' => $userId]);
332        $row = $stmt->fetch(PDO::FETCH_ASSOC);
333        return $row ? $row['photoUrl'] : null;
334    }
335
336    /**
337     * Clear custom photo for employee (allows next sync to pull fresh)
338     *
339     * @param int $userId User ID
340     * @return bool Success status
341     */
342    public function clearEmployeePhoto(int $userId): bool
343    {
344        if ($this->useUnifiedUsers) {
345            $stmt = $this->centralDb->prepare("
346                UPDATE users
347                SET photoUrl = NULL, avatarOverride = 0, updatedAt = NOW()
348                WHERE id = :id
349            ");
350            return $stmt->execute(['id' => $userId]);
351        }
352
353        // Legacy
354        $stmt = $this->db->prepare("
355            UPDATE employees
356            SET photoUrl = NULL, avatarOverride = 0
357            WHERE employeeID = :id
358        ");
359        return $stmt->execute([':id' => $userId]);
360    }
361
362    /**
363     * Enable or disable unified users mode
364     *
365     * @param bool $enabled True to use unified users table
366     */
367    public function setUseUnifiedUsers(bool $enabled): void
368    {
369        $this->useUnifiedUsers = $enabled;
370    }
371}