319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
// Timetable route registration.
|
|
|
|
const { buildPagination } = require('../../../lib/pagination');
|
|
const renderTimetableGroupsPage = require('./list');
|
|
const renderTimetableGroupAddPage = require('./add');
|
|
const renderTimetableGroupEditPage = require('./edit');
|
|
|
|
async function getDataSourceUsageMaps(pool, common) {
|
|
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
|
|
const timetableGroupIds = new Set();
|
|
|
|
for (let index = 0; index < slides.length; index += 1) {
|
|
const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(slides[index].content_json) : null;
|
|
if (!content || typeof content !== 'object') {
|
|
continue;
|
|
}
|
|
|
|
(function walk(value) {
|
|
if (!value || typeof value !== 'object') {
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach(walk);
|
|
return;
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(value, 'schedule_group_id')) {
|
|
const timetableGroupId = Number(value.schedule_group_id);
|
|
if (Number.isFinite(timetableGroupId)) {
|
|
timetableGroupIds.add(timetableGroupId);
|
|
}
|
|
}
|
|
|
|
Object.keys(value).forEach(function (key) {
|
|
walk(value[key]);
|
|
});
|
|
})(content);
|
|
}
|
|
|
|
return timetableGroupIds;
|
|
}
|
|
|
|
function readScheduleArrayFieldValue(body, keys) {
|
|
const searchKeys = Array.isArray(keys) ? keys : [keys];
|
|
for (let index = 0; index < searchKeys.length; index += 1) {
|
|
const key = searchKeys[index];
|
|
const value = body && Object.prototype.hasOwnProperty.call(body, key) ? body[key] : undefined;
|
|
if (Array.isArray(value)) {
|
|
return value;
|
|
}
|
|
if (value !== undefined && value !== null) {
|
|
return [value];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function buildScheduleEntryRows(req, parseDateTimeLocal) {
|
|
const body = req.body || {};
|
|
const ids = readScheduleArrayFieldValue(body, ['entry_id[]', 'entry_id']);
|
|
const titles = readScheduleArrayFieldValue(body, ['entry_title[]', 'entry_title']);
|
|
const descriptions = readScheduleArrayFieldValue(body, ['entry_short_description[]', 'entry_short_description']);
|
|
const starts = readScheduleArrayFieldValue(body, ['entry_start_datetime[]', 'entry_start_datetime']);
|
|
const ends = readScheduleArrayFieldValue(body, ['entry_end_datetime[]', 'entry_end_datetime']);
|
|
|
|
const lengths = [ids.length, titles.length, descriptions.length, starts.length, ends.length].filter(Boolean);
|
|
if (lengths.length && lengths.some(function (value) { return value !== lengths[0]; })) {
|
|
const error = new Error('Schedule entry data is invalid.');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
|
|
const rows = [];
|
|
for (let index = 0; index < titles.length; index += 1) {
|
|
const idValue = Number(ids[index]);
|
|
const title = String(titles[index] || '').trim();
|
|
const shortDescription = String(descriptions[index] || '').trim();
|
|
const startDatetime = parseDateTimeLocal(starts[index]);
|
|
const endDatetime = parseDateTimeLocal(ends[index]);
|
|
const hasContent = title || shortDescription || String(starts[index] || '').trim() || String(ends[index] || '').trim() || Number.isFinite(idValue);
|
|
|
|
if (!hasContent) {
|
|
continue;
|
|
}
|
|
|
|
if (!title) {
|
|
const error = new Error('Each schedule entry requires a title.');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
|
|
if (!startDatetime) {
|
|
const error = new Error('Each schedule entry requires a start datetime.');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
|
|
if (endDatetime && endDatetime < startDatetime) {
|
|
const error = new Error('Schedule entry end datetime must be after the start datetime.');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
|
|
rows.push({
|
|
id: Number.isFinite(idValue) && idValue > 0 ? idValue : null,
|
|
title: title,
|
|
shortDescription: shortDescription,
|
|
startDatetime: startDatetime,
|
|
endDatetime: endDatetime
|
|
});
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
module.exports = function registerTimetableRoutes(app, deps) {
|
|
const pool = deps.pool;
|
|
const common = deps.common;
|
|
const pages = deps.pages;
|
|
const getAuditUserId = deps.getAuditUserId;
|
|
const redirectAfterSave = deps.redirectAfterSave;
|
|
const parseDateTimeLocal = deps.parseDateTimeLocal;
|
|
const formatDashboardDate = deps.formatDashboardDate || function (value) {
|
|
return value ? String(value) : '';
|
|
};
|
|
const requirePermission = deps.requirePermission;
|
|
|
|
const LIST_PAGE_SIZE = 25;
|
|
|
|
app.get('/data-sources/timetables', requirePermission('timetables.read'), async function (req, res, next) {
|
|
try {
|
|
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
|
|
const search = common.getSearchQuery(req);
|
|
const sort = common.getSortQuery(req);
|
|
const direction = common.getSortDirectionQuery(req);
|
|
const data = await common.fetchTimetableGroupsPage(pool, page, LIST_PAGE_SIZE, search, sort, direction);
|
|
const usageIds = await getDataSourceUsageMaps(pool, common);
|
|
const timetableGroups = (data.timetableGroups || []).map(function (timetableGroup) {
|
|
return Object.assign({}, timetableGroup, {
|
|
inUse: usageIds.has(Number(timetableGroup.id))
|
|
});
|
|
});
|
|
|
|
res.send(renderTimetableGroupsPage({
|
|
timetableGroups: timetableGroups,
|
|
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'Timetables', 'Timetable groups')
|
|
}, req.query.message ? String(req.query.message) : '', req.currentUser, formatDashboardDate));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/data-sources/timetables/new', requirePermission('timetables.create'), function (req, res) {
|
|
res.send(renderTimetableGroupAddPage(null, [], req.query.message ? String(req.query.message) : '', req.currentUser));
|
|
});
|
|
|
|
app.get('/data-sources/timetables/:id/edit', requirePermission('timetables.update'), async function (req, res, next) {
|
|
try {
|
|
const timetableGroup = await common.fetchTimetableGroupById(pool, Number(req.params.id));
|
|
if (!timetableGroup) {
|
|
return res.status(404).send('Timetable group not found');
|
|
}
|
|
|
|
const usageIds = await getDataSourceUsageMaps(pool, common);
|
|
const timetableEntries = typeof common.fetchTimetableEntriesByGroupId === 'function'
|
|
? await common.fetchTimetableEntriesByGroupId(pool, timetableGroup.id)
|
|
: [];
|
|
|
|
res.send(renderTimetableGroupEditPage(Object.assign({}, timetableGroup, {
|
|
shortDescription: timetableGroup.short_description || '',
|
|
inUse: usageIds.has(Number(timetableGroup.id))
|
|
}), timetableEntries, req.query.message ? String(req.query.message) : '', req.currentUser));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/data-sources/timetables', requirePermission('timetables.create'), async function (req, res, next) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const payload = common.buildTimetableGroupPayload(req, null);
|
|
if (await common.fetchDuplicateName(pool, 'i_schedule_groups', payload.name)) {
|
|
return res.redirect('/data-sources/timetables/new?message=' + encodeURIComponent('A timetable group with that name already exists.'));
|
|
}
|
|
|
|
const entryRows = buildScheduleEntryRows(req, parseDateTimeLocal);
|
|
const actorId = getAuditUserId(req);
|
|
await connection.beginTransaction();
|
|
const [result] = await connection.query(
|
|
'INSERT INTO i_schedule_groups (name, short_description, created_by, modified_by) VALUES (?, ?, ?, ?)',
|
|
[payload.name, payload.shortDescription || null, actorId, actorId]
|
|
);
|
|
|
|
if (entryRows.length) {
|
|
const insertRows = entryRows.map(function (entry) {
|
|
return [
|
|
result.insertId,
|
|
entry.title,
|
|
entry.shortDescription || null,
|
|
entry.startDatetime,
|
|
entry.endDatetime || null,
|
|
actorId,
|
|
actorId
|
|
];
|
|
});
|
|
|
|
await connection.query(
|
|
'INSERT INTO i_schedule_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
|
[insertRows]
|
|
);
|
|
}
|
|
|
|
await connection.commit();
|
|
redirectAfterSave(req, res, '/data-sources/timetables/' + result.insertId + '/edit', {
|
|
closeUrl: '/data-sources/timetables',
|
|
newUrl: '/data-sources/timetables/new',
|
|
message: 'Timetable group created.'
|
|
});
|
|
} catch (error) {
|
|
try {
|
|
await connection.rollback();
|
|
} catch (_rollbackError) {
|
|
// Ignore rollback failures and surface the original error.
|
|
}
|
|
next(error);
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
app.post('/data-sources/timetables/:id', requirePermission('timetables.update'), async function (req, res, next) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const timetableGroup = await common.fetchTimetableGroupById(pool, Number(req.params.id));
|
|
if (!timetableGroup) {
|
|
return res.status(404).send('Timetable group not found');
|
|
}
|
|
|
|
const payload = common.buildTimetableGroupPayload(req, timetableGroup);
|
|
if (await common.fetchDuplicateName(pool, 'i_schedule_groups', payload.name, timetableGroup.id)) {
|
|
return res.redirect('/data-sources/timetables/' + timetableGroup.id + '/edit?message=' + encodeURIComponent('A timetable group with that name already exists.'));
|
|
}
|
|
|
|
const entryRows = buildScheduleEntryRows(req, parseDateTimeLocal);
|
|
const actorId = getAuditUserId(req);
|
|
await connection.beginTransaction();
|
|
await connection.query(
|
|
'UPDATE i_schedule_groups SET name = ?, short_description = ?, modified_by = ? WHERE id = ?',
|
|
[payload.name, payload.shortDescription || null, actorId, timetableGroup.id]
|
|
);
|
|
await connection.query('DELETE FROM i_schedule_entries WHERE schedule_group_id = ?', [timetableGroup.id]);
|
|
|
|
if (entryRows.length) {
|
|
const insertRows = entryRows.map(function (entry) {
|
|
return [
|
|
timetableGroup.id,
|
|
entry.title,
|
|
entry.shortDescription || null,
|
|
entry.startDatetime,
|
|
entry.endDatetime || null,
|
|
actorId,
|
|
actorId
|
|
];
|
|
});
|
|
|
|
await connection.query(
|
|
'INSERT INTO i_schedule_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
|
[insertRows]
|
|
);
|
|
}
|
|
|
|
await connection.commit();
|
|
redirectAfterSave(req, res, '/data-sources/timetables/' + timetableGroup.id + '/edit', {
|
|
closeUrl: '/data-sources/timetables',
|
|
newUrl: '/data-sources/timetables/new',
|
|
message: 'Timetable group updated.'
|
|
});
|
|
} catch (error) {
|
|
try {
|
|
await connection.rollback();
|
|
} catch (_rollbackError) {
|
|
// Ignore rollback failures and surface the original error.
|
|
}
|
|
next(error);
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
app.post('/data-sources/timetables/:id/delete', requirePermission('timetables.delete'), async function (req, res, next) {
|
|
try {
|
|
const timetableGroup = await common.fetchTimetableGroupById(pool, Number(req.params.id));
|
|
if (!timetableGroup) {
|
|
return res.status(404).send('Timetable group not found');
|
|
}
|
|
|
|
const usageIds = await getDataSourceUsageMaps(pool, common);
|
|
if (usageIds.has(Number(timetableGroup.id))) {
|
|
return res.redirect('/data-sources/timetables?message=' + encodeURIComponent('This timetable group is still used by one or more slides.'));
|
|
}
|
|
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
await connection.beginTransaction();
|
|
await connection.query('DELETE FROM i_schedule_groups WHERE id = ?', [timetableGroup.id]);
|
|
await connection.commit();
|
|
} catch (error) {
|
|
await connection.rollback();
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
res.redirect('/data-sources/timetables?message=' + encodeURIComponent('Timetable group deleted.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}; |