Files
barometer-server/lib/program_sessions.php
tom.hempel 7fefc264f4
Some checks failed
PHPUnit / test (push) Has been cancelled
session cylces and adjustments
2026-06-29 13:26:08 +02:00

378 lines
12 KiB
PHP

<?php
/**
* Program session definitions: ordered visit slots with assigned questionnaires.
*/
const QDB_PROGRAM_SESSIONS_AUTO_REPEAT_KEY = 'program_sessions_auto_repeat';
/**
* @return array{autoRepeatSessions: bool}
*/
function qdb_program_session_settings_get(PDO $pdo): array
{
require_once __DIR__ . '/settings.php';
qdb_ensure_system_setting_table($pdo);
$stmt = $pdo->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<array<string, mixed>>
*/
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<array<string, mixed>>
*/
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<array{questionnaireID: string, orderIndex: int}>
*/
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<array{questionnaireID: string, orderIndex: int}> $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<string, mixed>
*/
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<array<string, mixed>>, 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'];
}