diff --git a/handlers/users.php b/handlers/users.php index 64708fa..7feef4c 100644 --- a/handlers/users.php +++ b/handlers/users.php @@ -168,6 +168,75 @@ case 'POST': } break; +case 'PUT': +case 'PATCH': + if ($callerRole !== 'admin') { + json_error('FORBIDDEN', 'Only admins can reassign coaches', 403); + } + + $body = read_json_body(); + $targetUserID = trim($body['userID'] ?? ''); + $supervisorID = trim($body['supervisorID'] ?? ''); + + if ($targetUserID === '' || $supervisorID === '') { + json_error('MISSING_FIELDS', 'userID and supervisorID are required', 400); + } + + try { + [$pdo, $tmpDb, $lockFp] = qdb_open(true); + + $row = $pdo->prepare( + "SELECT u.userID, u.username, u.role, u.entityID, c.supervisorID + FROM users u + LEFT JOIN coach c ON c.coachID = u.entityID AND u.role = 'coach' + WHERE u.userID = :uid" + ); + $row->execute([':uid' => $targetUserID]); + $target = $row->fetch(PDO::FETCH_ASSOC); + + if (!$target) { + qdb_discard($tmpDb, $lockFp); + json_error('NOT_FOUND', 'User not found', 404); + } + if (($target['role'] ?? '') !== 'coach') { + qdb_discard($tmpDb, $lockFp); + json_error('INVALID_FIELD', 'Only coaches can be reassigned to a supervisor', 400); + } + + $svCheck = $pdo->prepare('SELECT username FROM supervisor WHERE supervisorID = :sid'); + $svCheck->execute([':sid' => $supervisorID]); + $svUsername = $svCheck->fetchColumn(); + if ($svUsername === false) { + qdb_discard($tmpDb, $lockFp); + json_error('NOT_FOUND', 'Supervisor not found', 404); + } + + if (($target['supervisorID'] ?? '') === $supervisorID) { + qdb_discard($tmpDb, $lockFp); + json_success([ + 'userID' => $targetUserID, + 'supervisorID' => $supervisorID, + 'supervisorUsername' => $svUsername, + ]); + } + + $pdo->prepare('UPDATE coach SET supervisorID = :sid WHERE coachID = :cid') + ->execute([':sid' => $supervisorID, ':cid' => $target['entityID']]); + + qdb_save($tmpDb, $lockFp); + + json_success([ + 'userID' => $targetUserID, + 'supervisorID' => $supervisorID, + 'supervisorUsername' => $svUsername, + ]); + } catch (Throwable $e) { + if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); + error_log($e->getMessage()); + json_error('SERVER_ERROR', 'Server error', 500); + } + break; + case 'DELETE': $body = read_json_body(); $targetUserID = trim($body['userID'] ?? ''); diff --git a/website/css/style.css b/website/css/style.css index e19aa3b..75b0589 100644 --- a/website/css/style.css +++ b/website/css/style.css @@ -333,13 +333,49 @@ code { flex-wrap: wrap; gap: 12px; } +.page-header-start { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} +.page-header-titles { + min-width: 0; +} .page-header h1 { font-size: 1.5rem; font-weight: 700; + margin: 0; } .page-header .actions { display: flex; + flex-wrap: wrap; gap: 8px; + align-items: center; +} +.btn-home { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + min-width: 40px; + padding: 0; + flex-shrink: 0; +} +.btn-home svg { + width: 20px; + height: 20px; +} +.coach-supervisor-select { + max-width: 100%; + min-width: 160px; + font-size: 0.85rem; + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface); + color: var(--text); } /* Questionnaire dashboard root (not .q-grid — avoids grid layout on nested cards) */ @@ -1725,6 +1761,58 @@ table.data-table tbody tr.row-orphan-category td { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; } +.page-header-sub { + margin: 6px 0 0; + font-size: 0.9rem; + color: var(--text-secondary); + font-weight: normal; +} +.home-kpi-link { + text-decoration: none; + color: inherit; + transition: border-color 0.15s, background 0.15s; +} +.home-kpi-link:hover { + border-color: var(--primary-border); + background: var(--surface-hover); +} +.home-dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-top: 20px; +} +.home-link-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 10px; +} +.home-link-card { + display: block; + padding: 14px 16px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-muted); + text-decoration: none; + color: inherit; + transition: border-color 0.15s, background 0.15s; +} +.home-link-card:hover { + border-color: var(--primary-border); + background: var(--surface-hover); +} +.home-link-title { + display: block; + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 4px; +} +.home-link-desc { + display: block; + font-size: 0.8rem; + color: var(--text-secondary); + line-height: 1.35; +} .insights-kpi { padding: 20px; text-align: center; diff --git a/website/index.html b/website/index.html index 5e0c83d..c9d20dc 100644 --- a/website/index.html +++ b/website/index.html @@ -17,12 +17,18 @@ Questionnaire Management