updated api
This commit is contained in:
91
handlers/activity.php
Normal file
91
handlers/activity.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
$tokenRec = require_valid_token();
|
||||
require_role(['admin', 'supervisor'], $tokenRec);
|
||||
|
||||
if ($method !== 'GET') {
|
||||
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
||||
}
|
||||
|
||||
$limit = min(500, max(1, (int)($_GET['limit'] ?? 100)));
|
||||
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
||||
$since = isset($_GET['since']) ? (int)$_GET['since'] : null;
|
||||
$until = isset($_GET['until']) ? (int)$_GET['until'] : null;
|
||||
$action = trim($_GET['action'] ?? '');
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||
|
||||
$role = $tokenRec['role'] ?? '';
|
||||
$entityID = $tokenRec['entityID'] ?? '';
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if ($role === 'supervisor') {
|
||||
$where[] = "(
|
||||
(ae.actorRole = 'coach' AND ae.actorEntityID IN (
|
||||
SELECT coachID FROM coach WHERE supervisorID = :sup_id
|
||||
))
|
||||
OR (ae.targetType = 'client' AND ae.targetID IN (
|
||||
SELECT clientCode FROM client WHERE coachID IN (
|
||||
SELECT coachID FROM coach WHERE supervisorID = :sup_id2
|
||||
)
|
||||
))
|
||||
OR (ae.actorRole = 'supervisor' AND ae.actorEntityID = :sup_self)
|
||||
)";
|
||||
$params[':sup_id'] = $entityID;
|
||||
$params[':sup_id2'] = $entityID;
|
||||
$params[':sup_self'] = $entityID;
|
||||
}
|
||||
|
||||
if ($since !== null) {
|
||||
$where[] = 'ae.createdAt >= :since';
|
||||
$params[':since'] = $since;
|
||||
}
|
||||
if ($until !== null) {
|
||||
$where[] = 'ae.createdAt <= :until';
|
||||
$params[':until'] = $until;
|
||||
}
|
||||
if ($action !== '') {
|
||||
$where[] = 'ae.action = :action';
|
||||
$params[':action'] = $action;
|
||||
}
|
||||
|
||||
$whereSql = implode(' AND ', $where);
|
||||
|
||||
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM activity_event ae WHERE $whereSql");
|
||||
$countStmt->execute($params);
|
||||
$total = (int)$countStmt->fetchColumn();
|
||||
|
||||
$sql = "
|
||||
SELECT ae.eventID, ae.createdAt, ae.actorUserID, ae.actorUsername, ae.actorRole,
|
||||
ae.actorEntityID, ae.action, ae.targetType, ae.targetID, ae.summary, ae.detailsJson
|
||||
FROM activity_event ae
|
||||
WHERE $whereSql
|
||||
ORDER BY ae.createdAt DESC
|
||||
LIMIT :lim OFFSET :off
|
||||
";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v);
|
||||
}
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$events = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($events as &$e) {
|
||||
$e['createdAt'] = (int)$e['createdAt'];
|
||||
$e['details'] = json_decode($e['detailsJson'] ?? '{}', true) ?: [];
|
||||
unset($e['detailsJson']);
|
||||
}
|
||||
unset($e);
|
||||
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
|
||||
json_success([
|
||||
'events' => $events,
|
||||
'total' => $total,
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
]);
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/activity.php';
|
||||
|
||||
$tokenRec = require_valid_token();
|
||||
require_role(['admin', 'supervisor'], $tokenRec);
|
||||
|
||||
@ -89,6 +91,17 @@ case 'POST':
|
||||
$count += $updStmt->rowCount();
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
log_activity(
|
||||
$pdo,
|
||||
$tokenRec,
|
||||
'clients_assigned',
|
||||
'coach',
|
||||
$coachID,
|
||||
"Assigned $count client(s) to coach $coachID",
|
||||
['clientCodes' => $clientCodes, 'count' => $count]
|
||||
);
|
||||
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
json_success(['assigned' => $count]);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/activity.php';
|
||||
|
||||
switch ($route) {
|
||||
|
||||
case 'auth/login':
|
||||
@ -41,9 +43,8 @@ case 'auth/login':
|
||||
);
|
||||
}
|
||||
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
|
||||
if ((int)$user['mustChangePassword'] === 1) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
$tempToken = bin2hex(random_bytes(32));
|
||||
token_add($tempToken, 10 * 60, [
|
||||
'role' => $user['role'],
|
||||
@ -59,6 +60,17 @@ case 'auth/login':
|
||||
]);
|
||||
}
|
||||
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
|
||||
[$pdoW, $tmpDbW, $lockFpW] = qdb_open(true);
|
||||
$tokenRec = [
|
||||
'role' => $user['role'],
|
||||
'entityID' => $user['entityID'],
|
||||
'userID' => $user['userID'],
|
||||
];
|
||||
log_activity($pdoW, $tokenRec, 'login', 'user', $user['userID'], "Login: $username");
|
||||
qdb_save($tmpDbW, $lockFpW);
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
token_add($token, 30 * 24 * 60 * 60, [
|
||||
'role' => $user['role'],
|
||||
@ -73,6 +85,7 @@ case 'auth/login':
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp);
|
||||
if (isset($tmpDbW, $lockFpW)) qdb_discard($tmpDbW, $lockFpW);
|
||||
error_log($e->getMessage());
|
||||
json_error('SERVER_ERROR', 'Server error', 500);
|
||||
}
|
||||
@ -137,6 +150,15 @@ case 'auth/change-password':
|
||||
"UPDATE users SET passwordHash = :h, mustChangePassword = 0 WHERE userID = :uid"
|
||||
)->execute([':h' => $newHash, ':uid' => $user['userID']]);
|
||||
|
||||
log_activity(
|
||||
$pdo,
|
||||
array_merge($tokenRec, ['role' => $user['role'], 'entityID' => $user['entityID'], 'userID' => $user['userID']]),
|
||||
'password_changed',
|
||||
'user',
|
||||
$user['userID'],
|
||||
"Password changed: $username"
|
||||
);
|
||||
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
// Revoke the old (possibly temp) token and issue a fresh one
|
||||
|
||||
178
handlers/client_export.php
Normal file
178
handlers/client_export.php
Normal file
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/activity.php';
|
||||
|
||||
$tokenRec = require_valid_token();
|
||||
require_role(['admin', 'supervisor'], $tokenRec);
|
||||
|
||||
if ($method !== 'GET') {
|
||||
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
||||
}
|
||||
|
||||
$clientCode = trim($_GET['clientCode'] ?? '');
|
||||
$exportAll = (strpos($route, 'export_all') !== false) || isset($_GET['export_all']);
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
|
||||
$clientFilter = '';
|
||||
$params = $rbacParams;
|
||||
if (!$exportAll) {
|
||||
if ($clientCode === '') {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('MISSING_PARAM', 'clientCode query param required', 400);
|
||||
}
|
||||
$clientFilter = ' AND cl.clientCode = :cc';
|
||||
$params[':cc'] = $clientCode;
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT cl.clientCode, cl.coachID,
|
||||
co.username AS coachUsername,
|
||||
sv.username AS supervisorUsername
|
||||
FROM client cl
|
||||
LEFT JOIN coach co ON co.coachID = cl.coachID
|
||||
LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID
|
||||
WHERE ($rbacClause) $clientFilter
|
||||
ORDER BY cl.clientCode
|
||||
";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$clients = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$exportAll && empty($clients)) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'Client not found or not authorized', 404);
|
||||
}
|
||||
|
||||
$subStmt = $pdo->prepare("
|
||||
SELECT qs.submissionID, qs.clientCode, qs.questionnaireID, qs.submissionVersion,
|
||||
qs.status, qs.startedAt, qs.completedAt, qs.uploadedAt, qs.sumPoints,
|
||||
qs.submittedByRole, qn.name AS questionnaireName
|
||||
FROM questionnaire_submission qs
|
||||
JOIN questionnaire qn ON qn.questionnaireID = qs.questionnaireID
|
||||
WHERE qs.clientCode = :cc
|
||||
ORDER BY qs.uploadedAt, qs.submissionVersion
|
||||
");
|
||||
|
||||
$ansStmt = $pdo->prepare("
|
||||
SELECT sa.questionID, sa.answerOptionID, sa.freeTextValue, sa.numericValue,
|
||||
q.defaultText AS questionText, ao.defaultText AS optionText
|
||||
FROM submission_answer sa
|
||||
JOIN question q ON q.questionID = sa.questionID
|
||||
LEFT JOIN answer_option ao ON ao.answerOptionID = sa.answerOptionID
|
||||
WHERE sa.submissionID = :sid
|
||||
ORDER BY q.orderIndex
|
||||
");
|
||||
|
||||
$rows = [];
|
||||
foreach ($clients as $cl) {
|
||||
$cc = $cl['clientCode'];
|
||||
$subStmt->execute([':cc' => $cc]);
|
||||
foreach ($subStmt->fetchAll(PDO::FETCH_ASSOC) as $sub) {
|
||||
$ansStmt->execute([':sid' => $sub['submissionID']]);
|
||||
$answers = $ansStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (empty($answers)) {
|
||||
$rows[] = build_export_row($cl, $sub, null);
|
||||
continue;
|
||||
}
|
||||
foreach ($answers as $a) {
|
||||
$rows[] = build_export_row($cl, $sub, $a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
|
||||
try {
|
||||
[$pdoW, $tmpDbW, $lockFpW] = qdb_open(true);
|
||||
log_activity(
|
||||
$pdoW,
|
||||
$tokenRec,
|
||||
'client_data_exported',
|
||||
'client',
|
||||
$exportAll ? '*' : $clientCode,
|
||||
$exportAll ? 'Exported all client data' : "Exported data for $clientCode",
|
||||
['exportAll' => $exportAll, 'clientCode' => $exportAll ? null : $clientCode, 'rowCount' => count($rows)]
|
||||
);
|
||||
qdb_save($tmpDbW, $lockFpW);
|
||||
} catch (Throwable $e) {
|
||||
if (isset($tmpDbW, $lockFpW)) qdb_discard($tmpDbW, $lockFpW);
|
||||
error_log('Activity log failed on export: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$filename = $exportAll
|
||||
? 'clients_export_' . date('Y-m-d') . '.csv'
|
||||
: 'client_' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $clientCode) . '_export.csv';
|
||||
|
||||
header('Content-Type: text/csv; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
echo "\xEF\xBB\xBF";
|
||||
|
||||
$out = fopen('php://output', 'w');
|
||||
$headers = [
|
||||
'clientCode', 'coach', 'supervisor', 'questionnaireID', 'questionnaireName',
|
||||
'submissionVersion', 'submissionID', 'status', 'sumPoints',
|
||||
'startedAt', 'completedAt', 'uploadedAt', 'submittedByRole',
|
||||
'questionText', 'answerOption', 'freeTextValue', 'numericValue',
|
||||
];
|
||||
fputcsv($out, $headers);
|
||||
foreach ($rows as $r) {
|
||||
fputcsv($out, [
|
||||
$r['clientCode'],
|
||||
$r['coach'],
|
||||
$r['supervisor'],
|
||||
$r['questionnaireID'],
|
||||
$r['questionnaireName'],
|
||||
$r['submissionVersion'],
|
||||
$r['submissionID'],
|
||||
$r['status'],
|
||||
$r['sumPoints'],
|
||||
$r['startedAt'],
|
||||
$r['completedAt'],
|
||||
$r['uploadedAt'],
|
||||
$r['submittedByRole'],
|
||||
$r['questionText'],
|
||||
$r['answerOption'],
|
||||
$r['freeTextValue'],
|
||||
$r['numericValue'],
|
||||
]);
|
||||
}
|
||||
fclose($out);
|
||||
exit;
|
||||
|
||||
function build_export_row(array $cl, array $sub, ?array $a): array {
|
||||
$fmt = static function ($ms) {
|
||||
if ($ms === null || $ms === '') return '';
|
||||
return date('Y-m-d H:i', (int)($ms / 1000));
|
||||
};
|
||||
$answerVal = '';
|
||||
if ($a) {
|
||||
if (!empty($a['optionText'])) {
|
||||
$answerVal = $a['optionText'];
|
||||
} elseif ($a['freeTextValue'] !== null && $a['freeTextValue'] !== '') {
|
||||
$answerVal = $a['freeTextValue'];
|
||||
} elseif ($a['numericValue'] !== null) {
|
||||
$answerVal = (string)$a['numericValue'];
|
||||
}
|
||||
}
|
||||
return [
|
||||
'clientCode' => $cl['clientCode'],
|
||||
'coach' => $cl['coachUsername'] ?? $cl['coachID'] ?? '',
|
||||
'supervisor' => $cl['supervisorUsername'] ?? '',
|
||||
'questionnaireID' => $sub['questionnaireID'],
|
||||
'questionnaireName' => $sub['questionnaireName'] ?? '',
|
||||
'submissionVersion' => (int)$sub['submissionVersion'],
|
||||
'submissionID' => $sub['submissionID'],
|
||||
'status' => $sub['status'],
|
||||
'sumPoints' => $sub['sumPoints'] !== null ? (int)$sub['sumPoints'] : '',
|
||||
'startedAt' => $fmt($sub['startedAt']),
|
||||
'completedAt' => $fmt($sub['completedAt']),
|
||||
'uploadedAt' => $fmt($sub['uploadedAt']),
|
||||
'submittedByRole' => $sub['submittedByRole'] ?? '',
|
||||
'questionText' => $a['questionText'] ?? '',
|
||||
'answerOption' => $answerVal,
|
||||
'freeTextValue' => $a['freeTextValue'] ?? '',
|
||||
'numericValue' => $a['numericValue'] !== null ? $a['numericValue'] : '',
|
||||
];
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/activity.php';
|
||||
|
||||
$tokenRec = require_valid_token();
|
||||
require_role(['admin', 'supervisor'], $tokenRec);
|
||||
|
||||
@ -68,6 +70,8 @@ case 'POST':
|
||||
$pdo->prepare("INSERT INTO client (clientCode, coachID) VALUES (:cc, :cid)")
|
||||
->execute([':cc' => $clientCode, ':cid' => $coachID]);
|
||||
|
||||
log_activity($pdo, $tokenRec, 'client_created', 'client', $clientCode, "Created client $clientCode", ['coachID' => $coachID]);
|
||||
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['clientCode' => $clientCode, 'coachID' => $coachID]);
|
||||
} catch (Throwable $e) {
|
||||
@ -100,6 +104,13 @@ case 'DELETE':
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("
|
||||
DELETE FROM submission_answer WHERE submissionID IN (
|
||||
SELECT submissionID FROM questionnaire_submission WHERE clientCode = :cc
|
||||
)
|
||||
")->execute([':cc' => $clientCode]);
|
||||
$pdo->prepare("DELETE FROM questionnaire_submission WHERE clientCode = :cc")
|
||||
->execute([':cc' => $clientCode]);
|
||||
$pdo->prepare("DELETE FROM client_answer WHERE clientCode = :cc")
|
||||
->execute([':cc' => $clientCode]);
|
||||
$pdo->prepare("DELETE FROM completed_questionnaire WHERE clientCode = :cc")
|
||||
@ -108,6 +119,8 @@ case 'DELETE':
|
||||
->execute([':cc' => $clientCode]);
|
||||
$pdo->commit();
|
||||
|
||||
log_activity($pdo, $tokenRec, 'client_deleted', 'client', $clientCode, "Deleted client $clientCode");
|
||||
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success([]);
|
||||
} catch (Throwable $e) {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/activity.php';
|
||||
|
||||
$tokenRec = require_valid_token();
|
||||
require_role(['admin', 'supervisor'], $tokenRec);
|
||||
|
||||
@ -139,6 +141,9 @@ case 'POST':
|
||||
':role' => $role, ':eid' => $entityID, ':mcp' => $mustChange, ':now' => $now,
|
||||
]);
|
||||
$pdo->commit();
|
||||
|
||||
log_activity($pdo, $tokenRec, 'user_created', 'user', $userID, "Created $role user $username", ['role' => $role, 'entityID' => $entityID]);
|
||||
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
$svUsername = null;
|
||||
@ -219,6 +224,9 @@ case 'DELETE':
|
||||
break;
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
log_activity($pdo, $tokenRec, 'user_deleted', 'user', $targetUserID, "Deleted {$target['role']} user", ['role' => $target['role']]);
|
||||
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
json_success([]);
|
||||
|
||||
Reference in New Issue
Block a user