This commit is contained in:
377
lib/program_sessions.php
Normal file
377
lib/program_sessions.php
Normal file
@ -0,0 +1,377 @@
|
||||
<?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'];
|
||||
}
|
||||
Reference in New Issue
Block a user