420 lines
14 KiB
PHP
420 lines
14 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;
|
|
}
|
|
|
|
/**
|
|
* sessionNumber (1-based visit index) and orderIndex (0-based sort key) stay in sync:
|
|
* orderIndex = sessionNumber - 1.
|
|
*
|
|
* @param array<string, mixed> $body
|
|
* @param array<string, mixed>|null $existing
|
|
* @return array{sessionNumber: int, orderIndex: int}
|
|
*/
|
|
function qdb_program_session_resolve_order(PDO $pdo, array $body, ?array $existing = null): array
|
|
{
|
|
if (isset($body['sessionNumber'])) {
|
|
$sessionNumber = max(1, (int)$body['sessionNumber']);
|
|
return [
|
|
'sessionNumber' => $sessionNumber,
|
|
'orderIndex' => $sessionNumber - 1,
|
|
];
|
|
}
|
|
if (isset($body['orderIndex'])) {
|
|
$orderIndex = max(0, (int)$body['orderIndex']);
|
|
return [
|
|
'sessionNumber' => $orderIndex + 1,
|
|
'orderIndex' => $orderIndex,
|
|
];
|
|
}
|
|
if ($existing !== null) {
|
|
$sessionNumber = max(1, (int)$existing['sessionNumber']);
|
|
return [
|
|
'sessionNumber' => $sessionNumber,
|
|
'orderIndex' => $sessionNumber - 1,
|
|
];
|
|
}
|
|
|
|
$maxNum = (int)$pdo->query('SELECT COALESCE(MAX(sessionNumber), 0) FROM program_session')->fetchColumn();
|
|
$maxOrd = (int)$pdo->query('SELECT COALESCE(MAX(orderIndex), -1) FROM program_session')->fetchColumn();
|
|
$sessionNumber = max($maxNum, $maxOrd + 1, 0) + 1;
|
|
|
|
return [
|
|
'sessionNumber' => $sessionNumber,
|
|
'orderIndex' => $sessionNumber - 1,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @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'];
|
|
}
|