// Timetable group and entry data access helpers. const { fetchPagedRows, validateMaxLength } = require('./utils'); const NAME_MAX_LENGTH = 255; const DESCRIPTION_MAX_LENGTH = 255; const DEFAULT_TIME_ZONE = 'Europe/London'; function normalizeTimeZone(value, fallback) { const raw = String(value || '').trim(); if (!raw) { return String(fallback || DEFAULT_TIME_ZONE).trim() || DEFAULT_TIME_ZONE; } try { new Intl.DateTimeFormat('en-GB', { timeZone: raw }).format(new Date()); return raw; } catch (_error) { const error = new Error('Timetable time zone is invalid.'); error.statusCode = 400; throw error; } } function normalizeDisplayMode(value) { const mode = String(value || 'upcoming').trim().toLowerCase(); if (mode === 'current' || mode === 'both') { return mode; } return 'upcoming'; } async function fetchTimetablesData(pool) { const [timetableGroups] = await pool.query(` SELECT g.id, g.name, g.short_description, g.timezone, g.created_at, g.modified_at, g.created_by, g.modified_by, (SELECT COUNT(*) FROM i_timetable_entries e WHERE e.schedule_group_id = g.id) AS entry_count, (SELECT MIN(e.start_datetime) FROM i_timetable_entries e WHERE e.schedule_group_id = g.id AND e.start_datetime IS NOT NULL) AS next_start_datetime FROM i_timetable_groups g ORDER BY g.modified_at DESC, g.id DESC `); const [timetableEntries] = await pool.query(` SELECT id, schedule_group_id, title, short_description, start_datetime, end_datetime, created_at, modified_at, created_by, modified_by FROM i_timetable_entries ORDER BY schedule_group_id ASC, start_datetime ASC, id ASC `); const entriesByGroupId = new Map(); timetableEntries.forEach(function (entry) { const groupId = Number(entry.schedule_group_id); if (!entriesByGroupId.has(groupId)) { entriesByGroupId.set(groupId, []); } entriesByGroupId.get(groupId).push(entry); }); const groups = timetableGroups.map(function (group) { return Object.assign({}, group, { entries: entriesByGroupId.get(Number(group.id)) || [] }); }); return { timetableGroups: groups, timetableEntries: timetableEntries }; } async function fetchTimetableGroupsPage(pool, page, pageSize, searchTerm, sortKey, sortDirection) { const paged = await fetchPagedRows(pool, { selectSql: `SELECT g.id, g.name, g.short_description, g.timezone, g.created_at, g.modified_at, g.created_by, g.modified_by, (SELECT COUNT(*) FROM i_timetable_entries e WHERE e.schedule_group_id = g.id) AS entry_count, (SELECT MIN(e.start_datetime) FROM i_timetable_entries e WHERE e.schedule_group_id = g.id AND e.start_datetime IS NOT NULL) AS next_start_datetime FROM i_timetable_groups g ORDER BY g.modified_at DESC, g.id DESC`, countSql: 'SELECT COUNT(*) AS count FROM i_timetable_groups', searchColumns: ['g.name', 'g.short_description'], searchTerm: searchTerm, sortColumns: { name: 'g.name', description: 'g.short_description', timezone: 'g.timezone', entries: 'entry_count', next_start: 'next_start_datetime', created: 'g.created_at', modified: 'g.modified_at' }, sortKey: sortKey, sortDirection: sortDirection, page: page, pageSize: pageSize }); return Object.assign({ timetableGroups: paged.rows }, paged); } async function fetchTimetableGroupById(pool, id) { const [rows] = await pool.query( 'SELECT id, name, short_description, timezone, created_at, modified_at, created_by, modified_by FROM i_timetable_groups WHERE id = ?', [id] ); return rows[0] || null; } async function fetchTimetableEntriesByGroupId(pool, timetableGroupId) { const [rows] = await pool.query( `SELECT id, schedule_group_id, title, short_description, start_datetime, end_datetime, created_at, modified_at, created_by, modified_by FROM i_timetable_entries WHERE schedule_group_id = ? ORDER BY start_datetime ASC, id ASC`, [timetableGroupId] ); return rows; } function buildTimetableGroupPayload(req, existingTimetableGroup) { const fallback = existingTimetableGroup || {}; const name = validateMaxLength(req.body.name || fallback.name || '', NAME_MAX_LENGTH, 'Timetable group name'); const shortDescription = validateMaxLength(req.body.short_description || req.body.shortDescription || fallback.short_description || '', DESCRIPTION_MAX_LENGTH, 'Timetable group description'); const timezone = normalizeTimeZone(req.body.timezone || req.body.time_zone || fallback.timezone || DEFAULT_TIME_ZONE, fallback.timezone || DEFAULT_TIME_ZONE); if (!name) { const error = new Error('Timetable group name is required.'); error.statusCode = 400; throw error; } return { name: name, shortDescription: shortDescription, timezone: timezone }; } module.exports = { normalizeDisplayMode, fetchTimetablesData, fetchTimetableGroupsPage, fetchTimetableGroupById, fetchTimetableEntriesByGroupId, buildTimetableGroupPayload, normalizeTimeZone };