From e589b46a8a1d68c9ae25e6cdccfd382e53b187b9 Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Wed, 1 Jul 2026 08:44:29 +0200 Subject: [PATCH] fixed session numbering --- handlers/program_sessions.php | 14 +++++----- lib/program_sessions.php | 42 ++++++++++++++++++++++++++++++ tests/Unit/ProgramSessionsTest.php | 30 +++++++++++++++++++++ website/js/pages/sessions.js | 34 ++++++++++-------------- 4 files changed, 92 insertions(+), 28 deletions(-) diff --git a/handlers/program_sessions.php b/handlers/program_sessions.php index d6e7c6a..22d095d 100644 --- a/handlers/program_sessions.php +++ b/handlers/program_sessions.php @@ -40,11 +40,12 @@ case 'POST': if ($name === '') { json_error('BAD_REQUEST', 'name is required', 400); } - $sessionNumber = max(1, (int)($body['sessionNumber'] ?? 1)); - $orderIndex = max(0, (int)($body['orderIndex'] ?? 0)); $members = qdb_parse_program_session_questionnaires($body['questionnaires'] ?? []); try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); + $order = qdb_program_session_resolve_order($pdo, $body); + $sessionNumber = $order['sessionNumber']; + $orderIndex = $order['orderIndex']; $err = qdb_validate_program_session_questionnaires($pdo, $members); if ($err !== null) { qdb_discard($tmpDb, $lockFp); @@ -103,6 +104,7 @@ case 'PUT': json_error('INVALID_FIELD', $err, 400); } } + $order = qdb_program_session_resolve_order($pdo, $body, $existing); $pdo->prepare( 'UPDATE program_session SET name = :name, @@ -115,12 +117,8 @@ case 'PUT': )->execute([ ':name' => $name, ':desc' => trim($body['description'] ?? $existing['description']), - ':num' => isset($body['sessionNumber']) - ? max(1, (int)$body['sessionNumber']) - : $existing['sessionNumber'], - ':ord' => isset($body['orderIndex']) - ? max(0, (int)$body['orderIndex']) - : $existing['orderIndex'], + ':num' => $order['sessionNumber'], + ':ord' => $order['orderIndex'], ':act' => isset($body['isActive']) ? (!empty($body['isActive']) ? 1 : 0) : $existing['isActive'], diff --git a/lib/program_sessions.php b/lib/program_sessions.php index b5a917d..61c8ea0 100644 --- a/lib/program_sessions.php +++ b/lib/program_sessions.php @@ -109,6 +109,48 @@ function qdb_get_program_session(PDO $pdo, string $sessionID): ?array { return $row; } +/** + * sessionNumber (1-based visit index) and orderIndex (0-based sort key) stay in sync: + * orderIndex = sessionNumber - 1. + * + * @param array $body + * @param array|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 */ diff --git a/tests/Unit/ProgramSessionsTest.php b/tests/Unit/ProgramSessionsTest.php index 6e03af7..f1e6b27 100644 --- a/tests/Unit/ProgramSessionsTest.php +++ b/tests/Unit/ProgramSessionsTest.php @@ -34,6 +34,36 @@ final class ProgramSessionsTest extends TestCase qdb_save($tmpDb, $lockFp); } + public function testResolveOrderFromSessionNumber(): void + { + [$pdo] = qdb_open(true); + require_once dirname(__DIR__, 2) . '/lib/program_sessions.php'; + + $order = qdb_program_session_resolve_order($pdo, ['sessionNumber' => 3]); + $this->assertSame(3, $order['sessionNumber']); + $this->assertSame(2, $order['orderIndex']); + } + + public function testResolveOrderFromOrderIndexLegacy(): void + { + [$pdo] = qdb_open(true); + require_once dirname(__DIR__, 2) . '/lib/program_sessions.php'; + + $order = qdb_program_session_resolve_order($pdo, ['orderIndex' => 4]); + $this->assertSame(5, $order['sessionNumber']); + $this->assertSame(4, $order['orderIndex']); + } + + public function testResolveOrderDefaultsToNextSession(): void + { + [$pdo] = qdb_open(true); + require_once dirname(__DIR__, 2) . '/lib/program_sessions.php'; + + $order = qdb_program_session_resolve_order($pdo, []); + $this->assertSame(2, $order['sessionNumber']); + $this->assertSame(1, $order['orderIndex']); + } + public function testAdvanceCycleWhenSessionComplete(): void { [$pdo, $tmpDb, $lockFp] = qdb_open(true); diff --git a/website/js/pages/sessions.js b/website/js/pages/sessions.js index 177ed91..5eb041f 100644 --- a/website/js/pages/sessions.js +++ b/website/js/pages/sessions.js @@ -36,8 +36,7 @@ export async function sessionsPage() { function renderSessions(sessions, questionnaires, settings) { const el = document.getElementById('sessionsContent'); const sorted = [...sessions].sort((a, b) => - (a.orderIndex ?? 0) - (b.orderIndex ?? 0) - || (a.sessionNumber ?? 0) - (b.sessionNumber ?? 0) + effectiveSessionNumber(a) - effectiveSessionNumber(b) || String(a.name).localeCompare(String(b.name)), ); const autoRepeat = !!settings.autoRepeatSessions; @@ -145,7 +144,7 @@ function sessionCardHTML(session, questionnaires) { ${esc(session.name)} ${activeBadge} -

Session ${session.sessionNumber} ยท Order ${session.orderIndex}

+

Session ${effectiveSessionNumber(session)}

${session.description ? `

${esc(session.description)}

` : ''} @@ -174,9 +173,7 @@ function openSessionModal(session, questionnaires, allSessions) { })) : []; const selectedIds = new Set(members.map(m => m.questionnaireID)); - const defaultOrder = allSessions?.length - ? Math.max(...allSessions.map(s => s.orderIndex ?? 0)) + 1 - : 0; + const defaultSessionNumber = (allSessions?.length ?? 0) + 1; const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; @@ -191,20 +188,14 @@ function openSessionModal(session, questionnaires, allSessions) {
- +
-
-
- - -
-
- -
+
+
@@ -314,7 +305,6 @@ function openSessionModal(session, questionnaires, allSessions) { const name = overlay.querySelector('#sessName')?.value.trim(); const description = overlay.querySelector('#sessDesc')?.value.trim() || ''; const sessionNumber = parseInt(overlay.querySelector('#sessNumber')?.value || '1', 10); - const orderIndex = parseInt(overlay.querySelector('#sessOrder')?.value || '0', 10); const isActive = overlay.querySelector('#sessActive')?.checked; if (!name) { showToast('Session name is required', 'error'); @@ -324,7 +314,6 @@ function openSessionModal(session, questionnaires, allSessions) { name, description, sessionNumber: Number.isFinite(sessionNumber) ? Math.max(1, sessionNumber) : 1, - orderIndex: Number.isFinite(orderIndex) ? Math.max(0, orderIndex) : 0, isActive, questionnaires: members.map((m, idx) => ({ questionnaireID: m.questionnaireID, @@ -347,6 +336,11 @@ function openSessionModal(session, questionnaires, allSessions) { }); } +function effectiveSessionNumber(session) { + if (!session) return 1; + return Math.max(session.sessionNumber ?? 1, (session.orderIndex ?? 0) + 1); +} + function esc(s) { const d = document.createElement('div'); d.textContent = s ?? '';