896 lines
33 KiB
PHP
896 lines
33 KiB
PHP
<?php
|
|
/**
|
|
* Dev-only fixture import: users, clients, completed questionnaires.
|
|
* All entity usernames / client codes must use the fixture prefix (default dev_).
|
|
*/
|
|
|
|
const QDB_DEV_GLASS_LABELS = ['never_glass', 'little_glass', 'moderate_glass', 'much_glass', 'extreme_glass'];
|
|
|
|
const QDB_DEV_GLASS_POINTS = [
|
|
'never_glass' => 0,
|
|
'little_glass' => 1,
|
|
'moderate_glass' => 2,
|
|
'much_glass' => 3,
|
|
'extreme_glass' => 4,
|
|
];
|
|
|
|
require_once __DIR__ . '/app_answers.php';
|
|
require_once __DIR__ . '/submissions.php';
|
|
|
|
/**
|
|
* @return array{imported: array, skipped: array, errors: string[]}
|
|
*/
|
|
function qdb_import_dev_fixture(PDO $pdo, array $fixture): array
|
|
{
|
|
$prefix = qdb_dev_validate_fixture($fixture);
|
|
$password = (string)($fixture['defaultPassword'] ?? 'socialvrlab');
|
|
if (strlen($password) < 6) {
|
|
json_error('INVALID_FIELD', 'defaultPassword must be at least 6 characters', 400);
|
|
}
|
|
|
|
$imported = [
|
|
'admins' => 0,
|
|
'supervisors' => 0,
|
|
'coaches' => 0,
|
|
'clients' => 0,
|
|
'completions' => 0,
|
|
'submissions' => 0,
|
|
'answers' => 0,
|
|
];
|
|
$skipped = [
|
|
'admins' => 0,
|
|
'supervisors' => 0,
|
|
'coaches' => 0,
|
|
'clients' => 0,
|
|
'completions' => 0,
|
|
];
|
|
$errors = [];
|
|
|
|
$coachIdByUsername = [];
|
|
$supervisorIdByUsername = [];
|
|
|
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
|
$now = time();
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
foreach ($fixture['admins'] ?? [] as $row) {
|
|
$username = qdb_dev_norm_username($row, $prefix);
|
|
if (qdb_dev_user_exists($pdo, $username)) {
|
|
$skipped['admins']++;
|
|
continue;
|
|
}
|
|
$entityID = qdb_dev_entity_id('admin', $username);
|
|
$userID = qdb_dev_user_id($username);
|
|
$location = trim($row['location'] ?? 'Dev');
|
|
$pdo->prepare('INSERT INTO admin (adminID, username, location) VALUES (:id, :u, :loc)')
|
|
->execute([':id' => $entityID, ':u' => $username, ':loc' => $location]);
|
|
qdb_dev_insert_user($pdo, $userID, $username, $hash, 'admin', $entityID, $now);
|
|
$imported['admins']++;
|
|
}
|
|
|
|
foreach ($fixture['supervisors'] ?? [] as $row) {
|
|
$username = qdb_dev_norm_username($row, $prefix);
|
|
if (qdb_dev_user_exists($pdo, $username)) {
|
|
$skipped['supervisors']++;
|
|
$supervisorIdByUsername[$username] = qdb_dev_lookup_entity_id($pdo, $username);
|
|
continue;
|
|
}
|
|
$entityID = qdb_dev_entity_id('supervisor', $username);
|
|
$userID = qdb_dev_user_id($username);
|
|
$location = trim($row['location'] ?? 'Dev');
|
|
$pdo->prepare('INSERT INTO supervisor (supervisorID, username, location) VALUES (:id, :u, :loc)')
|
|
->execute([':id' => $entityID, ':u' => $username, ':loc' => $location]);
|
|
qdb_dev_insert_user($pdo, $userID, $username, $hash, 'supervisor', $entityID, $now);
|
|
$supervisorIdByUsername[$username] = $entityID;
|
|
$imported['supervisors']++;
|
|
}
|
|
|
|
foreach ($fixture['coaches'] ?? [] as $row) {
|
|
$username = qdb_dev_norm_username($row, $prefix);
|
|
$svUser = trim($row['supervisor'] ?? $row['supervisorUsername'] ?? '');
|
|
if ($svUser === '' || !qdb_dev_has_prefix($svUser, $prefix)) {
|
|
$errors[] = "Coach $username: missing or invalid supervisor";
|
|
continue;
|
|
}
|
|
$supervisorID = $supervisorIdByUsername[$svUser]
|
|
?? qdb_dev_lookup_entity_id($pdo, $svUser);
|
|
if ($supervisorID === '') {
|
|
$errors[] = "Coach $username: supervisor $svUser not found";
|
|
continue;
|
|
}
|
|
if (qdb_dev_user_exists($pdo, $username)) {
|
|
$skipped['coaches']++;
|
|
$coachIdByUsername[$username] = qdb_dev_lookup_entity_id($pdo, $username);
|
|
continue;
|
|
}
|
|
$entityID = qdb_dev_entity_id('coach', $username);
|
|
$userID = qdb_dev_user_id($username);
|
|
$pdo->prepare('INSERT INTO coach (coachID, supervisorID, username) VALUES (:id, :sid, :u)')
|
|
->execute([':id' => $entityID, ':sid' => $supervisorID, ':u' => $username]);
|
|
qdb_dev_insert_user($pdo, $userID, $username, $hash, 'coach', $entityID, $now);
|
|
$coachIdByUsername[$username] = $entityID;
|
|
$imported['coaches']++;
|
|
}
|
|
|
|
foreach ($fixture['clients'] ?? [] as $row) {
|
|
$clientCode = qdb_dev_norm_client_code($row, $prefix);
|
|
$coachUser = trim($row['coach'] ?? $row['coachUsername'] ?? '');
|
|
if ($coachUser === '' || !qdb_dev_has_prefix($coachUser, $prefix)) {
|
|
$errors[] = "Client $clientCode: missing or invalid coach";
|
|
continue;
|
|
}
|
|
$coachID = $coachIdByUsername[$coachUser]
|
|
?? qdb_dev_lookup_entity_id($pdo, $coachUser);
|
|
if ($coachID === '') {
|
|
$errors[] = "Client $clientCode: coach $coachUser not found";
|
|
continue;
|
|
}
|
|
$chk = $pdo->prepare('SELECT 1 FROM client WHERE clientCode = :cc');
|
|
$chk->execute([':cc' => $clientCode]);
|
|
if ($chk->fetch()) {
|
|
$skipped['clients']++;
|
|
continue;
|
|
}
|
|
$pdo->prepare('INSERT INTO client (clientCode, coachID) VALUES (:cc, :cid)')
|
|
->execute([':cc' => $clientCode, ':cid' => $coachID]);
|
|
$imported['clients']++;
|
|
}
|
|
|
|
$variant = 0;
|
|
foreach ($fixture['completions'] ?? [] as $row) {
|
|
$clientCode = qdb_dev_norm_client_code($row, $prefix);
|
|
$qnID = trim($row['questionnaireID'] ?? '');
|
|
if ($qnID === '') {
|
|
$errors[] = 'Completion missing questionnaireID';
|
|
continue;
|
|
}
|
|
$qnChk = $pdo->prepare('SELECT 1 FROM questionnaire WHERE questionnaireID = :id');
|
|
$qnChk->execute([':id' => $qnID]);
|
|
if (!$qnChk->fetch()) {
|
|
$errors[] = "Questionnaire not in DB: $qnID";
|
|
continue;
|
|
}
|
|
$clChk = $pdo->prepare('SELECT coachID FROM client WHERE clientCode = :cc');
|
|
$clChk->execute([':cc' => $clientCode]);
|
|
$coachID = $clChk->fetchColumn();
|
|
if ($coachID === false) {
|
|
$errors[] = "Client not found: $clientCode";
|
|
continue;
|
|
}
|
|
$doneChk = $pdo->prepare(
|
|
'SELECT 1 FROM completed_questionnaire WHERE clientCode = :cc AND questionnaireID = :qn'
|
|
);
|
|
$doneChk->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
|
if ($doneChk->fetch()) {
|
|
$skipped['completions']++;
|
|
continue;
|
|
}
|
|
|
|
$coachUserStmt = $pdo->prepare(
|
|
"SELECT u.userID FROM users u
|
|
WHERE u.role = 'coach' AND u.entityID = :cid LIMIT 1"
|
|
);
|
|
$coachUserStmt->execute([':cid' => $coachID]);
|
|
$coachUserID = (string)($coachUserStmt->fetchColumn() ?: '');
|
|
$tokenRec = ['userID' => $coachUserID, 'role' => 'coach'];
|
|
|
|
$uploads = qdb_dev_normalize_uploads($row, $variant, $now);
|
|
$archiveSubmissions = qdb_table_exists($pdo, 'questionnaire_submission');
|
|
|
|
foreach ($uploads as $uploadIndex => $upload) {
|
|
$variantSeed = (int)($upload['variant'] ?? ($variant + $uploadIndex));
|
|
$completedAt = (int)($upload['submittedAt'] ?? $upload['completedAt'] ?? $now);
|
|
$startedAt = (int)($upload['startedAt'] ?? ($completedAt - 600));
|
|
|
|
$answers = qdb_dev_build_answers_for_questionnaire($pdo, $qnID, $variantSeed);
|
|
$answerCount = qdb_dev_persist_submission(
|
|
$pdo,
|
|
$qnID,
|
|
$clientCode,
|
|
(string)$coachID,
|
|
$answers,
|
|
$startedAt,
|
|
$completedAt
|
|
);
|
|
$imported['answers'] += $answerCount;
|
|
|
|
if ($archiveSubmissions) {
|
|
$spStmt = $pdo->prepare(
|
|
'SELECT sumPoints FROM completed_questionnaire
|
|
WHERE clientCode = :cc AND questionnaireID = :qn'
|
|
);
|
|
$spStmt->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
|
$sumPoints = (int)($spStmt->fetchColumn() ?: 0);
|
|
qdb_record_submission_after_submit(
|
|
$pdo,
|
|
$clientCode,
|
|
$qnID,
|
|
$tokenRec,
|
|
$startedAt,
|
|
$completedAt,
|
|
$sumPoints,
|
|
(string)$coachID,
|
|
$completedAt
|
|
);
|
|
$imported['submissions']++;
|
|
}
|
|
}
|
|
|
|
$imported['completions']++;
|
|
$variant++;
|
|
}
|
|
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
|
|
return ['imported' => $imported, 'skipped' => $skipped, 'errors' => $errors];
|
|
}
|
|
|
|
function qdb_dev_validate_fixture(array $fixture): string
|
|
{
|
|
$prefix = trim((string)($fixture['prefix'] ?? 'dev_'));
|
|
if ($prefix === '' || !preg_match('/^dev[a-z0-9_]*$/i', $prefix)) {
|
|
json_error('INVALID_FIELD', 'fixture.prefix must start with "dev" (e.g. dev_)', 400);
|
|
}
|
|
$fixtureVersion = (int)($fixture['fixtureVersion'] ?? 0);
|
|
if ($fixtureVersion !== 1 && $fixtureVersion !== 2) {
|
|
json_error('INVALID_FIELD', 'fixtureVersion must be 1 or 2', 400);
|
|
}
|
|
return $prefix;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{submittedAt: int, startedAt: int, variant: int}>
|
|
*/
|
|
function qdb_dev_normalize_uploads(array $row, int $fallbackVariant, int $now): array
|
|
{
|
|
if (!empty($row['uploads']) && is_array($row['uploads'])) {
|
|
$out = [];
|
|
foreach ($row['uploads'] as $i => $upload) {
|
|
if (!is_array($upload)) {
|
|
continue;
|
|
}
|
|
$submittedAt = (int)($upload['submittedAt'] ?? $upload['completedAt'] ?? $now);
|
|
$startedAt = (int)($upload['startedAt'] ?? ($submittedAt - 600));
|
|
$out[] = [
|
|
'submittedAt' => $submittedAt,
|
|
'startedAt' => $startedAt,
|
|
'variant' => (int)($upload['variant'] ?? ($fallbackVariant + $i)),
|
|
];
|
|
}
|
|
if ($out !== []) {
|
|
usort($out, static fn($a, $b) => $a['submittedAt'] <=> $b['submittedAt']);
|
|
return $out;
|
|
}
|
|
}
|
|
|
|
$versionCount = max(1, (int)($row['versions'] ?? $row['uploadCount'] ?? 1));
|
|
$finalCompleted = isset($row['completedAt']) ? (int)$row['completedAt'] : ($now - ($fallbackVariant % 90) * 86400);
|
|
$uploads = [];
|
|
for ($v = 0; $v < $versionCount; $v++) {
|
|
$gapDays = ($versionCount - 1 - $v) * 14 + ($fallbackVariant % 5);
|
|
$completedAt = $finalCompleted - ($gapDays * 86400);
|
|
$uploads[] = [
|
|
'submittedAt' => $completedAt,
|
|
'startedAt' => isset($row['startedAt']) && $v === $versionCount - 1
|
|
? (int)$row['startedAt']
|
|
: ($completedAt - 600 - ($v * 120)),
|
|
'variant' => $fallbackVariant + $v,
|
|
];
|
|
}
|
|
usort($uploads, static fn($a, $b) => $a['submittedAt'] <=> $b['submittedAt']);
|
|
return $uploads;
|
|
}
|
|
|
|
function qdb_dev_has_prefix(string $value, string $prefix): bool
|
|
{
|
|
return str_starts_with(strtolower($value), strtolower($prefix))
|
|
|| str_starts_with(strtoupper($value), strtoupper(rtrim($prefix, '_')));
|
|
}
|
|
|
|
function qdb_dev_norm_username(array $row, string $prefix): string
|
|
{
|
|
$username = trim($row['username'] ?? '');
|
|
if ($username === '' || !qdb_dev_has_prefix($username, $prefix)) {
|
|
json_error('INVALID_FIELD', "Username must use prefix $prefix", 400);
|
|
}
|
|
return $username;
|
|
}
|
|
|
|
function qdb_dev_norm_client_code(array $row, string $prefix): string
|
|
{
|
|
$code = trim($row['clientCode'] ?? '');
|
|
$upperPrefix = strtoupper(rtrim($prefix, '_'));
|
|
if ($code === '' || !str_starts_with($code, $upperPrefix)) {
|
|
json_error('INVALID_FIELD', "clientCode must start with $upperPrefix", 400);
|
|
}
|
|
return $code;
|
|
}
|
|
|
|
function qdb_dev_entity_id(string $role, string $username): string
|
|
{
|
|
return 'dev_ent_' . $role . '_' . substr(hash('sha256', $username), 0, 24);
|
|
}
|
|
|
|
function qdb_dev_user_id(string $username): string
|
|
{
|
|
return 'dev_uid_' . substr(hash('sha256', 'user:' . $username), 0, 28);
|
|
}
|
|
|
|
function qdb_dev_user_exists(PDO $pdo, string $username): bool
|
|
{
|
|
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE username = :u');
|
|
$stmt->execute([':u' => $username]);
|
|
return (bool)$stmt->fetch();
|
|
}
|
|
|
|
function qdb_dev_lookup_entity_id(PDO $pdo, string $username): string
|
|
{
|
|
$stmt = $pdo->prepare('SELECT entityID FROM users WHERE username = :u');
|
|
$stmt->execute([':u' => $username]);
|
|
$id = $stmt->fetchColumn();
|
|
return $id !== false ? (string)$id : '';
|
|
}
|
|
|
|
function qdb_dev_insert_user(
|
|
PDO $pdo,
|
|
string $userID,
|
|
string $username,
|
|
string $hash,
|
|
string $role,
|
|
string $entityID,
|
|
int $now
|
|
): void {
|
|
$mcp = 0;
|
|
$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' => $mcp,
|
|
':now' => $now,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return list<array{questionID: string, answerOptionKey?: string, freeTextValue?: string, numericValue?: float}>
|
|
*/
|
|
function qdb_dev_build_answers_for_questionnaire(PDO $pdo, string $qnID, int $variant): array
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
'SELECT questionID, type, configJson, orderIndex
|
|
FROM question WHERE questionnaireID = :qn ORDER BY orderIndex'
|
|
);
|
|
$stmt->execute([':qn' => $qnID]);
|
|
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$aoStmt = $pdo->prepare(
|
|
'SELECT ao.answerOptionID, ao.questionID, ao.defaultText, ao.points, ao.orderIndex
|
|
FROM answer_option ao
|
|
JOIN question q ON q.questionID = ao.questionID
|
|
WHERE q.questionnaireID = :qn
|
|
ORDER BY ao.orderIndex'
|
|
);
|
|
$aoStmt->execute([':qn' => $qnID]);
|
|
$optionsByQuestion = [];
|
|
foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) {
|
|
$optionsByQuestion[$ao['questionID']][] = $ao;
|
|
}
|
|
|
|
$answers = [];
|
|
$freeTextSamples = ['Testantwort', 'Dev-Eingabe', 'Beispieltext', 'N/A'];
|
|
|
|
foreach ($questions as $q) {
|
|
$type = $q['type'] ?? '';
|
|
$localId = qdb_question_local_id($q['questionID'], $qnID);
|
|
$config = json_decode($q['configJson'] ?? '{}', true) ?: [];
|
|
$v = $variant + (int)($q['orderIndex'] ?? 0);
|
|
|
|
switch ($type) {
|
|
case 'last_page':
|
|
break;
|
|
|
|
case 'glass_scale_question':
|
|
$symData = [];
|
|
foreach ($config['symptoms'] ?? [] as $si => $symptomKey) {
|
|
$sk = trim((string)$symptomKey);
|
|
if ($sk === '') {
|
|
continue;
|
|
}
|
|
$symData[$sk] = QDB_DEV_GLASS_LABELS[($v + $si) % count(QDB_DEV_GLASS_LABELS)];
|
|
}
|
|
if ($symData !== []) {
|
|
$answers[] = [
|
|
'questionID' => $localId,
|
|
'freeTextValue' => json_encode($symData, JSON_UNESCAPED_UNICODE),
|
|
];
|
|
}
|
|
break;
|
|
|
|
case 'radio_question':
|
|
case 'client_not_signed':
|
|
$opts = $optionsByQuestion[$q['questionID']] ?? [];
|
|
if ($opts) {
|
|
$pick = $opts[$v % count($opts)];
|
|
$key = qdb_option_key($pick) ?: $pick['defaultText'];
|
|
$answers[] = ['questionID' => $localId, 'answerOptionKey' => $key];
|
|
}
|
|
break;
|
|
|
|
case 'multi_check_box_question':
|
|
$opts = $optionsByQuestion[$q['questionID']] ?? [];
|
|
if ($opts) {
|
|
$pickCount = min(5, max(2, count($opts) > 20 ? 4 : 2), count($opts));
|
|
$keys = [];
|
|
for ($i = 0; $i < $pickCount; $i++) {
|
|
$pick = $opts[($v + $i) % count($opts)];
|
|
$key = qdb_option_key($pick) ?: $pick['defaultText'];
|
|
if ($key !== '') {
|
|
$keys[] = $key;
|
|
}
|
|
}
|
|
if ($keys !== []) {
|
|
$answers[] = [
|
|
'questionID' => $localId,
|
|
'freeTextValue' => qdb_encode_multi_check_values($keys),
|
|
];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'string_spinner':
|
|
$opts = $config['options'] ?? [];
|
|
if (is_array($opts) && $opts !== []) {
|
|
$answers[] = [
|
|
'questionID' => $localId,
|
|
'freeTextValue' => (string)$opts[$v % count($opts)],
|
|
];
|
|
}
|
|
break;
|
|
|
|
case 'value_spinner':
|
|
case 'slider_question':
|
|
$range = $config['range'] ?? ['min' => 0, 'max' => 10];
|
|
$min = (int)($range['min'] ?? 0);
|
|
$max = (int)($range['max'] ?? 10);
|
|
if ($max < $min) {
|
|
$max = $min;
|
|
}
|
|
$span = max(1, $max - $min + 1);
|
|
$answers[] = [
|
|
'questionID' => $localId,
|
|
'numericValue' => (float)($min + ($v % $span)),
|
|
];
|
|
break;
|
|
|
|
case 'date_spinner':
|
|
$year = 2018 + ($v % 6);
|
|
$month = 1 + ($v % 12);
|
|
$day = 1 + ($v % 28);
|
|
$precision = $config['precision'] ?? 'full';
|
|
$dateValue = match ($precision) {
|
|
'year' => sprintf('%04d', $year),
|
|
'year_month' => sprintf('%04d-%02d', $year, $month),
|
|
default => sprintf('%04d-%02d-%02d', $year, $month, $day),
|
|
};
|
|
$answers[] = [
|
|
'questionID' => $localId,
|
|
'freeTextValue' => $dateValue,
|
|
];
|
|
break;
|
|
|
|
case 'free_text':
|
|
$answers[] = [
|
|
'questionID' => $localId,
|
|
'freeTextValue' => $freeTextSamples[$v % count($freeTextSamples)],
|
|
];
|
|
break;
|
|
|
|
case 'client_coach_code_question':
|
|
$answers[] = [
|
|
'questionID' => $localId,
|
|
'freeTextValue' => 'DEV-IMPORT',
|
|
];
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $answers;
|
|
}
|
|
|
|
/**
|
|
* @param list<array{questionID: string, answerOptionKey?: string, freeTextValue?: string, numericValue?: float}> $answers
|
|
*/
|
|
function qdb_dev_persist_submission(
|
|
PDO $pdo,
|
|
string $qnID,
|
|
string $clientCode,
|
|
string $assignedByCoach,
|
|
array $answers,
|
|
int $startedAt,
|
|
int $completedAt
|
|
): int {
|
|
$qRows = $pdo->prepare('SELECT questionID, type, configJson FROM question WHERE questionnaireID = :qn');
|
|
$qRows->execute([':qn' => $qnID]);
|
|
$shortIdMap = [];
|
|
$typeByFullId = [];
|
|
$freeTextMaxLen = [];
|
|
foreach ($qRows->fetchAll(PDO::FETCH_ASSOC) as $qRow) {
|
|
$fullId = $qRow['questionID'];
|
|
$parts = explode('__', $fullId);
|
|
$shortIdMap[end($parts)] = $fullId;
|
|
$typeByFullId[$fullId] = $qRow['type'] ?? '';
|
|
if (($qRow['type'] ?? '') === 'free_text') {
|
|
$cfg = json_decode($qRow['configJson'] ?? '{}', true) ?: [];
|
|
$freeTextMaxLen[$fullId] = max(1, min((int)($cfg['maxLength'] ?? 500), 10000));
|
|
}
|
|
}
|
|
|
|
$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 = [];
|
|
foreach ($aoRows->fetchAll(PDO::FETCH_ASSOC) as $ao) {
|
|
$optionMap[$ao['questionID']][$ao['defaultText']] = [
|
|
'answerOptionID' => $ao['answerOptionID'],
|
|
'points' => (int)$ao['points'],
|
|
];
|
|
}
|
|
|
|
$sumPoints = 0;
|
|
$written = 0;
|
|
$answeredAt = $completedAt;
|
|
$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;
|
|
}
|
|
|
|
$fullQID = $shortIdMap[$shortId] ?? null;
|
|
if ($fullQID === null) {
|
|
continue;
|
|
}
|
|
|
|
$freeTextValue = isset($a['freeTextValue']) ? (string)$a['freeTextValue'] : null;
|
|
$numericValue = isset($a['numericValue']) ? (float)$a['numericValue'] : null;
|
|
if ($freeTextValue !== null && $freeTextValue !== '') {
|
|
$maxLen = $freeTextMaxLen[$fullQID] ?? 2000;
|
|
$freeTextValue = sanitize_free_text($freeTextValue, $maxLen);
|
|
if ($freeTextValue === '') {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$answerOptionID = null;
|
|
$qType = $typeByFullId[$fullQID] ?? '';
|
|
|
|
if ($qType === 'glass_scale_question' && $freeTextValue !== null && $freeTextValue !== '') {
|
|
$decoded = json_decode($freeTextValue, true);
|
|
if (is_array($decoded)) {
|
|
foreach ($decoded as $label) {
|
|
$label = trim((string)$label);
|
|
if ($label !== '') {
|
|
$sumPoints += QDB_DEV_GLASS_POINTS[$label] ?? 0;
|
|
}
|
|
}
|
|
}
|
|
} elseif ($qType === 'multi_check_box_question' && $freeTextValue !== null && $freeTextValue !== '') {
|
|
foreach (qdb_decode_multi_check_values($freeTextValue) as $mk) {
|
|
if (isset($optionMap[$fullQID][$mk])) {
|
|
$sumPoints += $optionMap[$fullQID][$mk]['points'];
|
|
}
|
|
}
|
|
} else {
|
|
$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,
|
|
]);
|
|
$written++;
|
|
}
|
|
|
|
$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,
|
|
]);
|
|
|
|
return $written;
|
|
}
|
|
|
|
/** @param list<string> $ids */
|
|
function qdb_dev_delete_by_ids(PDO $pdo, string $table, string $column, array $ids): int
|
|
{
|
|
$ids = array_values(array_unique(array_filter($ids, static fn($id) => $id !== '' && $id !== null)));
|
|
if ($ids === []) {
|
|
return 0;
|
|
}
|
|
$total = 0;
|
|
foreach (array_chunk($ids, 200) as $chunk) {
|
|
$ph = implode(',', array_fill(0, count($chunk), '?'));
|
|
$stmt = $pdo->prepare("DELETE FROM $table WHERE $column IN ($ph)");
|
|
$stmt->execute($chunk);
|
|
$total += $stmt->rowCount();
|
|
}
|
|
return $total;
|
|
}
|
|
|
|
/**
|
|
* Remove dev-prefixed test users, clients, and their answers (does not touch other accounts).
|
|
*
|
|
* @return array{deleted: array<string, int>}
|
|
*/
|
|
function qdb_remove_dev_test_data(PDO $pdo, array $options = []): array
|
|
{
|
|
$usernamePrefix = trim((string)($options['usernamePrefix'] ?? 'dev_'));
|
|
$clientPrefix = trim((string)($options['clientCodePrefix'] ?? 'DEV-CL-'));
|
|
if ($usernamePrefix === '' || stripos($usernamePrefix, 'dev') !== 0) {
|
|
json_error('INVALID_FIELD', 'usernamePrefix must start with "dev"', 400);
|
|
}
|
|
if ($clientPrefix === '' || stripos($clientPrefix, 'DEV') !== 0) {
|
|
json_error('INVALID_FIELD', 'clientCodePrefix must start with "DEV"', 400);
|
|
}
|
|
|
|
$userLike = $usernamePrefix . '%';
|
|
$clientLike = $clientPrefix . '%';
|
|
|
|
$deleted = [
|
|
'submissions' => 0,
|
|
'followup_notes' => 0,
|
|
'client_answers' => 0,
|
|
'completed_questionnaires' => 0,
|
|
'clients' => 0,
|
|
'sessions' => 0,
|
|
'users' => 0,
|
|
'coaches' => 0,
|
|
'supervisors' => 0,
|
|
'admins' => 0,
|
|
];
|
|
|
|
$coachIds = [];
|
|
$stmt = $pdo->prepare('SELECT coachID FROM coach WHERE username LIKE :pfx');
|
|
$stmt->execute([':pfx' => $userLike]);
|
|
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $id) {
|
|
$coachIds[$id] = true;
|
|
}
|
|
|
|
$supervisorIds = [];
|
|
$stmt = $pdo->prepare('SELECT supervisorID FROM supervisor WHERE username LIKE :pfx');
|
|
$stmt->execute([':pfx' => $userLike]);
|
|
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $id) {
|
|
$supervisorIds[$id] = true;
|
|
}
|
|
|
|
$adminIds = [];
|
|
$stmt = $pdo->prepare('SELECT adminID FROM admin WHERE username LIKE :pfx');
|
|
$stmt->execute([':pfx' => $userLike]);
|
|
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $id) {
|
|
$adminIds[$id] = true;
|
|
}
|
|
|
|
$userIds = [];
|
|
$userRows = $pdo->prepare('SELECT userID, role, entityID FROM users WHERE username LIKE :pfx');
|
|
$userRows->execute([':pfx' => $userLike]);
|
|
foreach ($userRows->fetchAll(PDO::FETCH_ASSOC) as $u) {
|
|
$userIds[$u['userID']] = true;
|
|
switch ($u['role']) {
|
|
case 'coach':
|
|
$coachIds[$u['entityID']] = true;
|
|
break;
|
|
case 'supervisor':
|
|
$supervisorIds[$u['entityID']] = true;
|
|
break;
|
|
case 'admin':
|
|
$adminIds[$u['entityID']] = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$coachIdList = array_keys($coachIds);
|
|
$clientCodes = [];
|
|
$ccStmt = $pdo->prepare('SELECT clientCode FROM client WHERE clientCode LIKE :pfx');
|
|
$ccStmt->execute([':pfx' => $clientLike]);
|
|
foreach ($ccStmt->fetchAll(PDO::FETCH_COLUMN) as $code) {
|
|
$clientCodes[$code] = true;
|
|
}
|
|
if ($coachIdList !== []) {
|
|
foreach (array_chunk($coachIdList, 200) as $chunk) {
|
|
$ph = implode(',', array_fill(0, count($chunk), '?'));
|
|
$byCoach = $pdo->prepare("SELECT clientCode FROM client WHERE coachID IN ($ph)");
|
|
$byCoach->execute($chunk);
|
|
foreach ($byCoach->fetchAll(PDO::FETCH_COLUMN) as $code) {
|
|
$clientCodes[$code] = true;
|
|
}
|
|
}
|
|
}
|
|
$clientCodeList = array_keys($clientCodes);
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
if ($clientCodeList !== []) {
|
|
$responseDeleted = qdb_delete_client_response_data($pdo, $clientCodeList);
|
|
$deleted['submissions'] += $responseDeleted['submissions'];
|
|
$deleted['followup_notes'] += $responseDeleted['followup_notes'];
|
|
$deleted['client_answers'] += $responseDeleted['client_answers'];
|
|
$deleted['completed_questionnaires'] += $responseDeleted['completed_questionnaires'];
|
|
}
|
|
if ($coachIdList !== []) {
|
|
$deleted['submissions'] += qdb_delete_submissions_for_coaches($pdo, $coachIdList);
|
|
$deleted['completed_questionnaires'] += qdb_dev_delete_by_ids(
|
|
$pdo,
|
|
'completed_questionnaire',
|
|
'assignedByCoach',
|
|
$coachIdList
|
|
);
|
|
}
|
|
|
|
if ($clientCodeList !== []) {
|
|
$deleted['clients'] += qdb_dev_delete_by_ids($pdo, 'client', 'clientCode', $clientCodeList);
|
|
}
|
|
if ($coachIdList !== []) {
|
|
foreach (array_chunk($coachIdList, 200) as $chunk) {
|
|
$ph = implode(',', array_fill(0, count($chunk), '?'));
|
|
$delCl = $pdo->prepare("DELETE FROM client WHERE coachID IN ($ph)");
|
|
$delCl->execute($chunk);
|
|
$deleted['clients'] += $delCl->rowCount();
|
|
}
|
|
}
|
|
$delClPrefix = $pdo->prepare('DELETE FROM client WHERE clientCode LIKE :pfx');
|
|
$delClPrefix->execute([':pfx' => $clientLike]);
|
|
$deleted['clients'] += $delClPrefix->rowCount();
|
|
|
|
$deleted['sessions'] = qdb_dev_delete_by_ids($pdo, 'session', 'userID', array_keys($userIds));
|
|
|
|
$deleted['users'] = qdb_dev_delete_by_ids($pdo, 'users', 'userID', array_keys($userIds));
|
|
$delUsersLike = $pdo->prepare('DELETE FROM users WHERE username LIKE :pfx');
|
|
$delUsersLike->execute([':pfx' => $userLike]);
|
|
$deleted['users'] += $delUsersLike->rowCount();
|
|
|
|
$deleted['coaches'] = qdb_dev_delete_by_ids($pdo, 'coach', 'coachID', $coachIdList);
|
|
$delCoachLike = $pdo->prepare('DELETE FROM coach WHERE username LIKE :pfx');
|
|
$delCoachLike->execute([':pfx' => $userLike]);
|
|
$deleted['coaches'] += $delCoachLike->rowCount();
|
|
|
|
$deleted['supervisors'] = qdb_dev_delete_by_ids($pdo, 'supervisor', 'supervisorID', array_keys($supervisorIds));
|
|
$delSvLike = $pdo->prepare('DELETE FROM supervisor WHERE username LIKE :pfx');
|
|
$delSvLike->execute([':pfx' => $userLike]);
|
|
$deleted['supervisors'] += $delSvLike->rowCount();
|
|
|
|
$deleted['admins'] = qdb_dev_delete_by_ids($pdo, 'admin', 'adminID', array_keys($adminIds));
|
|
$delAdLike = $pdo->prepare('DELETE FROM admin WHERE username LIKE :pfx');
|
|
$delAdLike->execute([':pfx' => $userLike]);
|
|
$deleted['admins'] += $delAdLike->rowCount();
|
|
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
|
|
return ['deleted' => $deleted];
|
|
}
|
|
|
|
/**
|
|
* Remove all clients, completions, coaches, and supervisors. Keeps admin accounts
|
|
* and questionnaire definitions (questions, translations, etc.).
|
|
*
|
|
* @return array{deleted: array<string, int>, kept: array<string, int>}
|
|
*/
|
|
function qdb_wipe_all_data_except_admins(PDO $pdo): array
|
|
{
|
|
$deleted = [
|
|
'client_answers' => 0,
|
|
'completed_questionnaires' => 0,
|
|
'clients' => 0,
|
|
'sessions' => 0,
|
|
'users' => 0,
|
|
'coaches' => 0,
|
|
'supervisors' => 0,
|
|
];
|
|
|
|
$keptStmt = $pdo->query("SELECT COUNT(*) FROM users WHERE role = 'admin'");
|
|
$keptAdmins = (int)$keptStmt->fetchColumn();
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
// Interview data references clients/coaches; coach references supervisor.
|
|
// Disable FK checks for this bulk wipe (same pattern as questionnaire delete).
|
|
$pdo->exec('PRAGMA foreign_keys = OFF');
|
|
|
|
$deleted['client_answers'] = (int)$pdo->exec('DELETE FROM client_answer');
|
|
if (qdb_table_exists($pdo, 'client_answer_submission')) {
|
|
$pdo->exec('DELETE FROM client_answer_submission');
|
|
}
|
|
if (qdb_table_exists($pdo, 'questionnaire_submission')) {
|
|
$pdo->exec('DELETE FROM questionnaire_submission');
|
|
}
|
|
if (qdb_table_exists($pdo, 'client_followup_note')) {
|
|
$pdo->exec('DELETE FROM client_followup_note');
|
|
}
|
|
$deleted['completed_questionnaires'] = (int)$pdo->exec('DELETE FROM completed_questionnaire');
|
|
$deleted['clients'] = (int)$pdo->exec('DELETE FROM client');
|
|
|
|
$deleted['coaches'] = (int)$pdo->exec('DELETE FROM coach');
|
|
$deleted['supervisors'] = (int)$pdo->exec('DELETE FROM supervisor');
|
|
|
|
$sess = $pdo->prepare(
|
|
"DELETE FROM session WHERE userID IN (SELECT userID FROM users WHERE role != 'admin')"
|
|
);
|
|
$sess->execute();
|
|
$deleted['sessions'] = $sess->rowCount();
|
|
|
|
$users = $pdo->prepare("DELETE FROM users WHERE role != 'admin'");
|
|
$users->execute();
|
|
$deleted['users'] = $users->rowCount();
|
|
|
|
$pdo->exec('PRAGMA foreign_keys = ON');
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
try {
|
|
$pdo->exec('PRAGMA foreign_keys = ON');
|
|
} catch (Throwable $ignored) {
|
|
}
|
|
throw $e;
|
|
}
|
|
|
|
return [
|
|
'deleted' => $deleted,
|
|
'kept' => ['admins' => $keptAdmins],
|
|
];
|
|
}
|