0, 'little_glass' => 1, 'moderate_glass' => 2, 'much_glass' => 3, 'extreme_glass' => 4, ]; require_once __DIR__ . '/app_answers.php'; require_once __DIR__ . '/submissions.php'; require_once __DIR__ . '/scoring.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, 'scoringRecomputed' => 0, ]; $skipped = [ 'admins' => 0, 'supervisors' => 0, 'coaches' => 0, 'clients' => 0, 'completions' => 0, ]; $errors = []; $coachIdByUsername = []; $supervisorIdByUsername = []; $affectedClients = []; $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[] = "Counselor $username: missing or invalid supervisor"; continue; } $supervisorID = $supervisorIdByUsername[$svUser] ?? qdb_dev_lookup_entity_id($pdo, $svUser); if ($supervisorID === '') { $errors[] = "Counselor $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']++; $affectedClients[$clientCode] = true; $variant++; } $upperPrefix = strtoupper(rtrim($prefix, '_')); $devClientStmt = $pdo->prepare('SELECT clientCode FROM client WHERE clientCode LIKE :pfx'); $devClientStmt->execute([':pfx' => $upperPrefix . '%']); foreach ($devClientStmt->fetchAll(PDO::FETCH_COLUMN) as $cc) { $affectedClients[(string)$cc] = true; } if ($affectedClients !== []) { $imported['scoringRecomputed'] = qdb_recompute_scores_for_clients( $pdo, array_keys($affectedClients) ); } $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 */ 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 */ 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': $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': $precision = $config['precision'] ?? 'full'; $dateRange = ($config['dateRange'] ?? 'past') === 'future' ? 'future' : 'past'; $year = $dateRange === 'future' ? ((int)date('Y') + ($v % 5)) : (2018 + ($v % 6)); $month = 1 + ($v % 12); $day = 1 + ($v % 28); $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 $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) ' . qdb_upsert_update( ['clientCode', 'questionID'], [ '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) ' . qdb_upsert_update( ['clientCode', 'questionnaireID'], [ '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 $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} */ 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, kept: array} */ 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). qdb_set_foreign_keys($pdo, false); $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(); qdb_set_foreign_keys($pdo, true); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } try { qdb_set_foreign_keys($pdo, true); } catch (Throwable $ignored) { } throw $e; } return [ 'deleted' => $deleted, 'kept' => ['admins' => $keptAdmins], ]; }