Files
nat-as-server/handlers/clients.php
tom.hempel f04388e0ec
Some checks failed
PHPUnit / test (push) Has been cancelled
changes to translation system
2026-07-09 14:59:56 +02:00

328 lines
12 KiB
PHP

<?php
$tokenRec = require_valid_token_web();
$callerRole = $tokenRec['role'];
$callerEntityID = $tokenRec['entityID'] ?? '';
switch ($method) {
case 'GET':
try {
[$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail();
$clientCode = trim((string)($_GET['clientCode'] ?? ''));
if ($clientCode !== '' && !empty($_GET['detail'])) {
require_once __DIR__ . '/../lib/submissions.php';
$detail = qdb_client_detail($pdo, $tokenRec, $clientCode);
qdb_discard($tmpDb, $lockFp);
json_success($detail);
}
$archiveFilter = trim((string)($_GET['archiveFilter'] ?? 'active'));
if (!in_array($archiveFilter, ['active', 'archived', 'all'], true)) {
$archiveFilter = 'active';
}
[$clause, $params] = rbac_client_filter($tokenRec, 'cl', $archiveFilter);
$stmt = $pdo->prepare(
"SELECT cl.clientCode, cl.coachID, cl.archived, cl.archivedAt,
co.username AS coachUsername,
CASE WHEN EXISTS (
SELECT 1 FROM completed_questionnaire cq WHERE cq.clientCode = cl.clientCode
) OR EXISTS (
SELECT 1 FROM questionnaire_submission qs WHERE qs.clientCode = cl.clientCode
) THEN 1 ELSE 0 END AS hasResponseData
FROM client cl
LEFT JOIN coach co ON co.coachID = cl.coachID
WHERE $clause
ORDER BY cl.archived ASC, hasResponseData DESC, cl.clientCode ASC"
);
foreach ($params as $k => $v) $stmt->bindValue($k, $v);
$stmt->execute();
$clients = $stmt->fetchAll(PDO::FETCH_ASSOC);
require_once __DIR__ . '/../lib/submissions.php';
$scoringSummary = qdb_clients_scoring_summary_for_list($pdo, $tokenRec);
foreach ($clients as &$client) {
$client['scoringProfiles'] = qdb_client_scoring_dots_for_client(
$client['clientCode'],
$scoringSummary
);
}
unset($client);
qdb_discard($tmpDb, $lockFp);
json_success([
'clients' => $clients,
'scoringProfiles' => $scoringSummary['profiles'],
]);
} catch (Throwable $e) {
qdb_handler_fail($e, 'clients', null, $tmpDb ?? null, $lockFp ?? null);
}
break;
case 'POST':
$body = read_json_body();
if (($body['action'] ?? '') === 'reset') {
$clientCode = trim((string)($body['clientCode'] ?? ''));
if ($clientCode === '') {
json_error('MISSING_FIELDS', 'clientCode is required', 400);
}
try {
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
[$clause, $params] = rbac_client_filter($tokenRec, 'cl', 'all');
$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);
}
require_once __DIR__ . '/../lib/submissions.php';
$pdo->beginTransaction();
$cleared = qdb_delete_client_response_data($pdo, [$clientCode]);
$pdo->commit();
qdb_save($tmpDb, $lockFp);
json_success([
'clientCode' => $clientCode,
'reset' => true,
'cleared' => $cleared,
]);
} catch (Throwable $e) {
qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
}
break;
}
$coachID = trim($body['coachID'] ?? '');
$bulk = !empty($body['bulk']);
if ($coachID === '') {
json_error('MISSING_FIELDS', 'coachID is required', 400);
}
try {
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
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', 'Counselor not found or not authorized', 404);
}
if ($bulk) {
$fromCode = trim($body['fromCode'] ?? '');
$toCode = trim($body['toCode'] ?? '');
if ($fromCode === '' || $toCode === '') {
qdb_discard($tmpDb, $lockFp);
json_error('MISSING_FIELDS', 'fromCode and toCode are required for bulk creation', 400);
}
require_once __DIR__ . '/../lib/client_code_range.php';
try {
$codes = qdb_expand_client_code_range($fromCode, $toCode);
} catch (InvalidArgumentException $e) {
qdb_discard($tmpDb, $lockFp);
json_error('INVALID_RANGE', $e->getMessage(), 400);
}
$chk = $pdo->prepare("SELECT clientCode FROM client WHERE clientCode = :cc");
$insert = $pdo->prepare(
"INSERT INTO client (clientCode, coachID, archived, archivedAt) VALUES (:cc, :cid, 0, 0)"
);
$created = [];
$skipped = [];
foreach ($codes as $code) {
$chk->execute([':cc' => $code]);
if ($chk->fetch()) {
$skipped[] = ['clientCode' => $code, 'reason' => 'duplicate'];
continue;
}
$insert->execute([':cc' => $code, ':cid' => $coachID]);
$created[] = $code;
}
qdb_save($tmpDb, $lockFp);
json_success([
'coachID' => $coachID,
'fromCode' => $fromCode,
'toCode' => $toCode,
'created' => $created,
'skipped' => $skipped,
'createdCount' => count($created),
'skippedCount' => count($skipped),
]);
}
$clientCode = trim($body['clientCode'] ?? '');
if ($clientCode === '') {
qdb_discard($tmpDb, $lockFp);
json_error('MISSING_FIELDS', 'clientCode is required', 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, archived, archivedAt) VALUES (:cc, :cid, 0, 0)"
)->execute([':cc' => $clientCode, ':cid' => $coachID]);
qdb_save($tmpDb, $lockFp);
json_success(['clientCode' => $clientCode, 'coachID' => $coachID]);
} catch (Throwable $e) {
qdb_handler_fail($e, 'clients', null, $tmpDb ?? null, $lockFp ?? null);
}
break;
case 'PUT':
$body = read_json_body();
$clientCode = trim((string)($body['clientCode'] ?? ''));
$profileID = trim((string)($body['profileID'] ?? ''));
$coachBand = trim((string)($body['coachBand'] ?? ''));
if ($clientCode === '' || $profileID === '' || $coachBand === '') {
json_error('MISSING_FIELDS', 'clientCode, profileID, and coachBand are required', 400);
}
$calculatedBand = trim((string)($body['calculatedBand'] ?? ''));
$weightedTotal = isset($body['weightedTotal']) ? (float)$body['weightedTotal'] : null;
try {
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
[$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);
}
require_once __DIR__ . '/../lib/scoring.php';
if (!qdb_is_valid_scoring_band($coachBand)) {
qdb_discard($tmpDb, $lockFp);
json_error('INVALID_FIELD', 'coachBand must be green, yellow, or red', 400);
}
$profile = qdb_save_coach_scoring_review(
$pdo,
$clientCode,
$profileID,
$coachBand,
(string)($tokenRec['userID'] ?? ''),
$weightedTotal,
$calculatedBand !== '' ? $calculatedBand : null,
);
qdb_save($tmpDb, $lockFp);
json_success(['profile' => $profile]);
} catch (InvalidArgumentException $e) {
qdb_discard($tmpDb ?? null, $lockFp ?? null);
json_error('INVALID_FIELD', $e->getMessage(), 400);
} catch (RuntimeException $e) {
qdb_discard($tmpDb ?? null, $lockFp ?? null);
json_error('NOT_FOUND', $e->getMessage(), 404);
} catch (Throwable $e) {
qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
}
break;
case 'PATCH':
$body = read_json_body();
$clientCode = trim((string)($body['clientCode'] ?? ''));
if ($clientCode === '' || !array_key_exists('archived', $body)) {
json_error('MISSING_FIELDS', 'clientCode and archived are required', 400);
}
$archived = !empty($body['archived']) ? 1 : 0;
try {
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
[$clause, $params] = rbac_client_filter($tokenRec, 'cl', 'all');
$chk = $pdo->prepare(
"SELECT clientCode, archived FROM client cl WHERE cl.clientCode = :cc AND ($clause)"
);
$chk->execute(array_merge([':cc' => $clientCode], $params));
$row = $chk->fetch(PDO::FETCH_ASSOC);
if (!$row) {
qdb_discard($tmpDb, $lockFp);
json_error('NOT_FOUND', 'Client not found or not authorized', 404);
}
$archivedAt = $archived ? time() : 0;
$pdo->prepare(
'UPDATE client SET archived = :a, archivedAt = :at WHERE clientCode = :cc'
)->execute([':a' => $archived, ':at' => $archivedAt, ':cc' => $clientCode]);
qdb_save($tmpDb, $lockFp);
json_success([
'clientCode' => $clientCode,
'archived' => $archived,
'archivedAt' => $archivedAt,
]);
} catch (Throwable $e) {
qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
}
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_write_or_fail();
// 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);
}
require_once __DIR__ . '/../lib/submissions.php';
$pdo->beginTransaction();
qdb_delete_client_response_data($pdo, [$clientCode]);
$pdo->prepare('DELETE FROM client WHERE clientCode = :cc')
->execute([':cc' => $clientCode]);
$pdo->commit();
qdb_save($tmpDb, $lockFp);
json_success([]);
} catch (Throwable $e) {
qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
}
break;
default:
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
}