563 lines
31 KiB
Twig
563 lines
31 KiB
Twig
{#
|
|
/**
|
|
* Copyright (C) 2023 Xibo Signage Ltd
|
|
*
|
|
* Xibo - Digital Signage - http://www.xibo.org.uk
|
|
*
|
|
* This file is part of Xibo.
|
|
*
|
|
* Xibo is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
*
|
|
* Xibo is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#}
|
|
{% extends "authed.twig" %}
|
|
{% import "inline.twig" as inline %}
|
|
{% import "forms.twig" as forms %}
|
|
|
|
{% block title %}{{ "Schedule"|trans }} | {% endblock %}
|
|
|
|
{% block actionMenu %}{% endblock %}
|
|
|
|
{% block pageContent %}
|
|
<div class="ots-static-page ots-displays-page">
|
|
<div class="page-header ots-page-header">
|
|
<h1>{% trans "Schedule" %}</h1>
|
|
<p class="text-muted">{% trans "Schedule content to your displays." %}</p>
|
|
</div>
|
|
|
|
<div class="widget content-card ots-displays-card">
|
|
<div class="widget-body ots-displays-body">
|
|
<div class="XiboGrid" id="{{ random() }}" data-grid-name="scheduleGridView">
|
|
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
|
<div class="ots-filter-header">
|
|
<h3 class="ots-filter-title">{% trans "Filter Schedule" %}</h3>
|
|
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
|
<i class="fa fa-chevron-down"></i>
|
|
</button>
|
|
</div>
|
|
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
|
<div class="FilterDiv card-body" id="schedule-filter">
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab" aria-selected="true"><span>{% trans "General" %}</span></a></li>
|
|
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab" aria-selected="false"><span>{% trans "Advanced" %}</span></a></li>
|
|
</ul>
|
|
<form class="form-inline">
|
|
<div class="tab-content">
|
|
<div class="tab-pane active" id="general-filter" role="tabpanel">
|
|
{% set title %}{% trans "Range" %}{% endset %}
|
|
{% set range %}{% trans "Custom" %}{% endset %}
|
|
{% set day %}{% trans "Day" %}{% endset %}
|
|
{% set week %}{% trans "Week" %}{% endset %}
|
|
{% set month %}{% trans "Month" %}{% endset %}
|
|
{% set year %}{% trans "Year" %}{% endset %}
|
|
{% set options = [
|
|
{ name: "custom", range: range },
|
|
{ name: "day", range: day },
|
|
{ name: "week", range: week },
|
|
{ name: "month", range: month },
|
|
{ name: "year", range: year },
|
|
] %}
|
|
{{ inline.dropdown("range", "single", title, "month", options, "name", "range", "", "date-range-input") }}
|
|
|
|
{% set title %}{% trans 'From Date' %}{% endset %}
|
|
{{ inline.dateTime("fromDt", title, "", "", "custom-date-range d-none", "", "") }}
|
|
|
|
{% set title %}{% trans 'To Date' %}{% endset %}
|
|
{{ inline.dateTime("toDt", title, "", "", "custom-date-range d-none", "", "") }}
|
|
|
|
{% set title %}{% trans "Date Controls" %}{% endset %}
|
|
<div class="form-group mr-1 mb-1 controls-date-range">
|
|
<div class="control-label mr-1" title=""
|
|
accesskey="">{{ title }}</div>
|
|
<div class="controls-date-inputs">
|
|
<div class="inputgroup date" id="dateInput">
|
|
<span class="btn btn-outline-primary date-open-button" role="button">
|
|
<i class="fa fa-calendar"></i>
|
|
</span>
|
|
<input type="text" class="form-control" id="dateInputLink" data-input style="display:none;"/>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-secondary" data-calendar-nav="prev"><span class="fa fa-caret-left"></span> {% trans "Prev" %}</button>
|
|
<button type="button" class="btn btn-outline-secondary" data-calendar-nav="today">{% trans "Today" %}</button>
|
|
<button type="button" class="btn btn-secondary" data-calendar-nav="next">{% trans "Next" %} <span class="fa fa-caret-right"></span></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% set title %}{% trans "Name" %}{% endset %}
|
|
{{ inline.inputNameGrid('name', title) }}
|
|
|
|
{% set title %}{% trans 'Event Type' %}{% endset %}
|
|
{{ inline.dropdown("eventTypeId", "single", title, "", [{eventTypeId: null, eventTypeName: "All"}]|merge(eventTypes), "eventTypeId", "eventTypeName") }}
|
|
|
|
{% set title %}{% trans "Layout / Campaign" %}{% endset %}
|
|
{% set helpText %}{% trans "Please select a Layout or Campaign for this Event to show" %}{% endset %}
|
|
|
|
<div class="form-group mr-1 mb-1">
|
|
<label class="control-label mr-1" for="campaignId" title=""
|
|
accesskey="">{{ title }}</label>
|
|
<select name="campaignId" id="campaignIdFilter" class="form-control"
|
|
data-search-url="{{ url_for("campaign.search") }}"
|
|
data-trans-campaigns="{% trans "Campaigns" %}"
|
|
data-trans-layouts="{% trans "Layouts" %}"
|
|
data-allow-clear="true"
|
|
data-width="100%"
|
|
title="{% trans "Layout / Campaign" %}"
|
|
data-placeholder="{% trans "Layout / Campaign" %}"
|
|
data-dropdownAutoWidth
|
|
>
|
|
</select>
|
|
</div>
|
|
|
|
{% set title %}{% trans "Displays" %}{% endset %}
|
|
<div class="form-group mr-1 mb-1 pagedSelect">
|
|
<label class="control-label mr-1" for="DisplayList" title=""
|
|
accesskey="">{{ title }}</label>
|
|
<select id="DisplayList" class="form-control" name="displaySpecificGroupIds[]"
|
|
data-width="100%"
|
|
data-placeholder="{% trans "Displays" %}"
|
|
data-search-url="{{ url_for("display.search") }}"
|
|
data-search-term="display"
|
|
data-id-property="displayGroupId"
|
|
data-text-property="display"
|
|
data-additional-property="displayGroupId"
|
|
data-allow-clear="true"
|
|
data-initial-key="displayGroupIds[]"
|
|
multiple>
|
|
</select>
|
|
</div>
|
|
|
|
{% set title %}{% trans "Display Groups" %}{% endset %}
|
|
<div class="form-group mr-2 mb-1 pagedSelect">
|
|
<label class="control-label mr-1" for="DisplayGroupList" title=""
|
|
accesskey="">{{ title }}</label>
|
|
<select id="DisplayGroupList" class="form-control" name="displayGroupIds[]"
|
|
data-width="100%"
|
|
data-placeholder="{% trans "Display Groups" %}"
|
|
data-search-url="{{ url_for("displayGroup.search") }}"
|
|
data-search-term="displayGroup"
|
|
data-id-property="displayGroupId"
|
|
data-text-property="displayGroup"
|
|
data-allow-clear="true"
|
|
data-initial-key="displayGroupIds[]"
|
|
multiple>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane" id="advanced-filter" role="tabpanel">
|
|
{% set label %}{% trans "Direct Schedule?" %}{% endset %}
|
|
{% set title %}{% trans "Show only events scheduled directly on selected Displays/Groups" %}{% endset %}
|
|
<div class="form-group ml-2 mr-3 mb-1">
|
|
<div class="form-check">
|
|
<input title="{{ title }}" class="form-check-input" type="checkbox" id="directSchedule" name="directSchedule">
|
|
<label class="form-check-label" title="{{ title }}" for="directSchedule" accesskey="">{{ label }}</label>
|
|
</div>
|
|
</div>
|
|
|
|
{% set title %}{% trans "Only show schedules which appear on all filtered displays/groups?" %}{% endset %}
|
|
{% set label %}{% trans "Shared Schedule?" %}{% endset %}
|
|
<div class="form-group ml-2 mr-3 mb-1">
|
|
<div class="form-check">
|
|
<input title="{{ title }}" class="form-check-input" type="checkbox" id="sharedSchedule" name="sharedSchedule">
|
|
<label class="form-check-label" title="{{ title }}" for="sharedSchedule" accesskey="">{{ label }}</label>
|
|
</div>
|
|
</div>
|
|
|
|
{% set title %}{% trans 'Geo Aware?' %}{% endset %}
|
|
{% set options = [
|
|
{ id: null, name: "Both"|trans },
|
|
{ id: 0, name: "No"|trans },
|
|
{ id: 1, name: "Yes"|trans }
|
|
] %}
|
|
{{ inline.dropdown("geoAware", "single", title, "both", options, "id", "name") }}
|
|
|
|
{% set title %}{% trans 'Recurring?' %}{% endset %}
|
|
{% set options = [
|
|
{ id: null, name: "Both" },
|
|
{ id: 0, name: "No"|trans },
|
|
{ id: 1, name: "Yes"|trans }
|
|
] %}
|
|
{{ inline.dropdown("recurring", "single", title, "both", options, "id", "name") }}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="XiboSchedule card content-card ots-table-card">
|
|
<div class="ots-table-toolbar">
|
|
{% if currentUser.featureEnabled("schedule.add") %}
|
|
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Scheduled event" %}" href="{{ url_for("schedule.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
|
{% endif %}
|
|
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
|
</div>
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs">
|
|
<li class="nav-item">
|
|
<a class="schedule-nav grid-nav nav-link active" id="grid-tab" href="#grid-view"
|
|
data-schedule-view="grid"
|
|
role="tab"
|
|
data-toggle="tab"><span>{% trans "Grid" %}</span></a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="schedule-nav calendar-nav nav-link" id="calendar-tab" href="#calendar-view"
|
|
data-schedule-view="calendar"
|
|
data-calendar-view="month"
|
|
role="tab"
|
|
data-toggle="tab"><span>{% trans "Calendar" %}</span></a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="xibo-calendar-header-container col-xl-12">
|
|
<div class="ots-calendar-nav">
|
|
<button type="button" class="ots-cal-arrow ots-cal-arrow-prev" id="ots-cal-prev" title="{% trans 'Previous' %}">
|
|
<i class="fa fa-chevron-left"></i>
|
|
</button>
|
|
<div class="xibo-calendar-header text-center">
|
|
<h1 class="page-header"></h1>
|
|
<div class="calendar-loading">
|
|
<span id="calendar-progress-table" class="fa fa-spin fa-cog"></span>
|
|
<span id="calendar-progress" class="fa fa-spin fa-cog"></span>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="ots-cal-arrow ots-cal-arrow-next" id="ots-cal-next" title="{% trans 'Next' %}">
|
|
<i class="fa fa-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-pane active" id="grid-view">
|
|
<div class="XiboData pt-3">
|
|
<table id="schedule-grid" class="table table-striped w-100"
|
|
data-state-preference-name="scheduleGrid">
|
|
<thead>
|
|
<tr>
|
|
<th>{% trans 'ID' %}</th>
|
|
<th></th>
|
|
<th>{% trans 'Event Type' %}</th>
|
|
<th>{% trans 'Name' %}</th>
|
|
<th>{% trans 'Start' %}</th>
|
|
<th>{% trans 'End' %}</th>
|
|
<th>{% trans 'Event' %}</th>
|
|
<th>{% trans 'Campaign ID' %}</th>
|
|
<th>{% trans 'Display Groups' %}</th>
|
|
<th>{% trans 'SoV' %}</th>
|
|
<th>{% trans 'Max Plays per Hour' %}</th>
|
|
<th>{% trans 'Geo Aware?' %}</th>
|
|
<th>{% trans 'Recurring?' %}</th>
|
|
<th>{% trans 'Recurrence Description' %}</th>
|
|
<th>{% trans 'Recurrence Type' %}</th>
|
|
<th>{% trans 'Recurrence Interval' %}</th>
|
|
<th>{% trans 'Recurrence Repeats On' %}</th>
|
|
<th>{% trans 'Recurrence End' %}</th>
|
|
<th>{% trans 'Priority?' %}</th>
|
|
<th>{% trans 'Criteria?' %}</th>
|
|
<th>{% trans 'Created On' %}</th>
|
|
<th>{% trans 'Updated On' %}</th>
|
|
<th>{% trans 'Modified By' %}</th>
|
|
<th class="rowMenu"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane" id="calendar-view">
|
|
{# ── 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="ots-legacy-calendar-stub"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-sm-12">
|
|
<div class="cal-legend">
|
|
<ul>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block javaScript %}
|
|
{# Initialise JS variables #}
|
|
<script type="text/javascript" nonce="{{ cspNonce }}">
|
|
{# JS variables #}
|
|
var scheduleRecurrenceDeleteUrl = "{{ url_for("schedule.recurrence.delete.form", {id:':id'}) }}";
|
|
var layoutPreviewUrl = "{{ theme.rootUri() }}preview/layout/preview/:id";
|
|
var scheduleSearchUrl = "{{ url_for("schedule.search") }}";
|
|
var userAgendaViewEnabled = "{{ currentUser.featureEnabled('schedule.agenda') }}";
|
|
|
|
{# Custom translations #}
|
|
var schedulePageTrans = {
|
|
always: "{% trans "Always" %}",
|
|
adjustTimesofTimer: "{% trans "Adjust the times of this timer. To add or remove a day, use the Display Profile." %}",
|
|
daysOfTheWeek: {
|
|
monday: "{% trans "Monday" %}",
|
|
tuesday: "{% trans "Tuesday" %}",
|
|
wednesday: "{% trans "Wednesday" %}",
|
|
thursday: "{% trans "Thursday" %}",
|
|
friday: "{% trans "Friday" %}",
|
|
saturday: "{% trans "Saturday" %}",
|
|
sunday: "{% trans "Sunday" %}",
|
|
},
|
|
};
|
|
</script>
|
|
|
|
{# 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() {
|
|
if (otsCalendar) { otsCalendar.prev(); }
|
|
});
|
|
$('#ots-cal-next').on('click', function() {
|
|
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 %}
|