Files
nat-as-server/lib/dev_fixture.php

814 lines
30 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';
/**
* @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,
'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;
}
$completedAt = isset($row['completedAt']) ? (int)$row['completedAt'] : ($now - ($variant % 90) * 86400);
$startedAt = isset($row['startedAt']) ? (int)$row['startedAt'] : ($completedAt - 600);
$answers = qdb_dev_build_answers_for_questionnaire($pdo, $qnID, $variant);
$answerCount = qdb_dev_persist_submission(
$pdo,
$qnID,
$clientCode,
(string)$coachID,
$answers,
$startedAt,
$completedAt
);
$imported['completions']++;
$imported['answers'] += $answerCount;
$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);
}
if ((int)($fixture['fixtureVersion'] ?? 0) !== 1) {
json_error('INVALID_FIELD', 'fixtureVersion must be 1', 400);
}
return $prefix;
}
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 = [
'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 {
$deleted['client_answers'] = qdb_dev_delete_by_ids($pdo, 'client_answer', 'clientCode', $clientCodeList);
if ($clientCodeList !== []) {
foreach (array_chunk($clientCodeList, 200) as $chunk) {
$ph = implode(',', array_fill(0, count($chunk), '?'));
$delCq = $pdo->prepare("DELETE FROM completed_questionnaire WHERE clientCode IN ($ph)");
$delCq->execute($chunk);
$deleted['completed_questionnaires'] += $delCq->rowCount();
}
}
if ($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],
];
}