removed legacy files

This commit is contained in:
2026-05-27 18:32:00 +02:00
parent 0f98393d1d
commit 9c887537e8
45 changed files with 91 additions and 6067 deletions

View File

@ -1,185 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
$method = $_SERVER['REQUEST_METHOD'];
switch ($method) {
case 'GET':
$qID = $_GET['questionID'] ?? '';
if (!$qID) {
http_response_code(400);
echo json_encode(["error" => "questionID query param required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$stmt = $pdo->prepare("
SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex
");
$stmt->execute([':qid' => $qID]);
$options = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($options as &$o) {
$o['points'] = (int)$o['points'];
$o['orderIndex'] = (int)$o['orderIndex'];
$tr = $pdo->prepare("SELECT languageCode, text FROM answer_option_translation WHERE answerOptionID = :id");
$tr->execute([':id' => $o['answerOptionID']]);
$o['translations'] = $tr->fetchAll(PDO::FETCH_ASSOC);
}
unset($o);
qdb_discard($tmpDb, $lockFp);
echo json_encode(["success" => true, "answerOptions" => $options]);
break;
case 'POST':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionID']) || !isset($body['defaultText'])) {
http_response_code(400);
echo json_encode(["error" => "questionID and defaultText required"]);
exit;
}
$id = bin2hex(random_bytes(16));
$qID = $body['questionID'];
$text = trim($body['defaultText']);
$points = (int)($body['points'] ?? 0);
$order = (int)($body['orderIndex'] ?? 0);
$nextQ = trim($body['nextQuestionId'] ?? '');
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$chk = $pdo->prepare("SELECT 1 FROM question WHERE questionID = :id");
$chk->execute([':id' => $qID]);
if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "Question not found"]);
exit;
}
if ($order === 0) {
$max = $pdo->prepare("SELECT COALESCE(MAX(orderIndex),0)+1 FROM answer_option WHERE questionID = :id");
$max->execute([':id' => $qID]);
$order = (int)$max->fetchColumn();
}
$pdo->prepare("INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId)
VALUES (:id, :qid, :t, :p, :o, :nq)")
->execute([':id' => $id, ':qid' => $qID, ':t' => $text, ':p' => $points, ':o' => $order, ':nq' => $nextQ]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true, "answerOption" => [
"answerOptionID" => $id, "questionID" => $qID, "defaultText" => $text,
"points" => $points, "orderIndex" => $order, "nextQuestionId" => $nextQ, "translations" => []
]]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'PUT':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['answerOptionID'])) {
http_response_code(400);
echo json_encode(["error" => "answerOptionID is required"]);
exit;
}
$id = $body['answerOptionID'];
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$existing = $pdo->prepare("SELECT * FROM answer_option WHERE answerOptionID = :id");
$existing->execute([':id' => $id]);
$row = $existing->fetch(PDO::FETCH_ASSOC);
if (!$row) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "Answer option not found"]);
exit;
}
$text = trim($body['defaultText'] ?? $row['defaultText']);
$points = (int)($body['points'] ?? $row['points']);
$order = (int)($body['orderIndex'] ?? $row['orderIndex']);
$nextQ = trim($body['nextQuestionId'] ?? $row['nextQuestionId']);
$pdo->prepare("UPDATE answer_option SET defaultText = :t, points = :p, orderIndex = :o, nextQuestionId = :nq WHERE answerOptionID = :id")
->execute([':t' => $text, ':p' => $points, ':o' => $order, ':nq' => $nextQ, ':id' => $id]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true, "answerOption" => [
"answerOptionID" => $id, "questionID" => $row['questionID'],
"defaultText" => $text, "points" => $points, "orderIndex" => $order,
"nextQuestionId" => $nextQ
]]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'DELETE':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['answerOptionID'])) {
http_response_code(400);
echo json_encode(["error" => "answerOptionID is required"]);
exit;
}
$id = $body['answerOptionID'];
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$pdo->exec("PRAGMA foreign_keys = OFF;");
$pdo->beginTransaction();
$pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id")->execute([':id' => $id]);
$pdo->prepare("UPDATE client_answer SET answerOptionID = NULL WHERE answerOptionID = :id")->execute([':id' => $id]);
$pdo->prepare("DELETE FROM answer_option WHERE answerOptionID = :id")->execute([':id' => $id]);
$pdo->commit();
$pdo->exec("PRAGMA foreign_keys = ON;");
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'PATCH':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionID']) || !is_array($body['order'] ?? null)) {
http_response_code(400);
echo json_encode(["error" => "questionID and order[] required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$stmt = $pdo->prepare("UPDATE answer_option SET orderIndex = :o WHERE answerOptionID = :id AND questionID = :qid");
foreach ($body['order'] as $idx => $aoid) {
$stmt->execute([':o' => $idx, ':id' => $aoid, ':qid' => $body['questionID']]);
}
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
default:
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
}

View File

@ -1,160 +0,0 @@
<?php
/**
* Android app API endpoint.
*
* GET (no params) -> ordered questionnaire list with conditions
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
* GET ?translations=1 -> global app UI strings only; **no auth** (login screen).
*
* POST (coach interview upload) is handled by api/index.php -> handlers/app_questionnaires.php
*/
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
require_once __DIR__ . '/../lib/response.php';
header('Content-Type: application/json; charset=UTF-8');
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET' && !empty($_GET['translations'])) {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
json_success(['translations' => qdb_build_app_translations_map($pdo)]);
qdb_discard($tmpDb, $lockFp);
exit;
}
$tokenRec = require_valid_token();
if ($method !== 'GET') {
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
}
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$qnID = $_GET['id'] ?? '';
if ($qnID) {
// Single questionnaire in app JSON format
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
$qn->execute([':id' => $qnID]);
$qnRow = $qn->fetch(PDO::FETCH_ASSOC);
if (!$qnRow) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "Questionnaire not found"]);
exit;
}
$stmt = $pdo->prepare("
SELECT questionID, defaultText, type, orderIndex, configJson
FROM question WHERE questionnaireID = :id ORDER BY orderIndex
");
$stmt->execute([':id' => $qnID]);
$dbQuestions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$questions = [];
foreach ($dbQuestions as $dbQ) {
$localId = $dbQ['questionID'];
$parts = explode('__', $localId);
$shortId = end($parts);
$layout = $dbQ['type'];
$config = qdb_parse_config_json($dbQ['configJson']);
$qKey = qdb_question_key($config, $dbQ['defaultText']);
$q = [
'id' => $shortId,
'layout' => $layout,
'question' => $qKey !== '' ? $qKey : $dbQ['defaultText'],
];
if ($qKey !== '' && !empty($config['noteBefore'])) {
$q['noteBeforeKey'] = qdb_note_before_key($qKey);
}
if ($qKey !== '' && !empty($config['noteAfter'])) {
$q['noteAfterKey'] = qdb_note_after_key($qKey);
}
if (isset($config['textKey'])) $q['textKey'] = $config['textKey'];
if (isset($config['textKey1'])) $q['textKey1'] = $config['textKey1'];
if (isset($config['textKey2'])) $q['textKey2'] = $config['textKey2'];
if (isset($config['hint'])) $q['hint'] = $config['hint'];
if (isset($config['hint1'])) $q['hint1'] = $config['hint1'];
if (isset($config['hint2'])) $q['hint2'] = $config['hint2'];
if (isset($config['symptoms'])) $q['symptoms'] = $config['symptoms'];
if (isset($config['scaleType'])) $q['scaleType'] = $config['scaleType'];
if (isset($config['range'])) $q['range'] = $config['range'];
if (isset($config['step'])) $q['step'] = (int)$config['step'];
if (isset($config['unitLabel'])) $q['unitLabel'] = $config['unitLabel'];
if (isset($config['constraints'])) $q['constraints'] = $config['constraints'];
if (isset($config['precision'])) $q['precision'] = $config['precision'];
if (isset($config['minSelection'])) $q['minSelection'] = $config['minSelection'];
if (isset($config['maxLength'])) $q['maxLength'] = (int)$config['maxLength'];
if (isset($config['nextQuestionId'])) $q['nextQuestionId'] = $config['nextQuestionId'];
if (isset($config['otherNextQuestionId'])) $q['otherNextQuestionId'] = $config['otherNextQuestionId'];
if (isset($config['otherOptionKey'])) $q['otherOptionKey'] = $config['otherOptionKey'];
if ($layout === 'string_spinner' && isset($config['options'])) {
$q['options'] = $config['options'];
}
if (($layout === 'value_spinner' || $layout === 'slider_question') && isset($config['valueOptions'])) {
$q['options'] = $config['valueOptions'];
}
// Answer options (for radio_question, multi_check_box_question, etc.)
$ao = $pdo->prepare("
SELECT defaultText, points, nextQuestionId
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex
");
$ao->execute([':qid' => $dbQ['questionID']]);
$opts = $ao->fetchAll(PDO::FETCH_ASSOC);
if ($opts) {
$optionsArr = [];
$pointsMap = [];
foreach ($opts as $opt) {
$o = ['key' => $opt['defaultText']];
if ($opt['nextQuestionId'] !== '') {
$o['nextQuestionId'] = $opt['nextQuestionId'];
}
$optionsArr[] = $o;
$pointsMap[$opt['defaultText']] = (int)$opt['points'];
}
$q['options'] = $optionsArr;
$hasNonZero = false;
foreach ($pointsMap as $v) { if ($v !== 0) { $hasNonZero = true; break; } }
$q['pointsMap'] = $pointsMap;
}
$questions[] = $q;
}
qdb_discard($tmpDb, $lockFp);
echo json_encode([
'meta' => ['id' => $qnID],
'questions' => $questions,
'translations' => qdb_build_questionnaire_translations_map($pdo, $qnID),
]);
exit;
}
// Default: ordered questionnaire list
$rows = $pdo->query("
SELECT questionnaireID, name, showPoints, conditionJson
FROM questionnaire
WHERE state = 'active'
ORDER BY orderIndex
")->fetchAll(PDO::FETCH_ASSOC);
$list = [];
foreach ($rows as $r) {
$list[] = [
'id' => $r['questionnaireID'],
'name' => $r['name'],
'showPoints' => (bool)(int)$r['showPoints'],
'condition' => json_decode($r['conditionJson'], true) ?: new stdClass(),
];
}
qdb_discard($tmpDb, $lockFp);
echo json_encode($list);

View File

@ -1,116 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
$callerRole = $tokenRec['role'];
$callerEntityID = $tokenRec['entityID'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
// ── GET: coaches + clients scoped by RBAC ────────────────────────────────
if ($method === 'GET') {
try {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
if ($callerRole === 'admin') {
$coaches = $pdo->query(
"SELECT c.coachID, c.username, c.supervisorID, sv.username AS supervisorUsername
FROM coach c
LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID
ORDER BY sv.username, c.username"
)->fetchAll(PDO::FETCH_ASSOC);
} else {
$stmt = $pdo->prepare(
"SELECT c.coachID, c.username, c.supervisorID, sv.username AS supervisorUsername
FROM coach c
LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID
WHERE c.supervisorID = :sid ORDER BY c.username"
);
$stmt->execute([':sid' => $callerEntityID]);
$coaches = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
[$clause, $params] = rbac_client_filter($tokenRec);
$clientStmt = $pdo->prepare(
"SELECT cl.clientCode, cl.coachID, co.username AS coachUsername
FROM client cl
LEFT JOIN coach co ON co.coachID = cl.coachID
WHERE $clause
ORDER BY cl.coachID, cl.clientCode"
);
foreach ($params as $k => $v) $clientStmt->bindValue($k, $v);
$clientStmt->execute();
$clients = $clientStmt->fetchAll(PDO::FETCH_ASSOC);
qdb_discard($tmpDb, $lockFp);
echo json_encode(["success" => true, "coaches" => $coaches, "clients" => $clients]);
} catch (Throwable $e) {
if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
exit;
}
// ── POST: assign clients to a coach ──────────────────────────────────────
if ($method === 'POST') {
$in = json_decode(file_get_contents('php://input'), true) ?: [];
$clientCodes = $in['clientCodes'] ?? [];
$coachID = trim($in['coachID'] ?? '');
if (empty($clientCodes) || $coachID === '') {
http_response_code(400);
echo json_encode(["error" => "clientCodes and coachID required"]);
exit;
}
if (!is_array($clientCodes)) $clientCodes = [$clientCodes];
try {
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
if ($callerRole === 'supervisor') {
$chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :sid");
$chk->execute([':cid' => $coachID, ':sid' => $callerEntityID]);
} else {
$chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid");
$chk->execute([':cid' => $coachID]);
}
if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(400);
echo json_encode(["error" => "Coach not found or not authorized"]);
exit;
}
// Only allow reassignment of clients the caller can already see
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec);
$pdo->beginTransaction();
$updStmt = $pdo->prepare(
"UPDATE client SET coachID = :cid WHERE clientCode = :cc AND ($rbacClause)"
);
$count = 0;
foreach ($clientCodes as $cc) {
$cc = trim($cc);
if ($cc === '') continue;
$updStmt->execute(array_merge([':cid' => $coachID, ':cc' => $cc], $rbacParams));
$count += $updStmt->rowCount();
}
$pdo->commit();
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true, "assigned" => $count]);
} catch (Throwable $e) {
if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
exit;
}
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);

View File

@ -1,152 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
$tokenRec = require_valid_token_web();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
header('Content-Type: application/json; charset=UTF-8');
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
exit;
}
if (!empty($_GET['bundle'])) {
$role = $tokenRec['role'] ?? '';
if (!in_array($role, ['admin', 'supervisor'], true)) {
header('Content-Type: application/json; charset=UTF-8');
http_response_code(403);
echo json_encode(["error" => "Permission denied"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$bundle = qdb_export_all_questionnaires_bundle($pdo);
qdb_discard($tmpDb, $lockFp);
$filename = 'questionnaires_bundle_' . date('Y-m-d_His') . '.json';
header('Content-Type: application/json; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
$qnID = $_GET['questionnaireID'] ?? '';
$format = strtolower($_GET['format'] ?? 'csv');
if (!$qnID) {
header('Content-Type: application/json; charset=UTF-8');
http_response_code(400);
echo json_encode(["error" => "questionnaireID query param required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
// Questionnaire metadata
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
$qn->execute([':id' => $qnID]);
$questionnaire = $qn->fetch(PDO::FETCH_ASSOC);
if (!$questionnaire) {
qdb_discard($tmpDb, $lockFp);
header('Content-Type: application/json; charset=UTF-8');
http_response_code(404);
echo json_encode(["error" => "Questionnaire not found"]);
exit;
}
// Questions ordered
$qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex, configJson FROM question WHERE questionnaireID = :id ORDER BY orderIndex");
$qStmt->execute([':id' => $qnID]);
$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
$questionIDs = array_column($questions, 'questionID');
$resultColumns = qdb_results_export_columns($questions, $qnID);
// Build answer-option lookup for resolving display text
$optionTextMap = [];
foreach ($questionIDs as $qid) {
$aoStmt = $pdo->prepare("SELECT answerOptionID, defaultText FROM answer_option WHERE questionID = :qid");
$aoStmt->execute([':qid' => $qid]);
foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) {
$optionTextMap[$ao['answerOptionID']] = $ao['defaultText'];
}
}
// RBAC filter
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
$sql = "
SELECT cl.clientCode, cl.coachID,
co.username AS coachUsername,
sv.username AS supervisorUsername,
cq.status, cq.sumPoints, cq.startedAt, cq.completedAt
FROM client cl
INNER JOIN completed_questionnaire cq ON cq.clientCode = cl.clientCode AND cq.questionnaireID = :qnid
LEFT JOIN coach co ON co.coachID = cl.coachID
LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID
WHERE $rbacClause
ORDER BY cl.clientCode
";
$params = array_merge([':qnid' => $qnID], $rbacParams);
$cStmt = $pdo->prepare($sql);
$cStmt->execute($params);
$clients = $cStmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch answers for each client
$qPlaceholders = !empty($questionIDs) ? implode(',', array_fill(0, count($questionIDs), '?')) : "'__none__'";
$answerStmt = $pdo->prepare("
SELECT questionID, answerOptionID, freeTextValue, numericValue
FROM client_answer
WHERE clientCode = ? AND questionID IN ($qPlaceholders)
");
$rows = [];
foreach ($clients as $c) {
$row = [
'clientCode' => $c['clientCode'],
'coach' => $c['coachUsername'] ?? $c['coachID'],
'supervisor' => $c['supervisorUsername'] ?? '',
'status' => $c['status'],
'sumPoints' => $c['sumPoints'],
'startedAt' => $c['startedAt'] ? date('Y-m-d H:i', (int)$c['startedAt']) : '',
'completedAt'=> $c['completedAt'] ? date('Y-m-d H:i', (int)$c['completedAt']) : '',
];
$bindParams = array_merge([$c['clientCode']], $questionIDs);
$answerStmt->execute($bindParams);
$answers = $answerStmt->fetchAll(PDO::FETCH_ASSOC);
$answerMap = [];
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
foreach ($resultColumns as $col) {
$qid = $col['questionID'];
$a = $answerMap[$qid] ?? null;
$row[$col['header']] = qdb_results_column_cell_value($col, $a, $optionTextMap);
}
$rows[] = $row;
}
qdb_discard($tmpDb, $lockFp);
// Output CSV
$safeName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $questionnaire['name']);
$filename = $safeName . '_v' . $questionnaire['version'] . '.csv';
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
$out = fopen('php://output', 'w');
// BOM for Excel UTF-8 recognition
fwrite($out, "\xEF\xBB\xBF");
if (!empty($rows)) {
fputcsv($out, array_keys($rows[0]));
foreach ($rows as $row) {
fputcsv($out, array_values($row));
}
} else {
$header = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'startedAt', 'completedAt'];
foreach ($resultColumns as $col) {
$header[] = $col['header'];
}
fputcsv($out, $header);
}
fclose($out);

View File

@ -48,7 +48,6 @@ $routes = [
'backup' => __DIR__ . '/../handlers/backup.php',
'dev' => __DIR__ . '/../handlers/dev.php',
'dev/import' => __DIR__ . '/../handlers/dev.php',
'download' => __DIR__ . '/../handlers/download.php',
];
try {

View File

@ -1,21 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
if ($_SERVER['REQUEST_METHOD'] !== 'DELETE' && $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
exit;
}
$token = get_bearer_token();
if (!$token) {
http_response_code(401);
echo json_encode(["error" => "Missing Bearer token"]);
exit;
}
token_revoke($token);
echo json_encode(["success" => true]);

View File

@ -1,169 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
$method = $_SERVER['REQUEST_METHOD'];
switch ($method) {
case 'GET':
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$rows = $pdo->query("
SELECT q.questionnaireID, q.name, q.version, q.state,
q.orderIndex, q.showPoints, q.conditionJson,
COUNT(qu.questionID) AS questionCount
FROM questionnaire q
LEFT JOIN question qu ON qu.questionnaireID = q.questionnaireID
GROUP BY q.questionnaireID
ORDER BY q.orderIndex, q.name
")->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as &$r) {
$r['orderIndex'] = (int)$r['orderIndex'];
$r['showPoints'] = (int)$r['showPoints'];
$r['conditionJson'] = $r['conditionJson'] ?: '{}';
}
unset($r);
qdb_discard($tmpDb, $lockFp);
echo json_encode(["success" => true, "questionnaires" => $rows]);
break;
case 'POST':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['name'])) {
http_response_code(400);
echo json_encode(["error" => "name is required"]);
exit;
}
$id = bin2hex(random_bytes(16));
$name = trim($body['name']);
$version = trim($body['version'] ?? '');
$state = trim($body['state'] ?? 'draft');
$order = (int)($body['orderIndex'] ?? 0);
$showPts = (int)($body['showPoints'] ?? 0);
$condJson = $body['conditionJson'] ?? '{}';
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
if ($order === 0) {
$order = (int)$pdo->query("SELECT COALESCE(MAX(orderIndex),0)+1 FROM questionnaire")->fetchColumn();
}
$pdo->prepare("INSERT INTO questionnaire (questionnaireID, name, version, state, orderIndex, showPoints, conditionJson)
VALUES (:id, :n, :v, :s, :o, :sp, :cj)")
->execute([':id' => $id, ':n' => $name, ':v' => $version, ':s' => $state,
':o' => $order, ':sp' => $showPts, ':cj' => $condJson]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true, "questionnaire" => [
"questionnaireID" => $id, "name" => $name, "version" => $version,
"state" => $state, "orderIndex" => $order, "showPoints" => $showPts,
"conditionJson" => $condJson, "questionCount" => 0
]]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'PUT':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionnaireID'])) {
http_response_code(400);
echo json_encode(["error" => "questionnaireID is required"]);
exit;
}
$id = $body['questionnaireID'];
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$existing = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
$existing->execute([':id' => $id]);
$row = $existing->fetch(PDO::FETCH_ASSOC);
if (!$row) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "Questionnaire not found"]);
exit;
}
$name = trim($body['name'] ?? $row['name']);
$version = trim($body['version'] ?? $row['version']);
$state = trim($body['state'] ?? $row['state']);
$order = (int)($body['orderIndex'] ?? $row['orderIndex']);
$showPts = (int)($body['showPoints'] ?? $row['showPoints']);
$condJson = $body['conditionJson'] ?? $row['conditionJson'];
$pdo->prepare("UPDATE questionnaire SET name = :n, version = :v, state = :s,
orderIndex = :o, showPoints = :sp, conditionJson = :cj
WHERE questionnaireID = :id")
->execute([':n' => $name, ':v' => $version, ':s' => $state,
':o' => $order, ':sp' => $showPts, ':cj' => $condJson, ':id' => $id]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true, "questionnaire" => [
"questionnaireID" => $id, "name" => $name, "version" => $version, "state" => $state,
"orderIndex" => $order, "showPoints" => $showPts, "conditionJson" => $condJson
]]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'DELETE':
require_role(['admin'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionnaireID'])) {
http_response_code(400);
echo json_encode(["error" => "questionnaireID is required"]);
exit;
}
$id = $body['questionnaireID'];
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$pdo->exec("PRAGMA foreign_keys = OFF;");
$pdo->beginTransaction();
// Get question IDs to cascade-delete answer options and translations
$qIds = $pdo->prepare("SELECT questionID FROM question WHERE questionnaireID = :id");
$qIds->execute([':id' => $id]);
$questionIDs = $qIds->fetchAll(PDO::FETCH_COLUMN);
foreach ($questionIDs as $qid) {
$aoIds = $pdo->prepare("SELECT answerOptionID FROM answer_option WHERE questionID = :qid");
$aoIds->execute([':qid' => $qid]);
$optionIDs = $aoIds->fetchAll(PDO::FETCH_COLUMN);
foreach ($optionIDs as $aoid) {
$pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id")->execute([':id' => $aoid]);
}
$pdo->prepare("DELETE FROM answer_option WHERE questionID = :qid")->execute([':qid' => $qid]);
$pdo->prepare("DELETE FROM question_translation WHERE questionID = :qid")->execute([':qid' => $qid]);
$pdo->prepare("DELETE FROM client_answer WHERE questionID = :qid")->execute([':qid' => $qid]);
}
$pdo->prepare("DELETE FROM question WHERE questionnaireID = :id")->execute([':id' => $id]);
$pdo->prepare("DELETE FROM completed_questionnaire WHERE questionnaireID = :id")->execute([':id' => $id]);
$pdo->prepare("DELETE FROM questionnaire WHERE questionnaireID = :id")->execute([':id' => $id]);
$pdo->commit();
$pdo->exec("PRAGMA foreign_keys = ON;");
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
default:
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
}

View File

@ -1,215 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
$method = $_SERVER['REQUEST_METHOD'];
switch ($method) {
case 'GET':
$qnID = $_GET['questionnaireID'] ?? '';
if (!$qnID) {
http_response_code(400);
echo json_encode(["error" => "questionnaireID query param required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$stmt = $pdo->prepare("
SELECT questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson
FROM question WHERE questionnaireID = :qid ORDER BY orderIndex
");
$stmt->execute([':qid' => $qnID]);
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($questions as &$q) {
$q['isRequired'] = (int)$q['isRequired'];
$q['orderIndex'] = (int)$q['orderIndex'];
$q['configJson'] = $q['configJson'] ?: '{}';
$ao = $pdo->prepare("
SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex
");
$ao->execute([':qid' => $q['questionID']]);
$q['answerOptions'] = $ao->fetchAll(PDO::FETCH_ASSOC);
foreach ($q['answerOptions'] as &$opt) {
$opt['points'] = (int)$opt['points'];
$opt['orderIndex'] = (int)$opt['orderIndex'];
}
$tr = $pdo->prepare("SELECT languageCode, text FROM question_translation WHERE questionID = :qid");
$tr->execute([':qid' => $q['questionID']]);
$q['translations'] = $tr->fetchAll(PDO::FETCH_ASSOC);
}
unset($q);
qdb_discard($tmpDb, $lockFp);
echo json_encode(["success" => true, "questions" => $questions]);
break;
case 'POST':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionnaireID']) || !isset($body['defaultText'])) {
http_response_code(400);
echo json_encode(["error" => "questionnaireID and defaultText required"]);
exit;
}
$id = bin2hex(random_bytes(16));
$qnID = $body['questionnaireID'];
$text = trim($body['defaultText']);
$type = trim($body['type'] ?? '');
$order = (int)($body['orderIndex'] ?? 0);
$req = (int)($body['isRequired'] ?? 0);
$config = $body['configJson'] ?? '{}';
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$chk = $pdo->prepare("SELECT 1 FROM questionnaire WHERE questionnaireID = :id");
$chk->execute([':id' => $qnID]);
if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "Questionnaire not found"]);
exit;
}
if ($order === 0) {
$max = $pdo->prepare("SELECT COALESCE(MAX(orderIndex),0)+1 FROM question WHERE questionnaireID = :id");
$max->execute([':id' => $qnID]);
$order = (int)$max->fetchColumn();
}
$pdo->prepare("INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson)
VALUES (:id, :qid, :t, :ty, :o, :r, :cj)")
->execute([':id' => $id, ':qid' => $qnID, ':t' => $text, ':ty' => $type,
':o' => $order, ':r' => $req, ':cj' => $config]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true, "question" => [
"questionID" => $id, "questionnaireID" => $qnID, "defaultText" => $text,
"type" => $type, "orderIndex" => $order, "isRequired" => $req,
"configJson" => $config, "answerOptions" => [], "translations" => []
]]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'PUT':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionID'])) {
http_response_code(400);
echo json_encode(["error" => "questionID is required"]);
exit;
}
$id = $body['questionID'];
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$existing = $pdo->prepare("SELECT * FROM question WHERE questionID = :id");
$existing->execute([':id' => $id]);
$row = $existing->fetch(PDO::FETCH_ASSOC);
if (!$row) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "Question not found"]);
exit;
}
$text = trim($body['defaultText'] ?? $row['defaultText']);
$type = trim($body['type'] ?? $row['type']);
$order = (int)($body['orderIndex'] ?? $row['orderIndex']);
$req = (int)($body['isRequired'] ?? $row['isRequired']);
$config = $body['configJson'] ?? $row['configJson'];
$pdo->prepare("UPDATE question SET defaultText = :t, type = :ty, orderIndex = :o, isRequired = :r, configJson = :cj WHERE questionID = :id")
->execute([':t' => $text, ':ty' => $type, ':o' => $order, ':r' => $req, ':cj' => $config, ':id' => $id]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true, "question" => [
"questionID" => $id, "questionnaireID" => $row['questionnaireID'],
"defaultText" => $text, "type" => $type, "orderIndex" => $order, "isRequired" => $req,
"configJson" => $config
]]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'DELETE':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionID'])) {
http_response_code(400);
echo json_encode(["error" => "questionID is required"]);
exit;
}
$id = $body['questionID'];
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$pdo->exec("PRAGMA foreign_keys = OFF;");
$pdo->beginTransaction();
$aoIds = $pdo->prepare("SELECT answerOptionID FROM answer_option WHERE questionID = :id");
$aoIds->execute([':id' => $id]);
foreach ($aoIds->fetchAll(PDO::FETCH_COLUMN) as $aoid) {
$pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id")->execute([':id' => $aoid]);
}
$pdo->prepare("DELETE FROM answer_option WHERE questionID = :id")->execute([':id' => $id]);
$pdo->prepare("DELETE FROM question_translation WHERE questionID = :id")->execute([':id' => $id]);
$pdo->prepare("DELETE FROM client_answer WHERE questionID = :id")->execute([':id' => $id]);
$pdo->prepare("DELETE FROM question WHERE questionID = :id")->execute([':id' => $id]);
$pdo->commit();
$pdo->exec("PRAGMA foreign_keys = ON;");
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'PATCH':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['questionnaireID']) || !is_array($body['order'] ?? null)) {
http_response_code(400);
echo json_encode(["error" => "questionnaireID and order[] required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$stmt = $pdo->prepare("UPDATE question SET orderIndex = :o WHERE questionID = :id AND questionnaireID = :qid");
foreach ($body['order'] as $idx => $qid) {
$stmt->execute([':o' => $idx, ':id' => $qid, ':qid' => $body['questionnaireID']]);
}
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
default:
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
}

View File

@ -1,143 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
exit;
}
$qnID = $_GET['questionnaireID'] ?? '';
$clientCode = $_GET['clientCode'] ?? '';
if (!$qnID) {
http_response_code(400);
echo json_encode(["error" => "questionnaireID query param required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
// Questionnaire metadata
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
$qn->execute([':id' => $qnID]);
$questionnaire = $qn->fetch(PDO::FETCH_ASSOC);
if (!$questionnaire) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "Questionnaire not found"]);
exit;
}
// Questions with answer options
$qStmt = $pdo->prepare("
SELECT questionID, defaultText, type, orderIndex, isRequired
FROM question WHERE questionnaireID = :id ORDER BY orderIndex
");
$qStmt->execute([':id' => $qnID]);
$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
$questionIDs = array_column($questions, 'questionID');
foreach ($questions as &$q) {
$q['isRequired'] = (int)$q['isRequired'];
$q['orderIndex'] = (int)$q['orderIndex'];
$ao = $pdo->prepare("SELECT answerOptionID, defaultText, points, orderIndex
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex");
$ao->execute([':qid' => $q['questionID']]);
$q['answerOptions'] = $ao->fetchAll(PDO::FETCH_ASSOC);
foreach ($q['answerOptions'] as &$opt) {
$opt['points'] = (int)$opt['points'];
$opt['orderIndex'] = (int)$opt['orderIndex'];
}
}
unset($q);
// RBAC filter
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
// Build client list
if ($clientCode) {
$clientSQL = "SELECT cl.clientCode, cl.coachID FROM client cl WHERE cl.clientCode = :cc AND $rbacClause";
$clientParams = array_merge([':cc' => $clientCode], $rbacParams);
} else {
$clientSQL = "SELECT cl.clientCode, cl.coachID FROM client cl WHERE $rbacClause";
$clientParams = $rbacParams;
}
// Only clients that have a completed_questionnaire entry for this questionnaire
$sql = "
SELECT cl.clientCode, cl.coachID,
co.username AS coachUsername,
sv.username AS supervisorUsername,
cq.status, cq.startedAt, cq.completedAt, cq.sumPoints, cq.assignedByCoach
FROM client cl
INNER JOIN completed_questionnaire cq ON cq.clientCode = cl.clientCode AND cq.questionnaireID = :qnid
LEFT JOIN coach co ON co.coachID = cl.coachID
LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID
WHERE $rbacClause
";
$params = array_merge([':qnid' => $qnID], $rbacParams);
if ($clientCode) {
$sql .= " AND cl.clientCode = :cc";
$params[':cc'] = $clientCode;
}
$sql .= " ORDER BY cl.clientCode";
$cStmt = $pdo->prepare($sql);
$cStmt->execute($params);
$clients = $cStmt->fetchAll(PDO::FETCH_ASSOC);
// Build question ID placeholders for answer fetch
if (!empty($questionIDs) && !empty($clients)) {
$qPlaceholders = implode(',', array_fill(0, count($questionIDs), '?'));
$answerStmt = $pdo->prepare("
SELECT clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt
FROM client_answer
WHERE clientCode = ? AND questionID IN ($qPlaceholders)
");
foreach ($clients as &$c) {
$c['sumPoints'] = $c['sumPoints'] !== null ? (int)$c['sumPoints'] : null;
$c['startedAt'] = $c['startedAt'] !== null ? (int)$c['startedAt'] : null;
$c['completedAt'] = $c['completedAt'] !== null ? (int)$c['completedAt'] : null;
$bindParams = array_merge([$c['clientCode']], $questionIDs);
$answerStmt->execute($bindParams);
$answers = $answerStmt->fetchAll(PDO::FETCH_ASSOC);
$answerMap = [];
foreach ($answers as $a) {
$answerMap[$a['questionID']] = [
'answerOptionID' => $a['answerOptionID'],
'freeTextValue' => $a['freeTextValue'],
'numericValue' => $a['numericValue'] !== null ? (float)$a['numericValue'] : null,
'answeredAt' => $a['answeredAt'] !== null ? (int)$a['answeredAt'] : null,
];
}
$c['answers'] = $answerMap;
}
unset($c);
} else {
foreach ($clients as &$c) {
$c['sumPoints'] = $c['sumPoints'] !== null ? (int)$c['sumPoints'] : null;
$c['startedAt'] = $c['startedAt'] !== null ? (int)$c['startedAt'] : null;
$c['completedAt'] = $c['completedAt'] !== null ? (int)$c['completedAt'] : null;
$c['answers'] = (object)[];
}
unset($c);
}
qdb_discard($tmpDb, $lockFp);
echo json_encode([
"success" => true,
"questionnaire" => $questionnaire,
"questions" => $questions,
"clients" => $clients
]);

View File

@ -1,291 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
$method = $_SERVER['REQUEST_METHOD'];
$validTypes = ['question', 'answer_option', 'string', 'language'];
switch ($method) {
case 'GET':
// GET ?languages=1 -> list all languages
if (isset($_GET['languages'])) {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$rows = $pdo->query("SELECT languageCode, name FROM language ORDER BY languageCode")->fetchAll(PDO::FETCH_ASSOC);
qdb_discard($tmpDb, $lockFp);
echo json_encode(["success" => true, "languages" => $rows]);
break;
}
// GET ?questionnaireID=X -> batch: all keys + all translations for a questionnaire
$qnID = $_GET['questionnaireID'] ?? '';
if ($qnID) {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$languages = $pdo->query("SELECT languageCode, name FROM language ORDER BY languageCode")->fetchAll(PDO::FETCH_ASSOC);
// Question keys + translations
$qStmt = $pdo->prepare("
SELECT q.questionID, q.defaultText, q.configJson
FROM question q WHERE q.questionnaireID = :id ORDER BY q.orderIndex
");
$qStmt->execute([':id' => $qnID]);
$dbQuestions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
$entries = [];
$stringKeys = [];
foreach ($dbQuestions as $dbQ) {
$entries[] = [
'key' => $dbQ['defaultText'],
'type' => 'question',
'entityId' => $dbQ['questionID'],
];
// Answer option keys
$aoStmt = $pdo->prepare("
SELECT answerOptionID, defaultText FROM answer_option
WHERE questionID = :qid ORDER BY orderIndex
");
$aoStmt->execute([':qid' => $dbQ['questionID']]);
foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) {
$entries[] = [
'key' => $ao['defaultText'],
'type' => 'answer_option',
'entityId' => $ao['answerOptionID'],
];
}
// String keys from configJson
$config = json_decode($dbQ['configJson'] ?: '{}', true) ?: [];
foreach (['textKey','textKey1','textKey2','hint','hint1','hint2'] as $field) {
if (!empty($config[$field])) $stringKeys[$config[$field]] = true;
}
if (!empty($config['symptoms']) && is_array($config['symptoms'])) {
foreach ($config['symptoms'] as $s) $stringKeys[$s] = true;
}
}
foreach (array_keys($stringKeys) as $sk) {
$entries[] = [
'key' => $sk,
'type' => 'string',
'entityId' => $sk,
];
}
// Fetch all existing translations for these entities
$translations = [];
// question translations
$qIds = array_filter(array_map(fn($e) => $e['type'] === 'question' ? $e['entityId'] : null, $entries));
if ($qIds) {
$ph = implode(',', array_fill(0, count($qIds), '?'));
$stmt = $pdo->prepare("SELECT questionID AS entityId, languageCode, text FROM question_translation WHERE questionID IN ($ph)");
$stmt->execute(array_values($qIds));
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$translations[$r['entityId']][$r['languageCode']] = $r['text'];
}
}
// answer_option translations
$aoIds = array_filter(array_map(fn($e) => $e['type'] === 'answer_option' ? $e['entityId'] : null, $entries));
if ($aoIds) {
$ph = implode(',', array_fill(0, count($aoIds), '?'));
$stmt = $pdo->prepare("SELECT answerOptionID AS entityId, languageCode, text FROM answer_option_translation WHERE answerOptionID IN ($ph)");
$stmt->execute(array_values($aoIds));
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$translations[$r['entityId']][$r['languageCode']] = $r['text'];
}
}
// string translations
$sKeys = array_filter(array_map(fn($e) => $e['type'] === 'string' ? $e['entityId'] : null, $entries));
if ($sKeys) {
$ph = implode(',', array_fill(0, count($sKeys), '?'));
$stmt = $pdo->prepare("SELECT stringKey AS entityId, languageCode, text FROM string_translation WHERE stringKey IN ($ph)");
$stmt->execute(array_values($sKeys));
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$translations[$r['entityId']][$r['languageCode']] = $r['text'];
}
}
// Attach translations to entries
foreach ($entries as &$e) {
$e['translations'] = $translations[$e['entityId']] ?? (object)[];
}
unset($e);
qdb_discard($tmpDb, $lockFp);
echo json_encode([
"success" => true,
"languages" => $languages,
"entries" => $entries,
]);
break;
}
// Original single-entity fetch
$type = $_GET['type'] ?? '';
$id = $_GET['id'] ?? '';
if (!in_array($type, $validTypes, true) || (!$id && $type !== 'string')) {
http_response_code(400);
echo json_encode(["error" => "type (question|answer_option|string) and id required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
if ($type === 'question') {
$stmt = $pdo->prepare("SELECT languageCode, text FROM question_translation WHERE questionID = :id");
$stmt->execute([':id' => $id]);
} elseif ($type === 'answer_option') {
$stmt = $pdo->prepare("SELECT languageCode, text FROM answer_option_translation WHERE answerOptionID = :id");
$stmt->execute([':id' => $id]);
} else {
if ($id) {
$stmt = $pdo->prepare("SELECT stringKey, languageCode, text FROM string_translation WHERE stringKey = :id");
$stmt->execute([':id' => $id]);
} else {
$stmt = $pdo->query("SELECT stringKey, languageCode, text FROM string_translation ORDER BY stringKey, languageCode");
}
}
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
qdb_discard($tmpDb, $lockFp);
echo json_encode(["success" => true, "translations" => $rows]);
break;
case 'PUT':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
$type = $body['type'] ?? '';
// Language management
if ($type === 'language') {
$lang = trim($body['languageCode'] ?? '');
$name = trim($body['name'] ?? '');
if (!$lang) {
http_response_code(400);
echo json_encode(["error" => "languageCode required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$pdo->prepare("INSERT INTO language (languageCode, name) VALUES (:lc, :n)
ON CONFLICT(languageCode) DO UPDATE SET name = excluded.name")
->execute([':lc' => $lang, ':n' => $name]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
}
$id = $body['id'] ?? '';
$lang = trim($body['languageCode'] ?? '');
$text = $body['text'] ?? '';
if (!in_array($type, $validTypes, true) || !$id || !$lang) {
http_response_code(400);
echo json_encode(["error" => "type, id, and languageCode required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
if ($type === 'question') {
$pdo->prepare("INSERT INTO question_translation (questionID, languageCode, text)
VALUES (:id, :lang, :t)
ON CONFLICT(questionID, languageCode) DO UPDATE SET text = excluded.text")
->execute([':id' => $id, ':lang' => $lang, ':t' => $text]);
} elseif ($type === 'answer_option') {
$pdo->prepare("INSERT INTO answer_option_translation (answerOptionID, languageCode, text)
VALUES (:id, :lang, :t)
ON CONFLICT(answerOptionID, languageCode) DO UPDATE SET text = excluded.text")
->execute([':id' => $id, ':lang' => $lang, ':t' => $text]);
} else {
$pdo->prepare("INSERT INTO string_translation (stringKey, languageCode, text)
VALUES (:id, :lang, :t)
ON CONFLICT(stringKey, languageCode) DO UPDATE SET text = excluded.text")
->execute([':id' => $id, ':lang' => $lang, ':t' => $text]);
}
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
case 'DELETE':
require_role(['admin', 'supervisor'], $tokenRec);
$body = json_decode(file_get_contents('php://input'), true);
$type = $body['type'] ?? '';
// Language deletion
if ($type === 'language') {
$lang = trim($body['languageCode'] ?? '');
if (!$lang) {
http_response_code(400);
echo json_encode(["error" => "languageCode required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$pdo->prepare("DELETE FROM language WHERE languageCode = :lc")->execute([':lc' => $lang]);
$pdo->prepare("DELETE FROM question_translation WHERE languageCode = :lc")->execute([':lc' => $lang]);
$pdo->prepare("DELETE FROM answer_option_translation WHERE languageCode = :lc")->execute([':lc' => $lang]);
$pdo->prepare("DELETE FROM string_translation WHERE languageCode = :lc")->execute([':lc' => $lang]);
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
}
$id = $body['id'] ?? '';
$lang = trim($body['languageCode'] ?? '');
if (!in_array($type, $validTypes, true) || !$id || !$lang) {
http_response_code(400);
echo json_encode(["error" => "type, id, and languageCode required"]);
exit;
}
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
if ($type === 'question') {
$pdo->prepare("DELETE FROM question_translation WHERE questionID = :id AND languageCode = :lang")
->execute([':id' => $id, ':lang' => $lang]);
} elseif ($type === 'answer_option') {
$pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id AND languageCode = :lang")
->execute([':id' => $id, ':lang' => $lang]);
} else {
$pdo->prepare("DELETE FROM string_translation WHERE stringKey = :id AND languageCode = :lang")
->execute([':id' => $id, ':lang' => $lang]);
}
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
break;
default:
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
}

View File

@ -1,274 +0,0 @@
<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
$callerRole = $tokenRec['role'];
$callerEntityID = $tokenRec['entityID'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
// ── GET: list users (+ supervisors for admin) ─────────────────────────────
if ($method === 'GET') {
try {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
if ($callerRole === 'admin') {
$users = $pdo->query(
"SELECT u.userID, u.username, u.role, u.entityID, u.mustChangePassword, u.createdAt,
COALESCE(a.location, s.location, '') AS location,
c.supervisorID,
sv.username AS supervisorUsername
FROM users u
LEFT JOIN admin a ON a.adminID = u.entityID AND u.role = 'admin'
LEFT JOIN supervisor s ON s.supervisorID = u.entityID AND u.role = 'supervisor'
LEFT JOIN coach c ON c.coachID = u.entityID AND u.role = 'coach'
LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID
ORDER BY u.role, u.username"
)->fetchAll(PDO::FETCH_ASSOC);
$supervisors = $pdo->query(
"SELECT supervisorID, username, location FROM supervisor ORDER BY username"
)->fetchAll(PDO::FETCH_ASSOC);
} else {
// Supervisor: only their coaches
$svNameStmt = $pdo->prepare("SELECT username FROM supervisor WHERE supervisorID = :svid");
$svNameStmt->execute([':svid' => $callerEntityID]);
$svName = $svNameStmt->fetchColumn() ?: '';
$stmt = $pdo->prepare(
"SELECT u.userID, u.username, u.role, u.entityID, u.mustChangePassword, u.createdAt,
'' AS location, c.supervisorID, :svname AS supervisorUsername
FROM users u
JOIN coach c ON c.coachID = u.entityID AND u.role = 'coach'
WHERE c.supervisorID = :svid
ORDER BY u.username"
);
$stmt->execute([':svid' => $callerEntityID, ':svname' => $svName]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
$supervisors = [];
}
qdb_discard($tmpDb, $lockFp);
echo json_encode([
"success" => true,
"users" => $users,
"supervisors" => $supervisors,
"callerRole" => $callerRole,
]);
} catch (Throwable $e) {
if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
exit;
}
// ── POST: create a user ───────────────────────────────────────────────────
if ($method === 'POST') {
$in = json_decode(file_get_contents('php://input'), true) ?: [];
$username = trim($in['username'] ?? '');
$password = (string)($in['password'] ?? '');
$role = strtolower(trim($in['role'] ?? ''));
$location = trim($in['location'] ?? '');
$supervisorID = trim($in['supervisorID'] ?? '');
$mustChange = isset($in['mustChangePassword']) ? (int)(bool)$in['mustChangePassword'] : 1;
if ($username === '' || $password === '' || $role === '') {
http_response_code(400);
echo json_encode(["error" => "username, password, and role are required"]);
exit;
}
if ($callerRole === 'supervisor' && $role !== 'coach') {
http_response_code(403);
echo json_encode(["error" => "Supervisors may only create coaches"]);
exit;
}
if (!in_array($role, ['admin', 'supervisor', 'coach'], true)) {
http_response_code(400);
echo json_encode(["error" => "role must be admin, supervisor, or coach"]);
exit;
}
if (strlen($password) < 6) {
http_response_code(400);
echo json_encode(["error" => "Password must be at least 6 characters"]);
exit;
}
// Supervisors always create coaches under themselves
if ($callerRole === 'supervisor') {
$supervisorID = $callerEntityID;
}
if ($role === 'coach' && $supervisorID === '') {
http_response_code(400);
echo json_encode(["error" => "supervisorID is required for coaches"]);
exit;
}
try {
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
$chk = $pdo->prepare("SELECT userID FROM users WHERE username = :u");
$chk->execute([':u' => $username]);
if ($chk->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(409);
echo json_encode(["error" => "Username already exists"]);
exit;
}
if ($role === 'coach') {
$svCheck = $pdo->prepare("SELECT supervisorID FROM supervisor WHERE supervisorID = :sid");
$svCheck->execute([':sid' => $supervisorID]);
if (!$svCheck->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(400);
echo json_encode(["error" => "Supervisor not found"]);
exit;
}
if ($callerRole === 'supervisor' && $supervisorID !== $callerEntityID) {
qdb_discard($tmpDb, $lockFp);
http_response_code(403);
echo json_encode(["error" => "Cannot create coaches for another supervisor"]);
exit;
}
}
$entityID = bin2hex(random_bytes(16));
$userID = bin2hex(random_bytes(16));
$hash = password_hash($password, PASSWORD_DEFAULT);
$now = time();
$pdo->beginTransaction();
switch ($role) {
case 'admin':
$pdo->prepare("INSERT INTO admin (adminID, username, location) VALUES (:id, :u, :loc)")
->execute([':id' => $entityID, ':u' => $username, ':loc' => $location]);
break;
case 'supervisor':
$pdo->prepare("INSERT INTO supervisor (supervisorID, username, location) VALUES (:id, :u, :loc)")
->execute([':id' => $entityID, ':u' => $username, ':loc' => $location]);
break;
case 'coach':
$pdo->prepare("INSERT INTO coach (coachID, supervisorID, username) VALUES (:id, :sid, :u)")
->execute([':id' => $entityID, ':sid' => $supervisorID, ':u' => $username]);
break;
}
$pdo->prepare(
"INSERT INTO users (userID, username, passwordHash, role, entityID, mustChangePassword, createdAt)
VALUES (:uid, :u, :hash, :role, :eid, :mcp, :now)"
)->execute([
':uid' => $userID, ':u' => $username, ':hash' => $hash,
':role' => $role, ':eid' => $entityID, ':mcp' => $mustChange, ':now' => $now,
]);
$pdo->commit();
qdb_save($tmpDb, $lockFp);
// Fetch supervisor name for response
$svUsername = null;
if ($role === 'coach') {
[$pdo2, $tmp2, $lk2] = qdb_open(false);
$svr = $pdo2->prepare("SELECT username FROM supervisor WHERE supervisorID = :sid");
$svr->execute([':sid' => $supervisorID]);
$svUsername = $svr->fetchColumn() ?: null;
qdb_discard($tmp2, $lk2);
}
echo json_encode([
"success" => true,
"userID" => $userID,
"entityID" => $entityID,
"username" => $username,
"role" => $role,
"location" => $location,
"supervisorID" => $role === 'coach' ? $supervisorID : null,
"supervisorUsername" => $svUsername,
"mustChangePassword" => $mustChange,
"createdAt" => $now,
]);
} catch (Throwable $e) {
if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
exit;
}
// ── DELETE: remove a user ─────────────────────────────────────────────────
if ($method === 'DELETE') {
$in = json_decode(file_get_contents('php://input'), true) ?: [];
$targetUserID = trim($in['userID'] ?? '');
if ($targetUserID === '') {
http_response_code(400);
echo json_encode(["error" => "userID is required"]);
exit;
}
if ($targetUserID === ($tokenRec['userID'] ?? '')) {
http_response_code(400);
echo json_encode(["error" => "Cannot delete your own account"]);
exit;
}
try {
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
$row = $pdo->prepare("SELECT role, entityID FROM users WHERE userID = :uid");
$row->execute([':uid' => $targetUserID]);
$target = $row->fetch(PDO::FETCH_ASSOC);
if (!$target) {
qdb_discard($tmpDb, $lockFp);
http_response_code(404);
echo json_encode(["error" => "User not found"]);
exit;
}
if ($callerRole === 'supervisor') {
if ($target['role'] !== 'coach') {
qdb_discard($tmpDb, $lockFp);
http_response_code(403);
echo json_encode(["error" => "Supervisors can only delete coaches"]);
exit;
}
$chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :svid");
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(403);
echo json_encode(["error" => "Coach is not under your supervision"]);
exit;
}
}
$pdo->beginTransaction();
$pdo->prepare("DELETE FROM users WHERE userID = :uid")->execute([':uid' => $targetUserID]);
switch ($target['role']) {
case 'admin':
$pdo->prepare("DELETE FROM admin WHERE adminID = :id")->execute([':id' => $target['entityID']]);
break;
case 'supervisor':
$pdo->prepare("DELETE FROM supervisor WHERE supervisorID = :id")->execute([':id' => $target['entityID']]);
break;
case 'coach':
$pdo->prepare("DELETE FROM coach WHERE coachID = :id")->execute([':id' => $target['entityID']]);
break;
}
$pdo->commit();
qdb_save($tmpDb, $lockFp);
echo json_encode(["success" => true]);
} catch (Throwable $e) {
if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp);
http_response_code(500);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
}
exit;
}
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);