diff --git a/.gitignore b/.gitignore index 08b9cc8..2ca2fb4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ tokens.jsonl valid_tokens.txt .env +*.sh # Encrypted database, temp files, backups uploads/* diff --git a/api/index.php b/api/index.php index c77d206..37852ee 100644 --- a/api/index.php +++ b/api/index.php @@ -38,6 +38,7 @@ $routes = [ 'translations' => __DIR__ . '/../handlers/translations.php', 'users' => __DIR__ . '/../handlers/users.php', 'assignments' => __DIR__ . '/../handlers/assignments.php', + 'clients' => __DIR__ . '/../handlers/clients.php', 'results' => __DIR__ . '/../handlers/results.php', 'export' => __DIR__ . '/../handlers/export.php', 'app_questionnaires' => __DIR__ . '/../handlers/app_questionnaires.php', diff --git a/handlers/assignments.php b/handlers/assignments.php index 8a1985b..40149fa 100644 --- a/handlers/assignments.php +++ b/handlers/assignments.php @@ -30,7 +30,7 @@ case 'GET': $coaches = $stmt->fetchAll(PDO::FETCH_ASSOC); } - [$clause, $params] = rbac_client_filter($tokenRec); + [$clause, $params] = rbac_client_filter($tokenRec, 'cl'); $clientStmt = $pdo->prepare( "SELECT cl.clientCode, cl.coachID, co.username AS coachUsername FROM client cl diff --git a/handlers/clients.php b/handlers/clients.php new file mode 100644 index 0000000..6d19187 --- /dev/null +++ b/handlers/clients.php @@ -0,0 +1,123 @@ +prepare( + "SELECT cl.clientCode, cl.coachID, co.username AS coachUsername + FROM client cl + LEFT JOIN coach co ON co.coachID = cl.coachID + WHERE $clause + ORDER BY cl.clientCode" + ); + foreach ($params as $k => $v) $stmt->bindValue($k, $v); + $stmt->execute(); + $clients = $stmt->fetchAll(PDO::FETCH_ASSOC); + + qdb_discard($tmpDb, $lockFp); + json_success(['clients' => $clients]); + } catch (Throwable $e) { + if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); + error_log($e->getMessage()); + json_error('SERVER_ERROR', 'Server error', 500); + } + break; + +case 'POST': + $body = read_json_body(); + $clientCode = trim($body['clientCode'] ?? ''); + $coachID = trim($body['coachID'] ?? ''); + + if ($clientCode === '' || $coachID === '') { + json_error('MISSING_FIELDS', 'clientCode and coachID are required', 400); + } + + try { + [$pdo, $tmpDb, $lockFp] = qdb_open(true); + + // Validate coach exists and caller is allowed to assign to them + if ($callerRole === 'supervisor') { + $chkCoach = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :sid"); + $chkCoach->execute([':cid' => $coachID, ':sid' => $callerEntityID]); + } else { + $chkCoach = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid"); + $chkCoach->execute([':cid' => $coachID]); + } + if (!$chkCoach->fetch()) { + qdb_discard($tmpDb, $lockFp); + json_error('NOT_FOUND', 'Coach not found or not authorized', 400); + } + + $chk = $pdo->prepare("SELECT clientCode FROM client WHERE clientCode = :cc"); + $chk->execute([':cc' => $clientCode]); + if ($chk->fetch()) { + qdb_discard($tmpDb, $lockFp); + json_error('DUPLICATE', 'Client code already exists', 409); + } + + $pdo->prepare("INSERT INTO client (clientCode, coachID) VALUES (:cc, :cid)") + ->execute([':cc' => $clientCode, ':cid' => $coachID]); + + qdb_save($tmpDb, $lockFp); + json_success(['clientCode' => $clientCode, 'coachID' => $coachID]); + } 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(); + $clientCode = trim($body['clientCode'] ?? ''); + + if ($clientCode === '') { + json_error('MISSING_FIELDS', 'clientCode is required', 400); + } + + try { + [$pdo, $tmpDb, $lockFp] = qdb_open(true); + + // Verify the client exists and the caller can see it (RBAC) + [$clause, $params] = rbac_client_filter($tokenRec, 'cl'); + $chk = $pdo->prepare( + "SELECT clientCode FROM client cl WHERE cl.clientCode = :cc AND ($clause)" + ); + $chk->execute(array_merge([':cc' => $clientCode], $params)); + if (!$chk->fetch()) { + qdb_discard($tmpDb, $lockFp); + json_error('NOT_FOUND', 'Client not found or not authorized', 404); + } + + $pdo->beginTransaction(); + $pdo->prepare("DELETE FROM client_answer WHERE clientCode = :cc") + ->execute([':cc' => $clientCode]); + $pdo->prepare("DELETE FROM completed_questionnaire WHERE clientCode = :cc") + ->execute([':cc' => $clientCode]); + $pdo->prepare("DELETE FROM client WHERE clientCode = :cc") + ->execute([':cc' => $clientCode]); + $pdo->commit(); + + qdb_save($tmpDb, $lockFp); + json_success([]); + } catch (Throwable $e) { + if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack(); + if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); + error_log($e->getMessage()); + json_error('SERVER_ERROR', 'Server error', 500); + } + break; + +default: + json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405); +} diff --git a/website/index.html b/website/index.html index 03a6837..1ce969a 100644 --- a/website/index.html +++ b/website/index.html @@ -27,6 +27,12 @@ Users +
${esc(e.message)}
`; + } +} + +function renderClients() { + const container = document.getElementById('clientsContent'); + + if (!clientsList.length) { + container.innerHTML = ` +Create the first client above.
+| Client Code | +Current Coach | +Actions | +
|---|