added client answer API endpoint
This commit is contained in:
49
.gitignore
vendored
49
.gitignore
vendored
@ -4,11 +4,56 @@ valid_tokens.txt
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
# Encrypted database, temp files, backups
|
# Encrypted database, temp files, backups
|
||||||
uploads/
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
|
||||||
# OS / editor
|
# Database lock files (just in case)
|
||||||
|
uploads/.qdb_lock
|
||||||
|
uploads/*.lock
|
||||||
|
|
||||||
|
# Logs / dumps / exports
|
||||||
|
*.log
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
*.dump
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Node/npm artifacts (if website/js uses npm)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# PHP composer artifacts (if using composer in future)
|
||||||
|
vendor/
|
||||||
|
composer.lock
|
||||||
|
|
||||||
|
# IDE / editor
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
|
||||||
|
# Ignore schema if generated
|
||||||
|
schema.sql
|
||||||
|
|
||||||
|
# Ignore browser build artifacts (if any)
|
||||||
|
website/js/dist/
|
||||||
|
website/js/build/
|
||||||
|
|
||||||
|
# Ignore backups at project root
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
*.zip
|
||||||
|
*.7z
|
||||||
@ -5,6 +5,8 @@
|
|||||||
* GET (no params) -> ordered questionnaire list with conditions
|
* GET (no params) -> ordered questionnaire list with conditions
|
||||||
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
||||||
* GET ?translations=1 -> all translations merged across all tables
|
* GET ?translations=1 -> all translations merged across all tables
|
||||||
|
*
|
||||||
|
* POST (coach interview upload) is handled by api/index.php -> handlers/app_questionnaires.php
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/../common.php';
|
require_once __DIR__ . '/../common.php';
|
||||||
require_once __DIR__ . '/../db_init.php';
|
require_once __DIR__ . '/../db_init.php';
|
||||||
|
|||||||
@ -4,10 +4,154 @@
|
|||||||
* GET (no params) -> ordered questionnaire list with conditions
|
* GET (no params) -> ordered questionnaire list with conditions
|
||||||
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
||||||
* GET ?translations=1 -> all translations merged across all tables
|
* GET ?translations=1 -> all translations merged across all tables
|
||||||
|
* POST -> submit interview answers for a client (coach / supervisor / admin)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$tokenRec = require_valid_token();
|
$tokenRec = require_valid_token();
|
||||||
|
|
||||||
|
if ($method === 'POST') {
|
||||||
|
require_role(['admin', 'supervisor', 'coach'], $tokenRec);
|
||||||
|
|
||||||
|
$body = read_json_body();
|
||||||
|
require_fields($body, ['questionnaireID', 'clientCode', 'answers']);
|
||||||
|
|
||||||
|
$qnID = trim($body['questionnaireID']);
|
||||||
|
$clientCode = trim($body['clientCode']);
|
||||||
|
$answers = $body['answers'];
|
||||||
|
|
||||||
|
if (!is_array($answers)) {
|
||||||
|
json_error('INVALID_FIELD', 'answers must be an array', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$startedAt = isset($body['startedAt']) ? (int)$body['startedAt'] : null;
|
||||||
|
$completedAt = isset($body['completedAt']) ? (int)$body['completedAt'] : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
|
||||||
|
|
||||||
|
// Verify questionnaire exists
|
||||||
|
$qnStmt = $pdo->prepare("SELECT questionnaireID FROM questionnaire WHERE questionnaireID = :id");
|
||||||
|
$qnStmt->execute([':id' => $qnID]);
|
||||||
|
if (!$qnStmt->fetch()) {
|
||||||
|
qdb_discard($tmpDb, $lockFp);
|
||||||
|
json_error('NOT_FOUND', 'Questionnaire not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||||
|
$clStmt = $pdo->prepare(
|
||||||
|
"SELECT cl.clientCode, cl.coachID FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"
|
||||||
|
);
|
||||||
|
$clStmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
|
||||||
|
$clientRow = $clStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$clientRow) {
|
||||||
|
qdb_discard($tmpDb, $lockFp);
|
||||||
|
json_error('NOT_FOUND', 'Client not found or not authorized', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignedByCoach = ($tokenRec['role'] === 'coach')
|
||||||
|
? ($tokenRec['entityID'] ?? '')
|
||||||
|
: ($clientRow['coachID'] ?? '');
|
||||||
|
|
||||||
|
// Load all questions for this questionnaire: full questionID keyed by short ID
|
||||||
|
$qRows = $pdo->prepare("SELECT questionID FROM question WHERE questionnaireID = :qn");
|
||||||
|
$qRows->execute([':qn' => $qnID]);
|
||||||
|
$shortIdMap = []; // shortId -> fullQuestionID
|
||||||
|
foreach ($qRows->fetchAll(PDO::FETCH_COLUMN) as $fullId) {
|
||||||
|
$parts = explode('__', $fullId);
|
||||||
|
$shortIdMap[end($parts)] = $fullId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all answer options for this questionnaire: keyed by [questionID][defaultText]
|
||||||
|
$aoRows = $pdo->prepare("
|
||||||
|
SELECT ao.answerOptionID, ao.questionID, ao.defaultText, ao.points
|
||||||
|
FROM answer_option ao
|
||||||
|
JOIN question q ON q.questionID = ao.questionID
|
||||||
|
WHERE q.questionnaireID = :qn
|
||||||
|
");
|
||||||
|
$aoRows->execute([':qn' => $qnID]);
|
||||||
|
$optionMap = []; // fullQuestionID -> [defaultText -> {answerOptionID, points}]
|
||||||
|
foreach ($aoRows->fetchAll(PDO::FETCH_ASSOC) as $ao) {
|
||||||
|
$optionMap[$ao['questionID']][$ao['defaultText']] = [
|
||||||
|
'answerOptionID' => $ao['answerOptionID'],
|
||||||
|
'points' => (int)$ao['points'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$sumPoints = 0;
|
||||||
|
$answerInsert = $pdo->prepare("
|
||||||
|
INSERT INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
||||||
|
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)
|
||||||
|
ON CONFLICT(clientCode, questionID) DO UPDATE SET
|
||||||
|
answerOptionID = excluded.answerOptionID,
|
||||||
|
freeTextValue = excluded.freeTextValue,
|
||||||
|
numericValue = excluded.numericValue,
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$answerInsert->execute([
|
||||||
|
':cc' => $clientCode,
|
||||||
|
':qid' => $fullQID,
|
||||||
|
':aoid' => $answerOptionID,
|
||||||
|
':ftv' => $freeTextValue,
|
||||||
|
':nv' => $numericValue,
|
||||||
|
':at' => $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)
|
||||||
|
ON CONFLICT(clientCode, questionnaireID) DO UPDATE SET
|
||||||
|
assignedByCoach = excluded.assignedByCoach,
|
||||||
|
status = 'completed',
|
||||||
|
startedAt = COALESCE(excluded.startedAt, startedAt),
|
||||||
|
completedAt = excluded.completedAt,
|
||||||
|
sumPoints = excluded.sumPoints
|
||||||
|
")->execute([
|
||||||
|
':cc' => $clientCode,
|
||||||
|
':qn' => $qnID,
|
||||||
|
':abc' => $assignedByCoach !== '' ? $assignedByCoach : null,
|
||||||
|
':sa' => $startedAt,
|
||||||
|
':ca' => $completedAt,
|
||||||
|
':sp' => $sumPoints,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
qdb_save($tmpDb, $lockFp);
|
||||||
|
|
||||||
|
json_success(['submitted' => true, 'sumPoints' => $sumPoints]);
|
||||||
|
} 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', 'Internal server error', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($method !== 'GET') {
|
if ($method !== 'GET') {
|
||||||
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user