updated api

This commit is contained in:
2026-05-22 13:26:29 +02:00
parent c86372af2e
commit 52d4ef42dc
17 changed files with 867 additions and 41 deletions

View File

@ -5,9 +5,12 @@
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
* GET ?translations=1 -> all translations merged across all tables
* GET ?clients=1 -> list of clients assigned to the authenticated coach
* GET ?clientCode=&questionnaireID=&answers=1 -> latest answers for re-edit
* POST -> submit interview answers for a client (coach / supervisor / admin)
*/
require_once __DIR__ . '/../lib/activity.php';
$tokenRec = require_valid_token();
if ($method === 'POST') {
@ -78,9 +81,74 @@ if ($method === 'POST') {
];
}
$uploadedAt = (int)(microtime(true) * 1000);
$submissionID = 'sub_' . bin2hex(random_bytes(12));
$submissionVersion = next_submission_version($pdo, $clientCode, $qnID);
$submittedByUserID = $tokenRec['userID'] ?? null;
$pdo->beginTransaction();
$sumPoints = 0;
$parsedAnswers = [];
foreach ($answers as $a) {
$shortId = trim($a['questionID'] ?? '');
if ($shortId === '') continue;
$fullQID = $shortIdMap[$shortId] ?? null;
if ($fullQID === null) continue;
$answerOptionID = null;
$answerOptionKey = null;
$freeTextValue = isset($a['freeTextValue']) ? (string)$a['freeTextValue'] : null;
$numericValue = isset($a['numericValue']) ? (float)$a['numericValue'] : null;
$answeredAt = isset($a['answeredAt']) ? (int)$a['answeredAt'] : null;
$optionKey = $a['answerOptionKey'] ?? null;
if ($optionKey !== null && isset($optionMap[$fullQID][(string)$optionKey])) {
$opt = $optionMap[$fullQID][(string)$optionKey];
$answerOptionID = $opt['answerOptionID'];
$answerOptionKey = (string)$optionKey;
$sumPoints += $opt['points'];
}
$parsedAnswers[] = [
'shortId' => $shortId,
'fullQID' => $fullQID,
'answerOptionID' => $answerOptionID,
'answerOptionKey' => $answerOptionKey,
'freeTextValue' => $freeTextValue,
'numericValue' => $numericValue,
'answeredAt' => $answeredAt,
];
}
$pdo->prepare("
INSERT INTO questionnaire_submission (
submissionID, clientCode, questionnaireID, assignedByCoach, status,
startedAt, completedAt, uploadedAt, sumPoints, submissionVersion,
submittedByUserID, submittedByRole
) VALUES (
:sid, :cc, :qn, :abc, 'completed', :sa, :ca, :ua, :sp, :sv, :uid, :role
)
")->execute([
':sid' => $submissionID,
':cc' => $clientCode,
':qn' => $qnID,
':abc' => $assignedByCoach !== '' ? $assignedByCoach : null,
':sa' => $startedAt,
':ca' => $completedAt,
':ua' => $uploadedAt,
':sp' => $sumPoints,
':sv' => $submissionVersion,
':uid' => $submittedByUserID,
':role' => $tokenRec['role'] ?? '',
]);
$subAnsInsert = $pdo->prepare("
INSERT INTO submission_answer (submissionID, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
VALUES (:sid, :qid, :aoid, :ftv, :nv, :at)
");
$answerInsert = $pdo->prepare("
INSERT INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)
@ -91,46 +159,34 @@ if ($method === 'POST') {
answeredAt = excluded.answeredAt
");
foreach ($answers as $a) {
$shortId = trim($a['questionID'] ?? '');
if ($shortId === '') continue;
// Resolve short ID to full questionID
$fullQID = $shortIdMap[$shortId] ?? null;
if ($fullQID === null) continue; // skip unknown questions
$answerOptionID = null;
$freeTextValue = isset($a['freeTextValue']) ? (string)$a['freeTextValue'] : null;
$numericValue = isset($a['numericValue']) ? (float)$a['numericValue'] : null;
$answeredAt = isset($a['answeredAt']) ? (int)$a['answeredAt'] : null;
// Resolve option key to answerOptionID and accumulate points
$optionKey = $a['answerOptionKey'] ?? null;
if ($optionKey !== null && isset($optionMap[$fullQID][(string)$optionKey])) {
$opt = $optionMap[$fullQID][(string)$optionKey];
$answerOptionID = $opt['answerOptionID'];
$sumPoints += $opt['points'];
}
foreach ($parsedAnswers as $pa) {
$subAnsInsert->execute([
':sid' => $submissionID,
':qid' => $pa['fullQID'],
':aoid' => $pa['answerOptionID'],
':ftv' => $pa['freeTextValue'],
':nv' => $pa['numericValue'],
':at' => $pa['answeredAt'],
]);
$answerInsert->execute([
':cc' => $clientCode,
':qid' => $fullQID,
':aoid' => $answerOptionID,
':ftv' => $freeTextValue,
':nv' => $numericValue,
':at' => $answeredAt,
':qid' => $pa['fullQID'],
':aoid' => $pa['answerOptionID'],
':ftv' => $pa['freeTextValue'],
':nv' => $pa['numericValue'],
':at' => $pa['answeredAt'],
]);
}
// Upsert completed_questionnaire record
$pdo->prepare("
INSERT INTO completed_questionnaire (clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, sumPoints)
VALUES (:cc, :qn, :abc, 'completed', :sa, :ca, :sp)
INSERT INTO completed_questionnaire (clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, uploadedAt, sumPoints)
VALUES (:cc, :qn, :abc, 'completed', :sa, :ca, :ua, :sp)
ON CONFLICT(clientCode, questionnaireID) DO UPDATE SET
assignedByCoach = excluded.assignedByCoach,
status = 'completed',
startedAt = COALESCE(excluded.startedAt, startedAt),
completedAt = excluded.completedAt,
uploadedAt = excluded.uploadedAt,
sumPoints = excluded.sumPoints
")->execute([
':cc' => $clientCode,
@ -138,13 +194,39 @@ if ($method === 'POST') {
':abc' => $assignedByCoach !== '' ? $assignedByCoach : null,
':sa' => $startedAt,
':ca' => $completedAt,
':ua' => $uploadedAt,
':sp' => $sumPoints,
]);
log_activity(
$pdo,
$tokenRec,
'questionnaire_submitted',
'client',
$clientCode,
"Uploaded $qnID for $clientCode (v$submissionVersion)",
[
'submissionID' => $submissionID,
'clientCode' => $clientCode,
'questionnaireID' => $qnID,
'uploadedAt' => $uploadedAt,
'completedAt' => $completedAt,
'submissionVersion' => $submissionVersion,
'sumPoints' => $sumPoints,
],
$uploadedAt
);
$pdo->commit();
qdb_save($tmpDb, $lockFp);
json_success(['submitted' => true, 'sumPoints' => $sumPoints]);
json_success([
'submitted' => true,
'sumPoints' => $sumPoints,
'submissionID' => $submissionID,
'uploadedAt' => $uploadedAt,
'submissionVersion' => $submissionVersion,
]);
} catch (Throwable $e) {
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp);
@ -159,9 +241,80 @@ if ($method !== 'GET') {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$qnID = $_GET['id'] ?? '';
$qnID = trim($_GET['id'] ?? $_GET['questionnaireID'] ?? '');
$translations = $_GET['translations'] ?? '';
$fetchClients = $_GET['clients'] ?? '';
$clientCode = trim($_GET['clientCode'] ?? '');
$fetchAnswers = ($_GET['answers'] ?? '') === '1' || ($_GET['answers'] ?? '') === 'true';
if ($fetchAnswers && $clientCode !== '' && $qnID !== '') {
require_role(['admin', 'supervisor', 'coach'], $tokenRec);
[$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);
}
$qRows = $pdo->prepare("SELECT questionID FROM question WHERE questionnaireID = :qn");
$qRows->execute([':qn' => $qnID]);
$fullToShort = [];
foreach ($qRows->fetchAll(PDO::FETCH_COLUMN) as $fullId) {
$parts = explode('__', $fullId);
$fullToShort[$fullId] = end($parts);
}
$aoRows = $pdo->prepare("
SELECT ao.answerOptionID, ao.questionID, ao.defaultText
FROM answer_option ao
JOIN question q ON q.questionID = ao.questionID
WHERE q.questionnaireID = :qn
");
$aoRows->execute([':qn' => $qnID]);
$optionKeyById = [];
foreach ($aoRows->fetchAll(PDO::FETCH_ASSOC) as $ao) {
$optionKeyById[$ao['answerOptionID']] = $ao['defaultText'];
}
$ansStmt = $pdo->prepare("
SELECT questionID, answerOptionID, freeTextValue, numericValue, answeredAt
FROM client_answer
WHERE clientCode = :cc AND questionID IN (
SELECT questionID FROM question WHERE questionnaireID = :qn
)
");
$ansStmt->execute([':cc' => $clientCode, ':qn' => $qnID]);
$outAnswers = [];
foreach ($ansStmt->fetchAll(PDO::FETCH_ASSOC) as $a) {
$shortId = $fullToShort[$a['questionID']] ?? null;
if ($shortId === null) continue;
$row = ['questionID' => $shortId];
if ($a['answerOptionID'] && isset($optionKeyById[$a['answerOptionID']])) {
$row['answerOptionKey'] = $optionKeyById[$a['answerOptionID']];
}
if ($a['freeTextValue'] !== null && $a['freeTextValue'] !== '') {
$row['freeTextValue'] = $a['freeTextValue'];
}
if ($a['numericValue'] !== null) {
$row['numericValue'] = (float)$a['numericValue'];
}
if ($a['answeredAt'] !== null) {
$row['answeredAt'] = (int)$a['answeredAt'];
}
$outAnswers[] = $row;
}
qdb_discard($tmpDb, $lockFp);
json_success([
'clientCode' => $clientCode,
'questionnaireID' => $qnID,
'answers' => $outAnswers,
]);
}
if ($fetchClients) {
require_role(['coach'], $tokenRec);
@ -181,7 +334,7 @@ if ($fetchClients) {
if (!empty($clientCodes)) {
$placeholders = implode(',', array_fill(0, count($clientCodes), '?'));
$stmt2 = $pdo->prepare("
SELECT clientCode, questionnaireID, sumPoints, completedAt
SELECT clientCode, questionnaireID, sumPoints, completedAt, uploadedAt
FROM completed_questionnaire
WHERE clientCode IN ($placeholders)
");
@ -191,6 +344,7 @@ if ($fetchClients) {
'questionnaireID' => $row['questionnaireID'],
'sumPoints' => (int) $row['sumPoints'],
'completedAt' => $row['completedAt'] !== null ? (int) $row['completedAt'] : null,
'uploadedAt' => $row['uploadedAt'] !== null ? (int) $row['uploadedAt'] : null,
];
}
}