pre-img swap

This commit is contained in:
Matt Batchelder
2026-03-23 21:09:27 -04:00
parent 87474b05a9
commit bbe8c1860c
395 changed files with 29643 additions and 712 deletions

View File

@@ -280,42 +280,38 @@
</div>
</div>
<div class="tab-pane" id="calendar-view">
<div class="row">
{# ── OTS FullCalendar container (replaces legacy CalendarContainer visually) ── #}
<div id="ots-fullcalendar"></div>
{# Legacy container kept hidden so Xibo bundle doesn't error.
NOTE: id="Calendar" is intentionally omitted to prevent the Xibo bundle
from initialising the legacy calendar and throwing _loadEvents errors.
The data-agenda-link is preserved on CalendarContainer for our event-click handler. #}
<div class="row d-none" id="ots-legacy-calendar-row">
<div id="CalendarContainer"
data-agenda-link="{{ url_for("schedule.events", {id: ':id'}) }}"
data-calendar-type="{{ settings.CALENDAR_TYPE }}" class="col-sm-12"
data-default-lat="{{ defaultLat }}"
data-default-long="{{ defaultLong }}">
<div class="calendar-view" id="Calendar"></div>
<div class="calendar-view" id="ots-legacy-calendar-stub"></div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="cal-legend">
<ul>
<li class="event-always"><span
class="fa fa-retweet"></span> {% trans "Always showing" %}</li>
<li class="event-info"><span
class="fa fa-desktop"></span> {% trans "Single Display" %}</li>
<li class="event-success"><span
class="fa fa-desktop"></span> {% trans "Multi Display" %}</li>
<li class="event-important"><span
class="fa fa-bullseye"></span> {% trans "Priority" %}</li>
<li class="event-special"><span
class="fa fa-repeat"></span> {% trans "Recurring" %}</li>
<li class="event-inverse"><span
class="fa fa-lock"></span> {% trans "View Only" %}</li>
<li class="event-command"><span
class="fa fa-wrench"></span> {% trans "Command" %}</li>
<li class="event-interrupt"><span
class="fa fa-hand-paper"></span> {% trans "Interrupt" %}</li>
<li class="event-geo-location"><span
class="fa fa-map-marker"></span> {% trans "Geo Location" %}</li>
<li class="event-action"><span
class="fa fa-paper-plane "></span> {% trans "Interactive Action" %}
</li>
<li class="event-sync"><span
class="fa fa-refresh"></span> {% trans "Synchronised" %}</li>
<li><span class="fa fa-retweet" style="color:#6366f1"></span> {% trans "Always showing" %}</li>
<li><span class="fa fa-desktop" style="color:#0ea5e9"></span> {% trans "Single Display" %}</li>
<li><span class="fa fa-desktop" style="color:#10b981"></span> {% trans "Multi Display" %}</li>
<li><span class="fa fa-bullseye" style="color:#ef4444"></span> {% trans "Priority" %}</li>
<li><span class="fa fa-repeat" style="color:#8b5cf6"></span> {% trans "Recurring" %}</li>
<li><span class="fa fa-lock" style="color:#64748b"></span> {% trans "View Only" %}</li>
<li><span class="fa fa-wrench" style="color:#f59e0b"></span> {% trans "Command" %}</li>
<li><span class="fa fa-hand-paper" style="color:#f97316"></span> {% trans "Interrupt" %}</li>
<li><span class="fa fa-map-marker" style="color:#14b8a6"></span> {% trans "Geo Location" %}</li>
<li><span class="fa fa-paper-plane" style="color:#ec4899"></span> {% trans "Interactive Action" %}</li>
<li><span class="fa fa-refresh" style="color:#06b6d4"></span> {% trans "Synchronised" %}</li>
</ul>
</div>
</div>
@@ -356,14 +352,211 @@
{# Add page source code bundle ( JS ) #}
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/pages/schedule-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
{# ── FullCalendar v6 (inlined to bypass MIME issues with /custom/ paths) ── #}
<script nonce="{{ cspNonce }}">
{% include "fullcalendar-lib.twig" %}
</script>
{# ── OTS FullCalendar integration ── #}
<script nonce="{{ cspNonce }}">
$(function() {
// OTS calendar nav arrows drive FullCalendar directly (do NOT proxy to
// Xibo's data-calendar-nav buttons — those trigger the legacy calendar
// and cause _loadEvents / options.events errors).
$('#ots-cal-prev').on('click', function() {
$('button[data-calendar-nav="prev"]').trigger('click');
if (otsCalendar) { otsCalendar.prev(); }
});
$('#ots-cal-next').on('click', function() {
$('button[data-calendar-nav="next"]').trigger('click');
if (otsCalendar) { otsCalendar.next(); }
});
// ── FullCalendar initialisation ──
var otsCalendar = null;
var calendarTabActive = false;
// Map Xibo event data to FullCalendar event object
function mapEvent(item) {
var title = item.schedule || item.campaign || item.command || item.layout || 'Event #' + item.eventId;
var cls = 'fc-event-single';
// Priority flag overrides everything
if (item.isPriority && parseInt(item.isPriority, 10) > 0) {
cls = 'fc-event-priority';
}
// Command events
else if (parseInt(item.eventTypeId, 10) === 2) {
cls = 'fc-event-command';
}
// Interrupt events
else if (parseInt(item.eventTypeId, 10) === 4) {
cls = 'fc-event-interrupt';
}
// Action events
else if (parseInt(item.eventTypeId, 10) === 6) {
cls = 'fc-event-action';
}
// "Always" daypart
else if (item.isAlways && parseInt(item.isAlways, 10) === 1) {
cls = 'fc-event-always';
}
// Geo-aware
else if (item.isGeoAware && parseInt(item.isGeoAware, 10) === 1) {
cls = 'fc-event-geo';
}
// Recurring
else if (item.recurrenceType && item.recurrenceType !== '' && item.recurrenceType !== 'null') {
cls = 'fc-event-recurring';
}
// Multi-display vs single
else if (item.displayGroups && item.displayGroups.length > 1) {
cls = 'fc-event-multi';
}
// Sync events
if (item.syncGroupId && parseInt(item.syncGroupId, 10) > 0) {
cls = 'fc-event-sync';
}
return {
id: item.eventId,
title: title,
start: item.fromDt,
end: item.toDt,
allDay: (item.isAlways && parseInt(item.isAlways, 10) === 1),
className: cls,
extendedProps: item
};
}
// Fetch events from Xibo schedule.search API
function fetchEvents(info, successCallback, failureCallback) {
var filterData = {
fromDt: info.startStr,
toDt: info.endStr
};
// Pull in active filter values
var $filter = $('#schedule-filter');
if ($filter.length) {
$filter.find(':input[name]').each(function() {
var $el = $(this);
var name = $el.attr('name');
var val = $el.val();
if (val && val !== '' && name !== 'fromDt' && name !== 'toDt') {
filterData[name] = val;
}
});
}
$.ajax({
url: scheduleSearchUrl,
type: 'GET',
dataType: 'json',
data: filterData,
success: function(response) {
var events = [];
var rows = response.data || response || [];
for (var i = 0; i < rows.length; i++) {
events.push(mapEvent(rows[i]));
}
successCallback(events);
},
error: function(xhr) {
console.warn('OTS: FullCalendar event fetch failed', xhr.status);
failureCallback(xhr);
}
});
}
// Handle event click → open Xibo edit form
function handleEventClick(info) {
var ev = info.event;
var props = ev.extendedProps || {};
if (props.eventId) {
var url = $('#CalendarContainer').data('agenda-link');
if (url) {
url = url.replace(':id', props.eventId);
XiboFormRender(url);
}
}
}
// Initialise FullCalendar on first Calendar-tab show
function initFullCalendar() {
if (otsCalendar) return;
var el = document.getElementById('ots-fullcalendar');
if (!el || typeof FullCalendar === 'undefined') return;
otsCalendar = new FullCalendar.Calendar(el, {
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
buttonText: {
today: '{{ "Today"|trans }}',
month: '{{ "Month"|trans }}',
week: '{{ "Week"|trans }}',
day: '{{ "Day"|trans }}',
list: '{{ "List"|trans }}'
},
firstDay: 1,
nowIndicator: true,
navLinks: true,
editable: false,
selectable: false,
eventDisplay: 'block',
dayMaxEvents: 4,
height: 'auto',
events: fetchEvents,
eventClick: handleEventClick,
loading: function(isLoading) {
$('#calendar-progress').toggle(isLoading);
}
});
otsCalendar.render();
}
// Init when Calendar tab is shown
$('a[data-toggle="tab"][href="#calendar-view"]').on('shown.bs.tab', function() {
calendarTabActive = true;
initFullCalendar();
// Hide the Xibo calendar header (we use FC's built-in toolbar)
$('.xibo-calendar-header-container').hide();
});
// Re-show Xibo header when switching back to Grid; suspend FC interception
$('a[data-toggle="tab"][href="#grid-view"]').on('shown.bs.tab', function() {
$('.xibo-calendar-header-container').show();
calendarTabActive = false;
});
// Intercept Xibo's data-calendar-nav buttons when calendar tab is active
// and drive FullCalendar directly, preventing the legacy calendar from being invoked.
$(document).on('click', '[data-calendar-nav]', function(e) {
if (!calendarTabActive || !otsCalendar) return; // Grid tab active — let Xibo handle it
e.stopImmediatePropagation();
var nav = $(this).data('calendar-nav');
if (nav === 'prev') { otsCalendar.prev(); }
else if (nav === 'next') { otsCalendar.next(); }
else if (nav === 'today') { otsCalendar.today(); }
});
// Refetch events when filter changes
$('#schedule-filter').on('change', ':input', function() {
if (otsCalendar) {
otsCalendar.refetchEvents();
}
});
// If Calendar tab is active by default (URL hash), init immediately
if (window.location.hash === '#calendar-view' || $('#calendar-tab').hasClass('active')) {
calendarTabActive = true;
setTimeout(initFullCalendar, 100);
}
});
</script>
{% endblock %}