This commit is contained in:
@ -7,6 +7,9 @@
|
||||
* GET ?clients=1 -> list of clients assigned to the authenticated coach
|
||||
* GET ?clientCode=X&answers=1 -> all completed questionnaire answers for one client (encrypted)
|
||||
* GET ?answersBulk=1 -> all assigned clients' answers in one payload (coach, encrypted)
|
||||
* GET ?scoringProfiles=1 -> active scoring profile definitions (encrypted)
|
||||
* GET ?scoringReview=1 -> coach review state (coachBand only; computed on device)
|
||||
* POST action=coachScoringReview -> coach agrees with or overrides calculated band (encrypted)
|
||||
* POST -> submit interview answers for a client (coach / supervisor / admin).
|
||||
* Re-posting the same clientCode + questionnaireID updates answers in place (re-edit).
|
||||
* Sensitive POST/GET ?clients=1 bodies use encrypted payloads (HKDF from Bearer token).
|
||||
@ -27,6 +30,58 @@ if ($method === 'POST') {
|
||||
require_role(['admin', 'supervisor', 'coach'], $tokenRec);
|
||||
|
||||
$body = read_encrypted_json_body($tokenRec['_token']);
|
||||
|
||||
if (($body['action'] ?? '') === 'coachScoringReview') {
|
||||
require_fields($body, ['clientCode', 'profileID', 'coachBand']);
|
||||
$clientCode = trim((string)$body['clientCode']);
|
||||
$profileID = trim((string)$body['profileID']);
|
||||
$coachBand = trim((string)($body['coachBand'] ?? ''));
|
||||
$calculatedBand = trim((string)($body['calculatedBand'] ?? ''));
|
||||
$weightedTotal = isset($body['weightedTotal']) ? (float)$body['weightedTotal'] : null;
|
||||
|
||||
try {
|
||||
$opened = qdb_open_write_or_fail();
|
||||
[$pdo, $tmpDb, $lockFp] = $opened;
|
||||
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
$clStmt = $pdo->prepare(
|
||||
"SELECT cl.clientCode FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"
|
||||
);
|
||||
$clStmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
|
||||
if (!$clStmt->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, 'Coach scoring review', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
require_fields($body, ['questionnaireID', 'clientCode', 'answers']);
|
||||
|
||||
$qnID = trim($body['questionnaireID']);
|
||||
@ -126,7 +181,6 @@ if ($method === 'POST') {
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$sumPoints = 0;
|
||||
$glassByParent = [];
|
||||
$submittedQuestionIds = [];
|
||||
$answerInsert = $pdo->prepare("
|
||||
@ -183,19 +237,11 @@ if ($method === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve option key to answerOptionID and accumulate points
|
||||
// Resolve option key to answerOptionID (points computed by scoring engine after save)
|
||||
$optionKey = $a['answerOptionKey'] ?? null;
|
||||
if ($optionKey !== null && isset($optionMap[$fullQID][(string)$optionKey])) {
|
||||
$opt = $optionMap[$fullQID][(string)$optionKey];
|
||||
$answerOptionID = $opt['answerOptionID'];
|
||||
$sumPoints += $opt['points'];
|
||||
} elseif (($shortIdToType[$shortId] ?? '') === 'multi_check_box_question'
|
||||
&& $freeTextValue !== null && $freeTextValue !== '') {
|
||||
foreach (qdb_decode_multi_check_values($freeTextValue) as $mk) {
|
||||
if (isset($optionMap[$fullQID][$mk])) {
|
||||
$sumPoints += $optionMap[$fullQID][$mk]['points'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hasValue = $answerOptionID !== null
|
||||
@ -247,6 +293,9 @@ if ($method === 'POST') {
|
||||
}
|
||||
|
||||
// Upsert completed_questionnaire record
|
||||
require_once __DIR__ . '/../lib/scoring.php';
|
||||
$sumPoints = qdb_compute_questionnaire_score($pdo, $clientCode, $qnID);
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO completed_questionnaire (clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, sumPoints)
|
||||
VALUES (:cc, :qn, :abc, 'completed', :sa, :ca, :sp)
|
||||
@ -277,6 +326,8 @@ if ($method === 'POST') {
|
||||
$assignedByCoach
|
||||
);
|
||||
|
||||
qdb_recompute_profile_scores_for_client($pdo, $clientCode, $qnID);
|
||||
|
||||
$pdo->commit();
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
@ -298,8 +349,50 @@ $translations = $_GET['translations'] ?? '';
|
||||
$fetchClients = $_GET['clients'] ?? '';
|
||||
$fetchAnswers = $_GET['answers'] ?? '';
|
||||
$fetchAnswersBulk = $_GET['answersBulk'] ?? '';
|
||||
$fetchScoringProfiles = $_GET['scoringProfiles'] ?? '';
|
||||
$fetchScoringReview = $_GET['scoringReview'] ?? '';
|
||||
$clientCode = trim($_GET['clientCode'] ?? '');
|
||||
|
||||
if ($fetchScoringProfiles) {
|
||||
require_role(['coach'], $tokenRec);
|
||||
require_once __DIR__ . '/../lib/scoring.php';
|
||||
$profiles = array_values(array_filter(
|
||||
qdb_list_scoring_profiles($pdo),
|
||||
static fn(array $p): bool => (int)($p['isActive'] ?? 0) === 1
|
||||
));
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_success_sensitive(['profiles' => $profiles], $tokenRec['_token']);
|
||||
}
|
||||
|
||||
if ($fetchScoringReview) {
|
||||
require_role(['coach'], $tokenRec);
|
||||
require_once __DIR__ . '/../lib/scoring.php';
|
||||
|
||||
$coachID = $tokenRec['entityID'] ?? '';
|
||||
if ($clientCode !== '') {
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
$clStmt = $pdo->prepare(
|
||||
"SELECT cl.clientCode FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"
|
||||
);
|
||||
$clStmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
|
||||
if (!$clStmt->fetch()) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'Client not found or not authorized', 404);
|
||||
}
|
||||
$clientCodes = [$clientCode];
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT clientCode FROM client WHERE coachID = :cid ORDER BY clientCode'
|
||||
);
|
||||
$stmt->execute([':cid' => $coachID]);
|
||||
$clientCodes = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
$clients = qdb_app_scoring_review_for_clients($pdo, $clientCodes);
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_success_sensitive(['clients' => $clients], $tokenRec['_token']);
|
||||
}
|
||||
|
||||
if ($fetchAnswersBulk) {
|
||||
require_role(['coach'], $tokenRec);
|
||||
require_once __DIR__ . '/../lib/app_answers.php';
|
||||
@ -441,6 +534,14 @@ if ($qnID) {
|
||||
$q['options'] = $config['valueOptions'];
|
||||
}
|
||||
|
||||
if (in_array($layout, ['glass_scale_question', 'slider_question', 'value_spinner'], true)) {
|
||||
require_once __DIR__ . '/../lib/scoring.php';
|
||||
$ruleMap = qdb_score_rule_map_for_question($pdo, $dbQ['questionID']);
|
||||
if ($ruleMap !== []) {
|
||||
$q['scoreRuleMap'] = $ruleMap;
|
||||
}
|
||||
}
|
||||
|
||||
$ao = $pdo->prepare("
|
||||
SELECT defaultText, points, nextQuestionId
|
||||
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex
|
||||
|
||||
Reference in New Issue
Block a user