Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 299
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserEmployeeLinkController
0.00% covered (danger)
0.00%
0 / 299
0.00% covered (danger)
0.00%
0 / 9
2550
0.00% covered (danger)
0.00%
0 / 1
 getMyEmployee
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 getAllLinks
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 getUnlinkedEmployees
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 getUnlinkedUsers
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 createLink
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
110
 removeLink
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
42
 triggerAutoLink
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
42
 promoteEmployee
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
72
 getEmployeeLinkStatus
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace BuyerKiosk\Core\Controllers;
4
5use BuyerKiosk\UserEmployee\UserEmployeeLinkManager;
6use BuyerKiosk\UserEmployee\UserEmployeePromotion;
7use BuyerKiosk\UserEmployee\EmployeeInvitationManager;
8use Exception;
9
10/**
11 * User-Employee Link API Controller
12 *
13 * Handles REST API endpoints for managing links between uf_user accounts
14 * and employee records, as well as employee promotion to user accounts.
15 *
16 * @package UserFrosting
17 */
18class UserEmployeeLinkController extends BaseController
19{
20    /**
21     * GET /:typeNum/api/user-employee/my-employee
22     * Get linked employee for current user
23     *
24     * @param string $typeNum Store identifier
25     */
26    public function getMyEmployee($typeNum)
27    {
28        // Check store access permission
29        if (!$this->_app->user->checkStoreGroup($typeNum)) {
30            $this->_app->response->setStatus(403);
31            $this->_app->response->headers->set('Content-Type', 'application/json');
32            $this->_app->response->setBody(json_encode([
33                'success' => false,
34                'error' => 'Access denied to this store'
35            ]));
36            return;
37        }
38
39        try {
40            $centralDb = dbConnectByName('kiosk_users');
41            $linkManager = new UserEmployeeLinkManager($centralDb);
42
43            $employee = $linkManager->getLinkedEmployee($this->_app->user->id, $typeNum);
44
45            $this->_app->response->headers->set('Content-Type', 'application/json');
46            $this->_app->response->setBody(json_encode([
47                'success' => true,
48                'employee' => $employee
49            ]));
50        } catch (Exception $e) {
51            error_log("UserEmployeeLinkController::getMyEmployee error: " . $e->getMessage());
52            $this->_app->response->setStatus(500);
53            $this->_app->response->headers->set('Content-Type', 'application/json');
54            $this->_app->response->setBody(json_encode([
55                'success' => false,
56                'error' => 'Failed to retrieve linked employee'
57            ]));
58        }
59    }
60
61    /**
62     * GET /:typeNum/api/user-employee/links
63     * Get all links for this store (admin only)
64     *
65     * @param string $typeNum Store identifier
66     */
67    public function getAllLinks($typeNum)
68    {
69        // Check store access and employees permission
70        if (!$this->_app->user->checkStoreGroup($typeNum) || !$this->_app->user->checkAccess('uri_employees')) {
71            $this->_app->response->setStatus(403);
72            $this->_app->response->headers->set('Content-Type', 'application/json');
73            $this->_app->response->setBody(json_encode([
74                'success' => false,
75                'error' => 'Access denied'
76            ]));
77            return;
78        }
79
80        try {
81            $centralDb = dbConnectByName('kiosk_users');
82            $linkManager = new UserEmployeeLinkManager($centralDb);
83
84            $links = $linkManager->getLinksForStore($typeNum);
85
86            // Convert to array format
87            $linkData = array_map(function($link) {
88                return $link->toArray();
89            }, $links);
90
91            $this->_app->response->headers->set('Content-Type', 'application/json');
92            $this->_app->response->setBody(json_encode([
93                'success' => true,
94                'links' => $linkData
95            ]));
96        } catch (Exception $e) {
97            error_log("UserEmployeeLinkController::getAllLinks error: " . $e->getMessage());
98            $this->_app->response->setStatus(500);
99            $this->_app->response->headers->set('Content-Type', 'application/json');
100            $this->_app->response->setBody(json_encode([
101                'success' => false,
102                'error' => 'Failed to retrieve links'
103            ]));
104        }
105    }
106
107    /**
108     * GET /:typeNum/api/user-employee/unlinked-employees
109     * Get employees without linked user accounts
110     *
111     * @param string $typeNum Store identifier
112     */
113    public function getUnlinkedEmployees($typeNum)
114    {
115        // Check store access and employees permission
116        if (!$this->_app->user->checkStoreGroup($typeNum) || !$this->_app->user->checkAccess('uri_employees')) {
117            $this->_app->halt(403, json_encode([
118                'success' => false,
119                'error' => 'Access denied'
120            ]));
121        }
122
123        try {
124            $centralDb = dbConnectByName('kiosk_users');
125            $linkManager = new UserEmployeeLinkManager($centralDb);
126
127            $employees = $linkManager->getUnlinkedEmployees($typeNum);
128
129            $this->_app->response->headers->set('Content-Type', 'application/json');
130            $this->_app->response->setBody(json_encode([
131                'success' => true,
132                'employees' => $employees
133            ]));
134        } catch (Exception $e) {
135            error_log("UserEmployeeLinkController::getUnlinkedEmployees error: " . $e->getMessage());
136            error_log("UserEmployeeLinkController::getUnlinkedEmployees trace: " . $e->getTraceAsString());
137            $this->_app->response->setStatus(500);
138            $this->_app->response->headers->set('Content-Type', 'application/json');
139            $this->_app->response->setBody(json_encode([
140                'success' => false,
141                'error' => 'Failed to retrieve unlinked employees: ' . $e->getMessage()
142            ]));
143        }
144    }
145
146    /**
147     * GET /:typeNum/api/user-employee/unlinked-users
148     * Get users without linked employee records
149     *
150     * @param string $typeNum Store identifier
151     */
152    public function getUnlinkedUsers($typeNum)
153    {
154        // Check store access and admin permissions
155        if (!$this->_app->user->checkStoreGroup($typeNum) || !$this->_app->user->checkAccess('uri_user_admin')) {
156            $this->_app->response->setStatus(403);
157            $this->_app->response->headers->set('Content-Type', 'application/json');
158            $this->_app->response->setBody(json_encode([
159                'success' => false,
160                'error' => 'Access denied'
161            ]));
162            return;
163        }
164
165        try {
166            $centralDb = dbConnectByName('kiosk_users');
167            $linkManager = new UserEmployeeLinkManager($centralDb);
168
169            $users = $linkManager->getUnlinkedUsers($typeNum);
170
171            $this->_app->response->headers->set('Content-Type', 'application/json');
172            $this->_app->response->setBody(json_encode([
173                'success' => true,
174                'users' => $users
175            ]));
176        } catch (Exception $e) {
177            error_log("UserEmployeeLinkController::getUnlinkedUsers error: " . $e->getMessage());
178            $this->_app->response->setStatus(500);
179            $this->_app->response->headers->set('Content-Type', 'application/json');
180            $this->_app->response->setBody(json_encode([
181                'success' => false,
182                'error' => 'Failed to retrieve unlinked users'
183            ]));
184        }
185    }
186
187    /**
188     * POST /:typeNum/api/user-employee/links
189     * Create manual link between user and employee
190     *
191     * @param string $typeNum Store identifier
192     */
193    public function createLink($typeNum)
194    {
195        // Check permissions
196        if (!$this->_app->user->checkStoreGroup($typeNum) ||
197            !$this->_app->user->checkAccess('uri_employees') ||
198            !$this->_app->user->checkAccess('uri_user_admin')) {
199            $this->_app->response->setStatus(403);
200            $this->_app->response->headers->set('Content-Type', 'application/json');
201            $this->_app->response->setBody(json_encode([
202                'success' => false,
203                'error' => 'Access denied'
204            ]));
205            return;
206        }
207
208        // Check CSRF token
209        if (!\NoCSRF::check('csrf_token', $this->_app->request->post())) {
210            $this->_app->response->setStatus(403);
211            $this->_app->response->headers->set('Content-Type', 'application/json');
212            $this->_app->response->setBody(json_encode([
213                'success' => false,
214                'error' => 'Invalid or missing CSRF token'
215            ]));
216            return;
217        }
218
219        try {
220            $data = $this->_app->request->post();
221            $userId = isset($data['userId']) ? (int) $data['userId'] : null;
222            $employeeId = isset($data['employeeId']) ? (int) $data['employeeId'] : null;
223
224            if (!$userId || !$employeeId) {
225                throw new Exception("Both userId and employeeId are required");
226            }
227
228            $centralDb = dbConnectByName('kiosk_users');
229            $linkManager = new UserEmployeeLinkManager($centralDb);
230
231            $linkId = $linkManager->createLink(
232                $userId,
233                $typeNum,
234                $employeeId,
235                'manual',
236                $this->_app->user->id
237            );
238
239            $this->_app->response->setStatus(201);
240            $this->_app->response->headers->set('Content-Type', 'application/json');
241            $this->_app->response->setBody(json_encode([
242                'success' => true,
243                'linkId' => $linkId
244            ]));
245        } catch (Exception $e) {
246            error_log("UserEmployeeLinkController::createLink error: " . $e->getMessage());
247            $this->_app->response->setStatus(400);
248            $this->_app->response->headers->set('Content-Type', 'application/json');
249            $this->_app->response->setBody(json_encode([
250                'success' => false,
251                'error' => $e->getMessage()
252            ]));
253        }
254    }
255
256    /**
257     * DELETE /:typeNum/api/user-employee/links/:linkId
258     * Remove link between user and employee
259     *
260     * @param string $typeNum Store identifier
261     * @param int $linkId Link ID
262     */
263    public function removeLink($typeNum, $linkId)
264    {
265        // Check permissions
266        if (!$this->_app->user->checkStoreGroup($typeNum) ||
267            !$this->_app->user->checkAccess('uri_employees') ||
268            !$this->_app->user->checkAccess('uri_user_admin')) {
269            $this->_app->response->setStatus(403);
270            $this->_app->response->headers->set('Content-Type', 'application/json');
271            $this->_app->response->setBody(json_encode([
272                'success' => false,
273                'error' => 'Access denied'
274            ]));
275            return;
276        }
277
278        try {
279            // Get the link to find userId
280            $centralDb = dbConnectByName('kiosk_users');
281            $stmt = $centralDb->prepare("SELECT userId FROM user_employee_links WHERE id = :id AND typeNum = :typeNum");
282            $stmt->bindValue(':id', $linkId, \PDO::PARAM_INT);
283            $stmt->bindValue(':typeNum', $typeNum, \PDO::PARAM_STR);
284            $stmt->execute();
285            $link = $stmt->fetch(\PDO::FETCH_ASSOC);
286
287            if (!$link) {
288                throw new Exception("Link not found");
289            }
290
291            $linkManager = new UserEmployeeLinkManager($centralDb);
292            $success = $linkManager->removeLink($link['userId'], $typeNum);
293
294            $this->_app->response->headers->set('Content-Type', 'application/json');
295            $this->_app->response->setBody(json_encode([
296                'success' => $success
297            ]));
298        } catch (Exception $e) {
299            error_log("UserEmployeeLinkController::removeLink error: " . $e->getMessage());
300            $this->_app->response->setStatus(400);
301            $this->_app->response->headers->set('Content-Type', 'application/json');
302            $this->_app->response->setBody(json_encode([
303                'success' => false,
304                'error' => $e->getMessage()
305            ]));
306        }
307    }
308
309    /**
310     * POST /:typeNum/api/user-employee/auto-link
311     * Trigger auto-linking for store
312     *
313     * @param string $typeNum Store identifier
314     */
315    public function triggerAutoLink($typeNum)
316    {
317        // Check permissions
318        if (!$this->_app->user->checkStoreGroup($typeNum) ||
319            !$this->_app->user->checkAccess('uri_employees') ||
320            !$this->_app->user->checkAccess('uri_user_admin')) {
321            $this->_app->response->setStatus(403);
322            $this->_app->response->headers->set('Content-Type', 'application/json');
323            $this->_app->response->setBody(json_encode([
324                'success' => false,
325                'error' => 'Access denied'
326            ]));
327            return;
328        }
329
330        // Check CSRF token
331        if (!\NoCSRF::check('csrf_token', $this->_app->request->post())) {
332            $this->_app->response->setStatus(403);
333            $this->_app->response->headers->set('Content-Type', 'application/json');
334            $this->_app->response->setBody(json_encode([
335                'success' => false,
336                'error' => 'Invalid or missing CSRF token'
337            ]));
338            return;
339        }
340
341        try {
342            $centralDb = dbConnectByName('kiosk_users');
343            $linkManager = new UserEmployeeLinkManager($centralDb);
344
345            // Try both name and login matching
346            $nameStats = $linkManager->autoLinkByName($typeNum);
347            $loginStats = $linkManager->autoLinkByLogin($typeNum);
348
349            // Sync flags
350            $linkManager->syncEmployeeFlags($typeNum);
351
352            $this->_app->response->headers->set('Content-Type', 'application/json');
353            $this->_app->response->setBody(json_encode([
354                'success' => true,
355                'stats' => [
356                    'byName' => $nameStats,
357                    'byLogin' => $loginStats,
358                    'totalLinked' => $nameStats['linked'] + $loginStats['linked']
359                ]
360            ]));
361        } catch (Exception $e) {
362            error_log("UserEmployeeLinkController::triggerAutoLink error: " . $e->getMessage());
363            $this->_app->response->setStatus(500);
364            $this->_app->response->headers->set('Content-Type', 'application/json');
365            $this->_app->response->setBody(json_encode([
366                'success' => false,
367                'error' => 'Auto-linking failed: ' . $e->getMessage()
368            ]));
369        }
370    }
371
372    /**
373     * POST /:typeNum/api/employees/:id/promote
374     * Promote employee to user account (admin creates credentials directly)
375     *
376     * @param string $typeNum Store identifier
377     * @param int $employeeId Employee ID
378     */
379    public function promoteEmployee($typeNum, $employeeId)
380    {
381        // Check permissions
382        if (!$this->_app->user->checkStoreGroup($typeNum) ||
383            !$this->_app->user->checkAccess('create_user')) {
384            $this->_app->response->setStatus(403);
385            $this->_app->response->headers->set('Content-Type', 'application/json');
386            $this->_app->response->setBody(json_encode([
387                'success' => false,
388                'error' => 'Access denied'
389            ]));
390            return;
391        }
392
393        // Check CSRF token
394        if (!\NoCSRF::check('csrf_token', $this->_app->request->post())) {
395            $this->_app->response->setStatus(403);
396            $this->_app->response->headers->set('Content-Type', 'application/json');
397            $this->_app->response->setBody(json_encode([
398                'success' => false,
399                'error' => 'Invalid or missing CSRF token'
400            ]));
401            return;
402        }
403
404        try {
405            $data = $this->_app->request->post();
406
407            $username = $data['username'] ?? '';
408            $email = $data['email'] ?? '';
409            $password = $data['password'] ?? '';
410            $groups = $data['groups'] ?? [];
411
412            if (empty($username) || empty($email) || empty($password)) {
413                throw new Exception("Username, email, and password are required");
414            }
415
416            $centralDb = dbConnectByName('kiosk_users');
417            $linkManager = new UserEmployeeLinkManager($centralDb);
418            $invitationManager = new EmployeeInvitationManager($centralDb);
419            $promotion = new UserEmployeePromotion($centralDb, $linkManager, $invitationManager);
420
421            $result = $promotion->promoteEmployee(
422                $typeNum,
423                $employeeId,
424                $username,
425                $email,
426                $password,
427                $groups,
428                $this->_app->user->id
429            );
430
431            $this->_app->response->setStatus(201);
432            $this->_app->response->headers->set('Content-Type', 'application/json');
433            $this->_app->response->setBody(json_encode([
434                'success' => true,
435                'user' => $result['user'],
436                'linkId' => $result['linkId']
437            ]));
438        } catch (Exception $e) {
439            error_log("UserEmployeeLinkController::promoteEmployee error: " . $e->getMessage());
440            $this->_app->response->setStatus(400);
441            $this->_app->response->headers->set('Content-Type', 'application/json');
442            $this->_app->response->setBody(json_encode([
443                'success' => false,
444                'error' => $e->getMessage()
445            ]));
446        }
447    }
448
449    /**
450     * GET /:typeNum/api/employees/:id/link-status
451     * Get link status for employee
452     *
453     * @param string $typeNum Store identifier
454     * @param int $employeeId Employee ID
455     */
456    public function getEmployeeLinkStatus($typeNum, $employeeId)
457    {
458        // Check store access and employees permission
459        if (!$this->_app->user->checkStoreGroup($typeNum) || !$this->_app->user->checkAccess('uri_employees')) {
460            $this->_app->response->setStatus(403);
461            $this->_app->response->headers->set('Content-Type', 'application/json');
462            $this->_app->response->setBody(json_encode([
463                'success' => false,
464                'error' => 'Access denied'
465            ]));
466            return;
467        }
468
469        try {
470            $centralDb = dbConnectByName('kiosk_users');
471            $linkManager = new UserEmployeeLinkManager($centralDb);
472            $invitationManager = new EmployeeInvitationManager($centralDb);
473
474            $hasLink = $linkManager->hasLinkedUser($typeNum, $employeeId);
475            $user = $hasLink ? $linkManager->getLinkedUser($typeNum, $employeeId) : null;
476            $pendingInvitations = $invitationManager->getPendingInvitationsForEmployee($typeNum, $employeeId);
477
478            $this->_app->response->headers->set('Content-Type', 'application/json');
479            $this->_app->response->setBody(json_encode([
480                'success' => true,
481                'hasLink' => $hasLink,
482                'user' => $user,
483                'hasPendingInvitation' => !empty($pendingInvitations),
484                'pendingInvitations' => array_map(function($inv) {
485                    return $inv->toArray();
486                }, $pendingInvitations)
487            ]));
488        } catch (Exception $e) {
489            error_log("UserEmployeeLinkController::getEmployeeLinkStatus error: " . $e->getMessage());
490            $this->_app->response->setStatus(500);
491            $this->_app->response->headers->set('Content-Type', 'application/json');
492            $this->_app->response->setBody(json_encode([
493                'success' => false,
494                'error' => 'Failed to retrieve link status'
495            ]));
496        }
497    }
498}