prepare( 'SELECT settingValue FROM system_setting WHERE settingKey = :k' ); $stmt->execute([':k' => QDB_PROGRAM_SESSIONS_AUTO_REPEAT_KEY]); $val = $stmt->fetchColumn(); return [ 'autoRepeatSessions' => $val !== false && (int)$val === 1, ]; } /** * @param array{autoRepeatSessions?: bool} $partial * @return array{autoRepeatSessions: bool} */ function qdb_program_session_settings_save(PDO $pdo, array $partial): array { $current = qdb_program_session_settings_get($pdo); if (array_key_exists('autoRepeatSessions', $partial)) { $current['autoRepeatSessions'] = !empty($partial['autoRepeatSessions']); } require_once __DIR__ . '/settings.php'; qdb_ensure_system_setting_table($pdo); $stmt = $pdo->prepare( 'INSERT INTO system_setting (settingKey, settingValue) VALUES (:k, :v) ON CONFLICT(settingKey) DO UPDATE SET settingValue = excluded.settingValue' ); $stmt->execute([ ':k' => QDB_PROGRAM_SESSIONS_AUTO_REPEAT_KEY, ':v' => $current['autoRepeatSessions'] ? '1' : '0', ]); return $current; } /** * @return list> */ function qdb_program_session_questionnaires(PDO $pdo, string $sessionID): array { $stmt = $pdo->prepare( 'SELECT psq.questionnaireID, psq.orderIndex, q.name FROM program_session_questionnaire psq LEFT JOIN questionnaire q ON q.questionnaireID = psq.questionnaireID WHERE psq.sessionID = :sid ORDER BY psq.orderIndex, psq.questionnaireID' ); $stmt->execute([':sid' => $sessionID]); $rows = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $rows[] = [ 'questionnaireID' => (string)$row['questionnaireID'], 'name' => (string)($row['name'] ?? ''), 'orderIndex' => (int)$row['orderIndex'], ]; } return $rows; } /** * @return list> */ function qdb_list_program_sessions(PDO $pdo, bool $activeOnly = false): array { $sql = 'SELECT sessionID, name, description, sessionNumber, orderIndex, isActive, createdAt, updatedAt FROM program_session'; if ($activeOnly) { $sql .= ' WHERE isActive = 1'; } $sql .= ' ORDER BY orderIndex, sessionNumber, name'; $stmt = $pdo->query($sql); $sessions = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $row['isActive'] = (int)$row['isActive']; $row['sessionNumber'] = (int)$row['sessionNumber']; $row['orderIndex'] = (int)$row['orderIndex']; $row['questionnaires'] = qdb_program_session_questionnaires($pdo, $row['sessionID']); $sessions[] = $row; } return $sessions; } function qdb_get_program_session(PDO $pdo, string $sessionID): ?array { $stmt = $pdo->prepare( 'SELECT sessionID, name, description, sessionNumber, orderIndex, isActive, createdAt, updatedAt FROM program_session WHERE sessionID = :sid' ); $stmt->execute([':sid' => $sessionID]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { return null; } $row['isActive'] = (int)$row['isActive']; $row['sessionNumber'] = (int)$row['sessionNumber']; $row['orderIndex'] = (int)$row['orderIndex']; $row['questionnaires'] = qdb_program_session_questionnaires($pdo, $sessionID); return $row; } /** * @return list */ function qdb_parse_program_session_questionnaires(array $rows): array { $out = []; $idx = 0; foreach ($rows as $row) { if (!is_array($row)) { continue; } $qnID = trim((string)($row['questionnaireID'] ?? '')); if ($qnID === '') { continue; } $out[] = [ 'questionnaireID' => $qnID, 'orderIndex' => (int)($row['orderIndex'] ?? $idx), ]; $idx++; } return $out; } function qdb_sync_program_session_questionnaires(PDO $pdo, string $sessionID, array $members): void { $pdo->prepare('DELETE FROM program_session_questionnaire WHERE sessionID = :sid') ->execute([':sid' => $sessionID]); if ($members === []) { return; } $ins = $pdo->prepare( 'INSERT INTO program_session_questionnaire (sessionID, questionnaireID, orderIndex) VALUES (:sid, :qn, :o)' ); foreach ($members as $m) { $ins->execute([ ':sid' => $sessionID, ':qn' => $m['questionnaireID'], ':o' => $m['orderIndex'], ]); } } /** * @param list $members */ function qdb_validate_program_session_questionnaires(PDO $pdo, array $members): ?string { if ($members === []) { return null; } $chk = $pdo->prepare('SELECT 1 FROM questionnaire WHERE questionnaireID = :id'); foreach ($members as $m) { $chk->execute([':id' => $m['questionnaireID']]); if (!$chk->fetch()) { return 'Unknown questionnaire: ' . $m['questionnaireID']; } } return null; } /** * @return array{cycleNumber: int, cycleStartedAt: int, updatedAt: int} */ function qdb_client_program_cycle_get(PDO $pdo, string $clientCode, bool $create = true): array { $stmt = $pdo->prepare( 'SELECT cycleNumber, cycleStartedAt, updatedAt FROM client_program_cycle WHERE clientCode = :cc' ); $stmt->execute([':cc' => $clientCode]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { return [ 'cycleNumber' => max(1, (int)$row['cycleNumber']), 'cycleStartedAt' => (int)$row['cycleStartedAt'], 'updatedAt' => (int)$row['updatedAt'], ]; } if (!$create) { return ['cycleNumber' => 1, 'cycleStartedAt' => 0, 'updatedAt' => 0]; } $now = time(); $pdo->prepare( 'INSERT INTO client_program_cycle (clientCode, cycleNumber, cycleStartedAt, updatedAt) VALUES (:cc, 1, 0, :t)' )->execute([':cc' => $clientCode, ':t' => $now]); return ['cycleNumber' => 1, 'cycleStartedAt' => 0, 'updatedAt' => $now]; } function qdb_program_session_is_complete_for_client( PDO $pdo, string $clientCode, array $session, int $cycleStartedAt, ): bool { $questionnaires = $session['questionnaires'] ?? []; if ($questionnaires === []) { return true; } if ($cycleStartedAt <= 0) { $stmt = $pdo->prepare( "SELECT 1 FROM completed_questionnaire WHERE clientCode = :cc AND questionnaireID = :qn AND status = 'completed' LIMIT 1" ); foreach ($questionnaires as $q) { $stmt->execute([ ':cc' => $clientCode, ':qn' => $q['questionnaireID'], ]); if (!$stmt->fetch()) { return false; } } return true; } $stmt = $pdo->prepare( "SELECT 1 FROM completed_questionnaire WHERE clientCode = :cc AND questionnaireID = :qn AND status = 'completed' AND completedAt > :cycleStart LIMIT 1" ); foreach ($questionnaires as $q) { $stmt->execute([ ':cc' => $clientCode, ':qn' => $q['questionnaireID'], ':cycleStart' => $cycleStartedAt, ]); if (!$stmt->fetch()) { return false; } } return true; } /** * @return array */ function qdb_client_program_progress(PDO $pdo, string $clientCode): array { $settings = qdb_program_session_settings_get($pdo); $sessions = qdb_list_program_sessions($pdo, true); $cycle = qdb_client_program_cycle_get($pdo, $clientCode); $totalSessions = count($sessions); if ($totalSessions === 0) { return [ 'cycleNumber' => $cycle['cycleNumber'], 'cycleStartedAt' => (int)$cycle['cycleStartedAt'], 'totalSessions' => 0, 'sessionsCompletedInCycle' => 0, 'currentSessionID' => null, 'currentSessionNumber' => null, 'currentSessionName' => null, 'cycleComplete' => true, 'programComplete' => true, 'autoRepeatSessions' => $settings['autoRepeatSessions'], ]; } $completedCount = 0; $currentSession = null; foreach ($sessions as $session) { if (!qdb_program_session_is_complete_for_client( $pdo, $clientCode, $session, $cycle['cycleStartedAt'], )) { $currentSession = $session; break; } $completedCount++; } $allComplete = $currentSession === null; $programComplete = $allComplete && !$settings['autoRepeatSessions']; return [ 'cycleNumber' => $cycle['cycleNumber'], 'cycleStartedAt' => (int)$cycle['cycleStartedAt'], 'totalSessions' => $totalSessions, 'sessionsCompletedInCycle' => $completedCount, 'currentSessionID' => $currentSession['sessionID'] ?? null, 'currentSessionNumber' => isset($currentSession['sessionNumber']) ? (int)$currentSession['sessionNumber'] : null, 'currentSessionName' => $currentSession['name'] ?? null, 'cycleComplete' => $allComplete, 'programComplete' => $programComplete, 'autoRepeatSessions' => $settings['autoRepeatSessions'], ]; } /** * When auto-repeat is enabled and every active session is complete in the current cycle, * advance to the next cycle so counselors start again at session 1. */ function qdb_maybe_advance_program_cycle(PDO $pdo, string $clientCode): bool { $settings = qdb_program_session_settings_get($pdo); if (!$settings['autoRepeatSessions']) { return false; } $sessions = qdb_list_program_sessions($pdo, true); if ($sessions === []) { return false; } $cycle = qdb_client_program_cycle_get($pdo, $clientCode); foreach ($sessions as $session) { if (!qdb_program_session_is_complete_for_client( $pdo, $clientCode, $session, $cycle['cycleStartedAt'], )) { return false; } } $now = time(); $pdo->prepare( 'UPDATE client_program_cycle SET cycleNumber = cycleNumber + 1, cycleStartedAt = :t, updatedAt = :t WHERE clientCode = :cc' )->execute([':cc' => $clientCode, ':t' => $now]); return true; } /** * Mobile-friendly session list (active only, no admin timestamps). * * @return array{sessions: list>, settings: array{autoRepeatSessions: bool}} */ function qdb_app_program_sessions_payload(PDO $pdo): array { $settings = qdb_program_session_settings_get($pdo); $sessions = qdb_list_program_sessions($pdo, true); $out = []; foreach ($sessions as $s) { $out[] = [ 'sessionID' => $s['sessionID'], 'name' => $s['name'], 'description' => $s['description'], 'sessionNumber' => $s['sessionNumber'], 'orderIndex' => $s['orderIndex'], 'questionnaires' => array_map(static function (array $q): array { return [ 'questionnaireID' => $q['questionnaireID'], 'orderIndex' => $q['orderIndex'], ]; }, $s['questionnaires']), ]; } return [ 'sessions' => $out, 'settings' => $settings, ]; } /** @deprecated Use qdb_app_program_sessions_payload() */ function qdb_app_program_sessions(PDO $pdo): array { return qdb_app_program_sessions_payload($pdo)['sessions']; }