608 lines
28 KiB
JavaScript
608 lines
28 KiB
JavaScript
module.exports = function registerAdminManageRoutes(app, deps) {
|
|
const pool = deps.pool;
|
|
const common = deps.common;
|
|
const pages = deps.pages;
|
|
const fetchOrderedPlaylistSlides = deps.fetchOrderedPlaylistSlides;
|
|
const fetchScreensByPlaylistId = deps.fetchScreensByPlaylistId;
|
|
const fetchPlaylistCanvasSignature = deps.fetchPlaylistCanvasSignature;
|
|
const getCanvasSignature = deps.getCanvasSignature;
|
|
const normalizeScheduleMode = deps.normalizeScheduleMode;
|
|
const parseDateTimeLocal = deps.parseDateTimeLocal;
|
|
const parseTimeLocal = deps.parseTimeLocal;
|
|
const readArrayField = deps.readArrayField;
|
|
const getAuditUserId = deps.getAuditUserId;
|
|
const redirectAfterSave = deps.redirectAfterSave;
|
|
const notifyPlayerScreens = deps.notifyPlayerScreens;
|
|
const broadcastDashboardState = deps.broadcastDashboardState;
|
|
const getScreenDeleteBlockMessage = deps.getScreenDeleteBlockMessage;
|
|
const getScreenConnections = deps.getScreenConnections;
|
|
const getPlaylistDeleteBlockMessage = deps.getPlaylistDeleteBlockMessage;
|
|
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
|
const PLAYER_PUBLIC_BASE_URL = deps.playerPublicBaseUrl;
|
|
const requirePermission = deps.requirePermission;
|
|
|
|
async function fetchAllScreenSlugs() {
|
|
const [rows] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
|
|
return (rows || [])
|
|
.map(function (row) {
|
|
return String(row && row.slug ? row.slug : '').trim();
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
app.post('/commands', requirePermission('dashboard.allow'), async function (req, res, next) {
|
|
try {
|
|
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
|
|
const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout')
|
|
? req.body.blackout
|
|
: req.query.blackout;
|
|
|
|
if (!command) {
|
|
return res.status(400).json({ error: 'Command is required' });
|
|
}
|
|
|
|
if (command !== 'reload' && command !== 'blackout') {
|
|
return res.status(400).json({ error: 'Unsupported command' });
|
|
}
|
|
|
|
const slugs = await fetchAllScreenSlugs();
|
|
if (!slugs.length) {
|
|
await broadcastDashboardState();
|
|
return res.json({ ok: true, command: command, sent: 0 });
|
|
}
|
|
|
|
const payload = command === 'blackout'
|
|
? {
|
|
command: 'blackout',
|
|
blackout: blackoutValue === true || blackoutValue === 'true' || blackoutValue === '1' ? true : false
|
|
}
|
|
: 'reload';
|
|
|
|
const sentCount = await notifyPlayerScreens(slugs, payload);
|
|
await broadcastDashboardState();
|
|
|
|
return res.json({
|
|
ok: true,
|
|
command: command,
|
|
sent: sentCount,
|
|
blackout: command === 'blackout' ? Boolean(payload.blackout) : undefined
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/playlists', requirePermission('playlists.create'), async function (req, res, next) {
|
|
try {
|
|
const name = String(req.body.name || '').trim();
|
|
const fadeBetweenSlides = req.body.fade_between_slides ? 1 : 0;
|
|
if (!name) {
|
|
return res.status(400).send('Playlist name is required.');
|
|
}
|
|
if (await common.fetchDuplicateName(pool, 'playlists', name)) {
|
|
return res.status(400).send('A playlist with that name already exists.');
|
|
}
|
|
const actorId = getAuditUserId(req);
|
|
const [result] = await pool.query('INSERT INTO playlists (name, fade_between_slides, created_by, modified_by) VALUES (?, ?, ?, ?)', [name, fadeBetweenSlides, actorId, actorId]);
|
|
redirectAfterSave(req, res, '/playlists?edit=' + result.insertId, {
|
|
closeUrl: '/playlists',
|
|
newUrl: '/playlists/new',
|
|
message: 'Playlist created.'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/playlists/:id', requirePermission('playlists.update'), async function (req, res, next) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const name = String(req.body.name || '').trim();
|
|
const fadeBetweenSlides = req.body.fade_between_slides ? 1 : 0;
|
|
if (!name) {
|
|
return res.status(400).send('Playlist name is required.');
|
|
}
|
|
const playlist = await common.fetchPlaylistById(connection, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
if (await common.fetchDuplicateName(pool, 'playlists', name, playlist.id)) {
|
|
return res.status(400).send('A playlist with that name already exists.');
|
|
}
|
|
|
|
const affectedScreens = await fetchScreensByPlaylistId(connection, playlist.id);
|
|
|
|
const slideIds = readArrayField(req.body, ['slide_id[]', 'slide_id']);
|
|
const durations = readArrayField(req.body, ['duration_seconds[]', 'duration_seconds']);
|
|
const scheduleModes = readArrayField(req.body, ['schedule_mode[]', 'schedule_mode']);
|
|
const scheduleStartDateTimes = readArrayField(req.body, ['schedule_start_datetime[]', 'schedule_start_datetime']);
|
|
const scheduleEndDateTimes = readArrayField(req.body, ['schedule_end_datetime[]', 'schedule_end_datetime']);
|
|
const scheduleStartTimes = readArrayField(req.body, ['schedule_start_time[]', 'schedule_start_time']);
|
|
const scheduleEndTimes = readArrayField(req.body, ['schedule_end_time[]', 'schedule_end_time']);
|
|
const scheduleDaysJsonValues = readArrayField(req.body, ['schedule_days_json[]', 'schedule_days_json']);
|
|
|
|
if (durations.length && durations.length !== slideIds.length) {
|
|
return res.status(400).send('Playlist slide data is invalid.');
|
|
}
|
|
if (scheduleModes.length && scheduleModes.length !== slideIds.length) {
|
|
return res.status(400).send('Playlist schedule data is invalid.');
|
|
}
|
|
|
|
const normalizedSlides = [];
|
|
const seenSlideIds = new Set();
|
|
for (let i = 0; i < slideIds.length; i += 1) {
|
|
const slideId = Number(slideIds[i]);
|
|
if (!Number.isInteger(slideId) || slideId <= 0) {
|
|
return res.status(400).send('Invalid slide selection.');
|
|
}
|
|
if (seenSlideIds.has(slideId)) {
|
|
return res.status(400).send('A slide can only be added to a playlist once.');
|
|
}
|
|
seenSlideIds.add(slideId);
|
|
|
|
const durationRaw = Number(durations[i]);
|
|
const durationSeconds = Number.isFinite(durationRaw) ? Math.max(1, Math.trunc(durationRaw)) : 10;
|
|
const scheduleMode = normalizeScheduleMode(scheduleModes[i]);
|
|
|
|
let scheduleStartDatetime = null;
|
|
let scheduleEndDatetime = null;
|
|
let scheduleStartTime = null;
|
|
let scheduleEndTime = null;
|
|
let scheduleDaysJson = null;
|
|
|
|
if (scheduleMode === 'dates') {
|
|
scheduleStartDatetime = parseDateTimeLocal(scheduleStartDateTimes[i]);
|
|
scheduleEndDatetime = parseDateTimeLocal(scheduleEndDateTimes[i]);
|
|
if (!scheduleStartDatetime || !scheduleEndDatetime) {
|
|
return res.status(400).send('Start and end datetimes are required for date scheduling.');
|
|
}
|
|
if (scheduleEndDatetime < scheduleStartDatetime) {
|
|
return res.status(400).send('End datetime must be after start datetime.');
|
|
}
|
|
} else if (scheduleMode === 'times') {
|
|
scheduleStartTime = parseTimeLocal(scheduleStartTimes[i]);
|
|
scheduleEndTime = parseTimeLocal(scheduleEndTimes[i]);
|
|
if (!scheduleStartTime || !scheduleEndTime) {
|
|
return res.status(400).send('Start and end times are required for time scheduling.');
|
|
}
|
|
|
|
let scheduleDays = [];
|
|
try {
|
|
const parsedDays = JSON.parse(String(scheduleDaysJsonValues[i] || '[]'));
|
|
scheduleDays = Array.isArray(parsedDays) ? parsedDays : [];
|
|
} catch (_error) {
|
|
scheduleDays = [];
|
|
}
|
|
scheduleDays = scheduleDays
|
|
.map(function (value) { return Number(value); })
|
|
.filter(function (value) { return Number.isInteger(value) && value >= 0 && value <= 6; });
|
|
if (!scheduleDays.length) {
|
|
return res.status(400).send('Select at least one day for time scheduling.');
|
|
}
|
|
scheduleDaysJson = JSON.stringify(Array.from(new Set(scheduleDays)).sort());
|
|
}
|
|
|
|
normalizedSlides.push({
|
|
slideId,
|
|
position: i,
|
|
durationSeconds,
|
|
scheduleMode,
|
|
scheduleStartDatetime,
|
|
scheduleEndDatetime,
|
|
scheduleStartTime,
|
|
scheduleEndTime,
|
|
scheduleDaysJson
|
|
});
|
|
}
|
|
|
|
if (normalizedSlides.length) {
|
|
const [slides] = await connection.query(
|
|
`SELECT sl.id, cs.width AS canvas_width, cs.height AS canvas_height
|
|
FROM slides sl
|
|
LEFT JOIN slide_templates st ON st.id = sl.template_id
|
|
LEFT JOIN canvas_sizes cs ON cs.id = st.canvas_size_id
|
|
WHERE sl.id IN (?)`,
|
|
[normalizedSlides.map(function (item) { return item.slideId; })]
|
|
);
|
|
if (slides.length !== normalizedSlides.length) {
|
|
return res.status(400).send('One or more selected slides no longer exist.');
|
|
}
|
|
const signatures = Array.from(new Set(
|
|
slides
|
|
.map(function (slide) { return getCanvasSignature(slide.canvas_width, slide.canvas_height); })
|
|
.filter(Boolean)
|
|
));
|
|
if (signatures.length > 1) {
|
|
return res.status(400).send('All playlist slides must share the same canvas size.');
|
|
}
|
|
}
|
|
|
|
const actorId = getAuditUserId(req);
|
|
|
|
await connection.beginTransaction();
|
|
await connection.query('UPDATE playlists SET name = ?, fade_between_slides = ?, modified_by = ? WHERE id = ?', [name, fadeBetweenSlides, actorId, playlist.id]);
|
|
await connection.query('DELETE FROM playlist_slides WHERE playlist_id = ?', [playlist.id]);
|
|
for (let i = 0; i < normalizedSlides.length; i += 1) {
|
|
const item = normalizedSlides[i];
|
|
await connection.query(
|
|
'INSERT INTO playlist_slides (playlist_id, slide_id, position, duration_seconds, schedule_mode, schedule_start_datetime, schedule_end_datetime, schedule_start_time, schedule_end_time, schedule_days_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[
|
|
playlist.id,
|
|
item.slideId,
|
|
item.position,
|
|
item.durationSeconds,
|
|
item.scheduleMode,
|
|
item.scheduleStartDatetime,
|
|
item.scheduleEndDatetime,
|
|
item.scheduleStartTime,
|
|
item.scheduleEndTime,
|
|
item.scheduleDaysJson,
|
|
actorId,
|
|
actorId
|
|
]
|
|
);
|
|
}
|
|
await connection.commit();
|
|
await notifyPlayerScreens(affectedScreens, 'refresh');
|
|
await broadcastDashboardState();
|
|
redirectAfterSave(req, res, '/playlists?edit=' + playlist.id, {
|
|
closeUrl: '/playlists',
|
|
newUrl: '/playlists/new',
|
|
message: 'Playlist updated.'
|
|
});
|
|
} catch (error) {
|
|
try {
|
|
await connection.rollback();
|
|
} catch (_rollbackError) {
|
|
// Ignore rollback failures and surface the original error.
|
|
}
|
|
next(error);
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
app.post('/playlists/:id/delete', requirePermission('playlists.delete'), async function (req, res, next) {
|
|
try {
|
|
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
const blockMessage = await getPlaylistDeleteBlockMessage(pool, playlist);
|
|
if (blockMessage) {
|
|
return res.redirect('/playlists?message=' + encodeURIComponent(blockMessage));
|
|
}
|
|
await pool.query('DELETE FROM playlists WHERE id = ?', [playlist.id]);
|
|
res.redirect('/playlists?message=' + encodeURIComponent('Playlist deleted.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/playlists/:id/slides', requirePermission('playlists.update'), async function (req, res, next) {
|
|
try {
|
|
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
const slideId = Number(req.body.slide_id);
|
|
if (!slideId) {
|
|
return res.status(400).send('Slide is required.');
|
|
}
|
|
const playlistCanvasSignature = await fetchPlaylistCanvasSignature(pool, playlist.id);
|
|
if (playlistCanvasSignature === 'mismatch') {
|
|
return res.status(400).send('This playlist already contains slides with different canvas sizes.');
|
|
}
|
|
const slide = await common.fetchSlideById(pool, slideId);
|
|
if (!slide) {
|
|
return res.status(404).send('Slide not found');
|
|
}
|
|
const slideCanvasSignature = getCanvasSignature(slide.canvas_width, slide.canvas_height);
|
|
if (playlistCanvasSignature && slideCanvasSignature !== playlistCanvasSignature) {
|
|
return res.status(400).send('The slide canvas size must match the existing playlist items.');
|
|
}
|
|
const durationSeconds = Math.max(1, Number(req.body.duration_seconds || 10));
|
|
const [positionRows] = await pool.query('SELECT COALESCE(MAX(position), -1) AS max_position FROM playlist_slides WHERE playlist_id = ?', [playlist.id]);
|
|
const nextPosition = Number(positionRows[0].max_position) + 1;
|
|
const actorId = getAuditUserId(req);
|
|
await pool.query('INSERT INTO playlist_slides (playlist_id, slide_id, position, duration_seconds, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?)', [playlist.id, slideId, nextPosition, durationSeconds, actorId, actorId]);
|
|
await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh');
|
|
await broadcastDashboardState();
|
|
res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide added to playlist.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/playlists/:id/slides/:playlistSlideId', requirePermission('playlists.update'), async function (req, res, next) {
|
|
try {
|
|
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
const playlistSlideId = Number(req.params.playlistSlideId);
|
|
const durationSeconds = Math.max(1, Number(req.body.duration_seconds || 10));
|
|
const actorId = getAuditUserId(req);
|
|
const [result] = await pool.query(
|
|
'UPDATE playlist_slides SET duration_seconds = ?, modified_by = ? WHERE id = ? AND playlist_id = ?',
|
|
[durationSeconds, actorId, playlistSlideId, playlist.id]
|
|
);
|
|
if (!result.affectedRows) {
|
|
return res.status(404).send('Playlist slide not found');
|
|
}
|
|
await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh');
|
|
await broadcastDashboardState();
|
|
res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide duration updated.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/playlists/:id/slides/:playlistSlideId/move', requirePermission('playlists.update'), async function (req, res, next) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const playlist = await common.fetchPlaylistById(connection, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
const playlistSlideId = Number(req.params.playlistSlideId);
|
|
const direction = String(req.body.direction || '').toLowerCase();
|
|
if (direction !== 'up' && direction !== 'down') {
|
|
return res.status(400).send('Invalid move direction.');
|
|
}
|
|
|
|
await connection.beginTransaction();
|
|
const orderedSlides = await fetchOrderedPlaylistSlides(connection, playlist.id);
|
|
const currentIndex = orderedSlides.findIndex(function (item) {
|
|
return Number(item.id) === playlistSlideId;
|
|
});
|
|
if (currentIndex === -1) {
|
|
await connection.rollback();
|
|
return res.status(404).send('Playlist slide not found');
|
|
}
|
|
|
|
const swapIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
|
if (swapIndex < 0 || swapIndex >= orderedSlides.length) {
|
|
await connection.rollback();
|
|
return res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide is already at the ' + (direction === 'up' ? 'top' : 'bottom') + '.'));
|
|
}
|
|
|
|
const currentSlide = orderedSlides[currentIndex];
|
|
const swapSlide = orderedSlides[swapIndex];
|
|
const actorId = getAuditUserId(req);
|
|
|
|
await connection.query('UPDATE playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [swapSlide.position, actorId, currentSlide.id, playlist.id]);
|
|
await connection.query('UPDATE playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [currentSlide.position, actorId, swapSlide.id, playlist.id]);
|
|
await connection.commit();
|
|
await notifyPlayerScreens(await fetchScreensByPlaylistId(connection, playlist.id), 'refresh');
|
|
await broadcastDashboardState();
|
|
res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide order updated.'));
|
|
} catch (error) {
|
|
await connection.rollback();
|
|
next(error);
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
app.get('/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.update'), async function (req, res, next) {
|
|
try {
|
|
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
const data = await common.fetchAdminData(pool);
|
|
let playlistSlide = (data.playlistSlides || []).find(function (item) {
|
|
return item.id === Number(req.params.playlistSlideId) && item.playlist_id === playlist.id;
|
|
});
|
|
if (!playlistSlide && Number(req.params.playlistSlideId) !== 0) {
|
|
return res.status(404).send('Playlist slide not found');
|
|
}
|
|
const scheduleMode = typeof req.query.schedule_mode === 'string' && req.query.schedule_mode ? String(req.query.schedule_mode) : String(playlistSlide && playlistSlide.schedule_mode || 'always');
|
|
const scheduleStartDatetime = typeof req.query.schedule_start_datetime === 'string' ? String(req.query.schedule_start_datetime) : (playlistSlide && playlistSlide.schedule_start_datetime) || null;
|
|
const scheduleEndDatetime = typeof req.query.schedule_end_datetime === 'string' ? String(req.query.schedule_end_datetime) : (playlistSlide && playlistSlide.schedule_end_datetime) || null;
|
|
const scheduleStartTime = typeof req.query.schedule_start_time === 'string' ? String(req.query.schedule_start_time) : (playlistSlide && playlistSlide.schedule_start_time) || null;
|
|
const scheduleEndTime = typeof req.query.schedule_end_time === 'string' ? String(req.query.schedule_end_time) : (playlistSlide && playlistSlide.schedule_end_time) || null;
|
|
const scheduleDaysJson = typeof req.query.schedule_days_json === 'string' ? String(req.query.schedule_days_json) : (playlistSlide && playlistSlide.schedule_days_json) || '[]';
|
|
const scheduleDays = common.parseJsonSafe(scheduleDaysJson) || [];
|
|
|
|
playlistSlide = Object.assign({}, playlistSlide || {}, {
|
|
id: playlistSlide ? playlistSlide.id : 0,
|
|
schedule_mode: scheduleMode,
|
|
schedule_days_json: scheduleDaysJson,
|
|
schedule_days: scheduleDays,
|
|
schedule_start_datetime: scheduleStartDatetime,
|
|
schedule_end_datetime: scheduleEndDatetime,
|
|
schedule_start_time: scheduleStartTime,
|
|
schedule_end_time: scheduleEndTime
|
|
});
|
|
return res.send(pages.renderPlaylistSlideConfigPage(playlist, playlistSlide, req.query.message ? String(req.query.message) : '', req.query.row_key ? String(req.query.row_key) : '', req.currentUser));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.update'), async function (req, res, next) {
|
|
try {
|
|
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
const rowKey = String(req.body.row_key || '').trim();
|
|
if (rowKey) {
|
|
return res.status(400).send('Schedule changes from the playlist editor are staged until you click Save changes.');
|
|
}
|
|
const playlistSlideId = Number(req.params.playlistSlideId);
|
|
let scheduleMode = normalizeScheduleMode(req.body.schedule_mode);
|
|
let scheduleStartDatetime = null;
|
|
let scheduleEndDatetime = null;
|
|
let scheduleStartTime = null;
|
|
let scheduleEndTime = null;
|
|
let scheduleDaysJson = null;
|
|
|
|
const hasDateRange = Boolean(req.body.schedule_start_datetime && req.body.schedule_end_datetime);
|
|
const hasTimeRange = Boolean(req.body.schedule_start_time && req.body.schedule_end_time);
|
|
const hasSelectedDays = Boolean(readArrayField(req.body, ['schedule_days', 'schedule_days[]']).length);
|
|
|
|
if (scheduleMode === 'dates' && !hasDateRange) {
|
|
scheduleMode = 'always';
|
|
} else if (scheduleMode === 'times' && (!hasTimeRange || !hasSelectedDays)) {
|
|
scheduleMode = 'always';
|
|
}
|
|
|
|
if (scheduleMode === 'dates') {
|
|
scheduleStartDatetime = parseDateTimeLocal(req.body.schedule_start_datetime);
|
|
scheduleEndDatetime = parseDateTimeLocal(req.body.schedule_end_datetime);
|
|
if (!scheduleStartDatetime || !scheduleEndDatetime) {
|
|
scheduleMode = 'always';
|
|
scheduleStartDatetime = null;
|
|
scheduleEndDatetime = null;
|
|
} else if (scheduleEndDatetime < scheduleStartDatetime) {
|
|
return res.status(400).send('End datetime must be after start datetime.');
|
|
}
|
|
} else if (scheduleMode === 'times') {
|
|
scheduleStartTime = parseTimeLocal(req.body.schedule_start_time);
|
|
scheduleEndTime = parseTimeLocal(req.body.schedule_end_time);
|
|
const scheduleDays = readArrayField(req.body, ['schedule_days', 'schedule_days[]']).map(function (value) {
|
|
return Number(value);
|
|
}).filter(function (value) {
|
|
return Number.isInteger(value) && value >= 0 && value <= 6;
|
|
});
|
|
if (!scheduleStartTime || !scheduleEndTime) {
|
|
scheduleMode = 'always';
|
|
scheduleStartTime = null;
|
|
scheduleEndTime = null;
|
|
scheduleDaysJson = null;
|
|
} else if (!scheduleDays.length) {
|
|
scheduleMode = 'always';
|
|
scheduleStartTime = null;
|
|
scheduleEndTime = null;
|
|
scheduleDaysJson = null;
|
|
} else {
|
|
if (scheduleEndTime < scheduleStartTime) {
|
|
return res.status(400).send('End time must be after start time.');
|
|
}
|
|
scheduleDaysJson = JSON.stringify(Array.from(new Set(scheduleDays)).sort());
|
|
}
|
|
}
|
|
|
|
const actorId = getAuditUserId(req);
|
|
const [result] = await pool.query(
|
|
'UPDATE playlist_slides SET schedule_mode = ?, schedule_start_datetime = ?, schedule_end_datetime = ?, schedule_start_time = ?, schedule_end_time = ?, schedule_days_json = ?, modified_by = ? WHERE id = ? AND playlist_id = ?',
|
|
[scheduleMode, scheduleStartDatetime, scheduleEndDatetime, scheduleStartTime, scheduleEndTime, scheduleDaysJson, actorId, playlistSlideId, playlist.id]
|
|
);
|
|
if (!result.affectedRows) {
|
|
return res.status(404).send('Playlist slide not found');
|
|
}
|
|
await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh');
|
|
await broadcastDashboardState();
|
|
res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide timings updated.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/playlists/:id/slides/:playlistSlideId/delete', requirePermission('playlists.update'), async function (req, res, next) {
|
|
try {
|
|
const playlist = await common.fetchPlaylistById(pool, Number(req.params.id));
|
|
if (!playlist) {
|
|
return res.status(404).send('Playlist not found');
|
|
}
|
|
await pool.query('DELETE FROM playlist_slides WHERE id = ? AND playlist_id = ?', [Number(req.params.playlistSlideId), playlist.id]);
|
|
await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh');
|
|
await broadcastDashboardState();
|
|
res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide removed.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/screens/new', requirePermission('screens.create'), async function (req, res, next) {
|
|
try {
|
|
const data = await common.fetchScreenEditData(pool);
|
|
res.send(pages.renderScreenFormPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/screens', requirePermission('screens.create'), async function (req, res, next) {
|
|
try {
|
|
const name = String(req.body.name || '').trim();
|
|
if (!name) {
|
|
return res.status(400).send('Screen name is required.');
|
|
}
|
|
if (await common.fetchDuplicateName(pool, 'screens', name)) {
|
|
return res.status(400).send('A screen with that name already exists.');
|
|
}
|
|
const slugInput = String(req.body.slug || '').trim();
|
|
const playlistId = req.body.playlist_id ? Number(req.body.playlist_id) : null;
|
|
const slug = await common.uniqueScreenSlug(pool, common.slugify(slugInput || name));
|
|
const actorId = getAuditUserId(req);
|
|
const [result] = await pool.query('INSERT INTO screens (name, slug, playlist_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, slug, playlistId, actorId, actorId]);
|
|
redirectAfterSave(req, res, '/screens?edit=' + result.insertId, {
|
|
closeUrl: '/screens',
|
|
newUrl: '/screens/new',
|
|
message: 'Screen created.'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/screens/:id', requirePermission('screens.update'), async function (req, res, next) {
|
|
try {
|
|
const name = String(req.body.name || '').trim();
|
|
if (!name) {
|
|
return res.status(400).send('Screen name is required.');
|
|
}
|
|
const screen = await common.fetchScreenById(pool, Number(req.params.id));
|
|
if (!screen) {
|
|
return res.status(404).send('Screen not found');
|
|
}
|
|
if (await common.fetchDuplicateName(pool, 'screens', name, screen.id)) {
|
|
return res.status(400).send('A screen with that name already exists.');
|
|
}
|
|
const slugInput = String(req.body.slug || '').trim();
|
|
const playlistId = req.body.playlist_id ? Number(req.body.playlist_id) : null;
|
|
const previousPlaylistId = screen.playlist_id;
|
|
const slug = await common.uniqueScreenSlug(pool, common.slugify(slugInput || name), screen.id);
|
|
const previousSlug = String(screen.slug || '').trim();
|
|
await pool.query('UPDATE screens SET name = ?, slug = ?, playlist_id = ?, modified_by = ? WHERE id = ?', [name, slug, playlistId, getAuditUserId(req), screen.id]);
|
|
if (previousPlaylistId !== playlistId && previousSlug) {
|
|
await notifyPlayerScreens([previousSlug], 'refresh');
|
|
}
|
|
if (previousSlug && previousSlug !== slug) {
|
|
await forwardPlayerCommand(previousSlug, {
|
|
command: 'redirect',
|
|
url: `${PLAYER_PUBLIC_BASE_URL}/screen/${encodeURIComponent(slug)}`
|
|
});
|
|
}
|
|
redirectAfterSave(req, res, '/screens?edit=' + screen.id, {
|
|
closeUrl: '/screens',
|
|
newUrl: '/screens/new',
|
|
message: 'Screen updated.'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.post('/screens/:id/delete', requirePermission('screens.delete'), async function (req, res, next) {
|
|
try {
|
|
const screen = await common.fetchScreenById(pool, Number(req.params.id));
|
|
if (!screen) {
|
|
return res.status(404).send('Screen not found');
|
|
}
|
|
const blockMessage = await getScreenDeleteBlockMessage(pool, screen, getScreenConnections);
|
|
if (blockMessage) {
|
|
return res.redirect('/screens?message=' + encodeURIComponent(blockMessage));
|
|
}
|
|
await pool.query('DELETE FROM screens WHERE id = ?', [screen.id]);
|
|
res.redirect('/screens?message=' + encodeURIComponent('Screen deleted.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
};
|