aboutsummaryrefslogtreecommitdiff
path: root/resources/assets/javascripts/lib
diff options
context:
space:
mode:
authorMoritz Strohm <strohm@data-quest.de>2026-01-16 09:36:16 +0000
committerMoritz Strohm <strohm@data-quest.de>2026-01-16 09:36:16 +0000
commitb58142fe5fa1ba1a99d850baa1465df6fa6e0d3b (patch)
treed73956252dc17dd054d0db8e0f167a4103b7ba83 /resources/assets/javascripts/lib
parentc3e07e221b0bef64d3ad4da48c6371c75ca12cc3 (diff)
updated Fullcalendar to version 6, closes #4887
Closes #4887 Merge request studip/studip!4438
Diffstat (limited to 'resources/assets/javascripts/lib')
-rw-r--r--resources/assets/javascripts/lib/action.ts26
-rw-r--r--resources/assets/javascripts/lib/calendar.ts226
-rw-r--r--resources/assets/javascripts/lib/datetime.ts (renamed from resources/assets/javascripts/lib/datetime.js)92
-rw-r--r--resources/assets/javascripts/lib/fullcalendar.js977
-rw-r--r--resources/assets/javascripts/lib/holiday.ts254
5 files changed, 580 insertions, 995 deletions
diff --git a/resources/assets/javascripts/lib/action.ts b/resources/assets/javascripts/lib/action.ts
new file mode 100644
index 0000000..4d247f6
--- /dev/null
+++ b/resources/assets/javascripts/lib/action.ts
@@ -0,0 +1,26 @@
+/**
+ * The Action class represents an action in Stud.IP with its usual attributes.
+ */
+class Action
+{
+ /**
+ * The URL for the action.
+ */
+ url: string;
+
+ label: string;
+
+ icon_name: string;
+
+ constructor(
+ url: string,
+ label: string,
+ icon_name: string = ''
+ ) {
+ this.url = url;
+ this.label = label;
+ this.icon_name = icon_name;
+ }
+}
+
+export {Action};
diff --git a/resources/assets/javascripts/lib/calendar.ts b/resources/assets/javascripts/lib/calendar.ts
new file mode 100644
index 0000000..05c2e99
--- /dev/null
+++ b/resources/assets/javascripts/lib/calendar.ts
@@ -0,0 +1,226 @@
+import {EventImpl} from "@fullcalendar/core/internal";
+import {CalendarOptions, PluginDef} from "@fullcalendar/core";
+import {getLocale} from "./gettext";
+import locale_de from "@fullcalendar/core/locales/de";
+import locale_en_gb from "@fullcalendar/core/locales/en-gb";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
+import interactionPlugin from "@fullcalendar/interaction";
+
+/**
+ * The EventURLParameters class represents URL parameters for calendar events
+ * that are passed to Stud.IP controllers to do something with the event.
+ */
+class EventURLParameters
+{
+ /**
+ * The start date of the event.
+ */
+ start: Date | null;
+
+ /**
+ * The end date of the event.
+ */
+ end: Date | null;
+
+ /**
+ * Whether the event is an all-day event (true) or not (false).
+ */
+ all_day: boolean;
+
+ /**
+ * The optional resource-ID of the event. In the Stud.IP context, this can be the same
+ * as the ID of a Stud.IP resource.
+ */
+ resource_id: string | null;
+
+ /**
+ * Constructs an instance using a Fullcalendar Event object.
+ *
+ * @param event The Fullcalendar event object to construct URL parameters from.
+ */
+ constructor(event?: EventImpl) {
+ this.resource_id = null;
+ if (event) {
+ this.start = event.start;
+ this.end = event.end;
+ this.all_day = event.allDay;
+ } else {
+ this.start = null;
+ this.end = null;
+ this.all_day = false;
+ }
+ }
+
+ setStart(start: Date) {
+ this.start = start;
+ }
+
+ setEnd(end: Date) {
+ this.end = end;
+ }
+
+ setAllDay(all_day: boolean) {
+ this.all_day = all_day;
+ }
+
+ setResourceId(resource_id: string) : void {
+ this.resource_id = resource_id;
+ }
+
+ /**
+ * Converts the parameters to a plain JavaScript object to be used
+ * with the existing code in Stud.IP to fire requests or to open a dialog.
+ */
+ toObject(): object {
+ return {
+ start: this.start ? this.start.toISOString() : null,
+ end: this.end ? this.end.toISOString() : null,
+ all_day: this.all_day ? '1' : '0',
+ resource_id: this.resource_id,
+ };
+ }
+}
+
+/**
+ * The StudipCalendarConfig class provides default values for the Fullcalendar
+ * configuration that do not need to be set for each calendar instance.
+ */
+class StudipCalendarConfig
+{
+ fullcalendar_config: CalendarOptions = {};
+
+ constructor(config: CalendarOptions) {
+ this.fullcalendar_config = config;
+
+ //Set fixed options:
+ this.fullcalendar_config.firstDay = 1;
+ this.fullcalendar_config.weekNumberCalculation = 'ISO';
+ this.fullcalendar_config.height = 'auto';
+ this.fullcalendar_config.contentHeight = 'auto';
+ this.fullcalendar_config.schedulerLicenseKey = 'GPL-My-Project-Is-Open-Source';
+ this.fullcalendar_config.timeZone = 'local';
+
+ const all_views: Set<string> = new Set<string>();
+ if (this.fullcalendar_config.views) {
+ for (const view of Object.keys(this.fullcalendar_config.views)) {
+ all_views.add(view);
+ }
+ }
+ if (this.fullcalendar_config.initialView) {
+ all_views.add(this.fullcalendar_config.initialView);
+ }
+ //Load the plugins according to the views, if they are not
+ //explicitly set:
+ if (!this.fullcalendar_config.plugins) {
+ //Add the plugins here so that users of this component
+ //do not need to add them separately.
+ const active_plugins: Array<PluginDef> = [interactionPlugin];
+ for (const view_name of all_views) {
+ if (view_name === 'dayGridMonth') {
+ active_plugins.push(dayGridPlugin);
+ } else if (view_name === 'timeGridWeek' || view_name === 'timeGridDay') {
+ active_plugins.push(timeGridPlugin);
+ } else if (view_name === 'resourceTimelineWeek'
+ || view_name === 'resourceTimelineDay') {
+ active_plugins.push(resourceTimelinePlugin)
+ }
+ }
+ this.fullcalendar_config.plugins = active_plugins;
+ }
+
+ //Fullcalendar needs a short version of the locale:
+ let short_locale: string = getLocale();
+ if (short_locale) {
+ short_locale = short_locale.replace('_', '-');
+ } else {
+ short_locale = 'de-DE';
+ }
+ this.fullcalendar_config.locales = [locale_de, locale_en_gb];
+
+ //Make sure the event sources item is an array:
+ if (!this.fullcalendar_config.eventSources) {
+ this.fullcalendar_config.eventSources = [];
+ }
+
+ //Provide defaults for options that can be altered:
+ if (!this.fullcalendar_config.eventTimeFormat) {
+ this.fullcalendar_config.eventTimeFormat = {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit'
+ };
+ }
+ if (!this.fullcalendar_config.slotLabelFormat) {
+ this.fullcalendar_config.slotLabelFormat = {
+ hour: 'numeric',
+ minute: '2-digit',
+ omitZeroMinute: false
+ };
+ }
+
+ if (!this.fullcalendar_config.nowIndicator) {
+ this.fullcalendar_config.nowIndicator = true;
+ }
+ if (!this.fullcalendar_config.slotMinTime) {
+ this.fullcalendar_config.slotMinTime = '08:00';
+ }
+ if (!this.fullcalendar_config.slotMaxTime) {
+ this.fullcalendar_config.slotMaxTime = '20:00';
+ }
+ if (!this.fullcalendar_config.initialDate) {
+ this.fullcalendar_config.initialDate = new Date();
+ }
+ if (!this.fullcalendar_config.initialView && all_views.has('timeGridWeek')) {
+ //At the moment, there is only one good default for timeGridWeek.
+ this.fullcalendar_config.initialView = 'timeGridWeek';
+ }
+ if (this.fullcalendar_config.allDaySlot === undefined) {
+ this.fullcalendar_config.allDaySlot = false;
+ }
+ if (this.fullcalendar_config.allDayText === undefined) {
+ this.fullcalendar_config.allDayText = '';
+ }
+ if (this.fullcalendar_config.weekNumbers === undefined) {
+ this.fullcalendar_config.weekNumbers = true;
+ }
+ if (this.fullcalendar_config.headerToolbar) {
+ //Check if the start and end item in the toolbar is empty.
+ //In that case, navigation is not desired because it is
+ //a semester calendar.
+ const toolbar = this.fullcalendar_config.headerToolbar;
+ if ((!toolbar.start || toolbar.start.length < 1)
+ && (!toolbar.end || toolbar.end.length < 1)
+ && this.fullcalendar_config.views) {
+
+ //No navigation items present and no day header format set.
+ //This is a semester calendar. Deactivate the display of
+ //specific dates and display only the day(s) of the week.
+ if (this.fullcalendar_config.views.timeGridWeek) {
+ this.fullcalendar_config.views.timeGridWeek.dayHeaderFormat = {
+ weekday: 'long'
+ };
+ }
+ if (this.fullcalendar_config.views.timeGridDay) {
+ this.fullcalendar_config.views.timeGridDay.dayHeaderFormat = {
+ weekday: 'long'
+ };
+ }
+ }
+ } else {
+ //No toolbar defined. Define a standard one:
+ this.fullcalendar_config.headerToolbar = {
+ start: Array.from(all_views).join(','),
+ center: 'title',
+ end: 'prev,today,next'
+ };
+ }
+ }
+
+ getConfig() : CalendarOptions {
+ return this.fullcalendar_config;
+ }
+}
+
+export {EventURLParameters, StudipCalendarConfig};
diff --git a/resources/assets/javascripts/lib/datetime.js b/resources/assets/javascripts/lib/datetime.ts
index 04d7260..aa8a0bc 100644
--- a/resources/assets/javascripts/lib/datetime.js
+++ b/resources/assets/javascripts/lib/datetime.ts
@@ -1,16 +1,18 @@
-import { $gettext } from "./gettext.ts";
+import {$gettext, $gettextInterpolate} from "./gettext";
-const DateTime = {
+class DateTime
+{
/**
- * A helper method for padding strings with leading zeros.
- * @param what The date to pad.
- * @param length The length of the string to output.
+ * A helper method for padding parts of dates with leading zeros.
+ * @param item The part of a date string to pad.
+ * @param target_length The length of the string to output.
* @returns {string} A padded version of $what.
*/
- pad(what, length = 2) {
- return `00000000${what}`.substr(-length);
- },
+ pad(item: number, target_length: number = 2) : string {
+ const target: string = `00000000${item}`;
+ return target.substring(target.length - target_length);
+ }
/**
* Returns an ISO representation of the specified Date object.
@@ -19,9 +21,9 @@ const DateTime = {
* @param date The Date object to format as ISO date.
* @returns {string} The ISO date string of the Date object.
*/
- getISODate(date) {
- return date.getFullYear() + '-' + this.pad(date.getMonth() + 1) + '-' + date.getDate();
- },
+ getISODate(date: Date) : string {
+ return date.getFullYear() + '-' + this.pad(date.getMonth() + 1) + '-' + this.pad(date.getDate());
+ }
/**
* Returns a formatted version of the specified Date object
@@ -36,16 +38,21 @@ const DateTime = {
*
* @returns {*|string} The date, formatted according to the Stud.IP format for dates.
*/
- getStudipDate(date, relative_value = false, date_only = false, html = false) {
+ getStudipDate(
+ date: Date,
+ relative_value: boolean = false,
+ date_only: boolean = false,
+ html: boolean = false) : string {
if (relative_value) {
- let now = Date.now();
- if (now - date < 1 * 60 * 1000) {
+ const now: number = Date.now();
+ const date_ts: number = date.getMilliseconds();
+ if (now - date_ts < 60 * 1000) {
return $gettext('Jetzt');
}
- if (now - date < 2 * 60 * 60 * 1000) {
- return $gettext(
+ if (now - date_ts < 2 * 60 * 60 * 1000) {
+ return $gettextInterpolate(
'Vor %{ minutes } Minuten',
- {minutes: Math.floor((now - date) / (1000 * 60))}
+ {minutes: Math.floor((now - date_ts) / (1000 * 60))}
);
}
return this.pad(date.getHours()) + ':' + this.pad(date.getMinutes());
@@ -79,7 +86,56 @@ const DateTime = {
return this.pad(date.getDate()) + '.' + this.pad(date.getMonth() + 1) + '.' + date.getFullYear() + ' ' + this.pad(date.getHours()) + ':' + this.pad(date.getMinutes());
}
}
-};
+
+ getDayOfWeekName(dow: number, short: boolean = false): string {
+ if (dow === 0 || dow === 7) {
+ if (short) {
+ return $gettext('So.');
+ } else {
+ return $gettext('Sonntag');
+ }
+ } else if (dow === 1) {
+ if (short) {
+ return $gettext('Mo.');
+ } else {
+ return $gettext('Montag');
+ }
+ } else if (dow === 2) {
+ if (short) {
+ return $gettext('Di.');
+ } else {
+ return $gettext('Dienstag');
+ }
+ } else if (dow === 3) {
+ if (short) {
+ return $gettext('Mi.');
+ } else {
+ return $gettext('Mittwoch');
+ }
+ } else if (dow === 4) {
+ if (short) {
+ return $gettext('Do.');
+ } else {
+ return $gettext('Donnerstag');
+ }
+ } else if (dow === 5) {
+ if (short) {
+ return $gettext('Fr.');
+ } else {
+ return $gettext('Freitag');
+ }
+ } else if (dow === 6) {
+ if (short) {
+ return $gettext('Sa.');
+ } else {
+ return $gettext('Samstag');
+ }
+ } else {
+ return '';
+ }
+ }
+}
export default DateTime;
+export const datetime: DateTime = new DateTime();
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
deleted file mode 100644
index 5882460..0000000
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ /dev/null
@@ -1,977 +0,0 @@
-/**
- * This class contains Stud.IP specific code for the fullcalendar package.
- */
-
-import { Calendar } from '@fullcalendar/core';
-import deLocale from '@fullcalendar/core/locales/de';
-import enLocale from '@fullcalendar/core/locales/en-gb';
-import interactionPlugin from '@fullcalendar/interaction';
-import { Draggable } from '@fullcalendar/interaction';
-import dayGridPlugin from '@fullcalendar/daygrid';
-import timeGridPlugin from '@fullcalendar/timegrid';
-import resourceCommonPlugin from '@fullcalendar/resource-common';
-import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
-import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
-
-import { jsPDF } from 'jspdf';
-import html2canvas from 'html2canvas';
-import Responsive from "./responsive";
-
-Date.prototype.getWeekNumber = function () {
- var d = new Date(Date.UTC(this.getFullYear(), this.getMonth(), this.getDate()));
- var dayNum = d.getUTCDay() || 7;
- d.setUTCDate(d.getUTCDate() + 4 - dayNum);
- var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
- return Math.ceil((((d - yearStart) / 86400000) + 1)/7);
-};
-
-function pad(what, length = 2, char = '0') {
- let padding = new Array(length + 1).join(char);
- return `${padding}${what}`.substr(-length);
-}
-
-class Fullcalendar
-{
- static holidayCache = sessionStorage.getItem('fullcalendar_holidays') ? JSON.parse(sessionStorage.getItem('fullcalendar_holidays')) : {};
- static vacationCache = sessionStorage.getItem('fullcalendar_vacations') ? JSON.parse(sessionStorage.getItem('fullcalendar_vacations')) : {};
-
- static loadHolidays(year) {
- if (this.holidayCache[year]) {
- return Promise.resolve(this.holidayCache[year]);
- }
-
- return STUDIP.jsonapi.withPromises().get('holidays', {
- data: { 'filter[year]': year }
- }).then(response => {
- const events = [];
- if (!response) {
- return events;
- }
-
- for (const [date, data] of Object.entries(response)) {
- const classNames = ['holiday'];
- if (data.mandatory) {
- classNames.push('official');
- }
-
- const day = new Date(date);
- events.push({
- // Note: Since allDay is set to true, the start and end time is ignored.
- // See the documentation: https://fullcalendar.io/docs/v4/event-parsing
- start: day,
- end: day,
- allDay: true,
- title: data.holiday,
- editable: false,
-
- classNames,
-
- // Note: Colours are set via SCSS.
- textColor: '',
- color: '',
- borderColor: '',
-
- rendering: 'background'
- });
- }
-
- this.holidayCache[year] = events;
-
- sessionStorage.setItem('fullcalendar_holidays', JSON.stringify(this.holidayCache));
-
- return events;
- });
- }
-
- static loadVacations(year) {
- if (this.vacationCache[year]) {
- return Promise.resolve(this.vacationCache[year]);
- }
-
- return STUDIP.jsonapi.withPromises().get('vacations', {
- data: {'filter[year]': year}
- }).then(response => {
- if (!response) {
- return [];
- }
-
- const items = [];
-
- for (const vacation_data of Object.values(response)) {
- const start = new Date(parseInt(vacation_data.start) * 1000);
- const end = new Date(parseInt(vacation_data.end) * 1000);
- items.push({
- start,
- end,
- allDay: true,
- title: vacation_data.name,
- editable: false,
- classNames: ['holiday'],
-
- // Note: Colours are set via SCSS.
- textColor: '',
- color: '',
- borderColor: '',
-
- rendering: 'background'
- });
- }
-
- this.vacationCache[year] = items;
-
- sessionStorage.setItem('fullcalendar_vacations', JSON.stringify(this.vacationCache));
-
- return items;
- });
- }
-
- /**
- * The initialisation method. It loads the JS files for fullcalendar
- * in case they are not loaded and sets up a fullcalendar instance
- * for the nodes specified in the parameter node.
- *
- * @param DOMElement|string node The node which shall have a full calendar.
- * This must either be a DOMElement or a string
- * containing a CSS selector.
- */
- static init(node, fullcalendar_options = null)
- {
- // Convert css selector to actual dom element
- node = $(node)[0];
-
- if (!node) {
- //We need a CSS selector or a node!
- return;
- }
-
- if (document.getElementById('external-events')) {
- new Draggable(document.getElementById('external-events'), {
- itemSelector: '.fc-event',
- eventData (eventEl) {
- return {
- title: eventEl.dataset.eventTitle,
- duration: eventEl.dataset.eventDuration,
- course_id: eventEl.dataset.eventCourse,
- tooltip: eventEl.dataset.eventTooltip,
- studip_api_urls: {drop: eventEl.dataset.eventDropUrl},
- studip_view_urls: {edit: eventEl.dataset.eventDetailsUrl}
- };
- }
- });
- }
-
- var calendar = new Calendar(node, fullcalendar_options);
- node.calendar = calendar;
- calendar.render();
-
- return calendar;
- }
-
- /**
- * Converts semester events to the default fullcalendar event format.
- * The begin and end date are converted to fit into the current week.
- */
- static convertSemesterEvents(event_data, fake_week_start = Date())
- {
- if (!event_data) {
- return {};
- }
-
- var start = String(event_data.start).split('T');
- var end = String(event_data.end).split('T');
-
- //start and end must be transformed to the current week.
- //Therefore, we need the ISO weekdays for begin and end.
- var fake_start = new Date(fake_week_start);
- fake_start.setHours(12);
- fake_start.setMinutes(0);
- fake_start.setSeconds(0);
- var fake_end = new Date(fake_week_start);
- fake_end.setHours(12);
- fake_end.setMinutes(0);
- fake_end.setSeconds(0);
-
- //Calculcate the week day of the current week for the event
- //from the current day and convert sunday to ISO format
- var start_day_diff = fake_start.getDay() || 7;
- var end_day_diff = fake_end.getDay() || 7;
-
- start_day_diff = start_day_diff - event_data.studip_weekday_begin;
- end_day_diff = end_day_diff - event_data.studip_weekday_end;
-
- fake_start = new Date(
- fake_start.getTime() - start_day_diff * 24 * 60 * 60 * 1000
- );
- fake_end = new Date(
- fake_end.getTime() - end_day_diff * 24 * 60 * 60 * 1000
- );
-
- //Output the modified begin and end date in the correct ISO format:
- event_data.start =`${fake_start.getFullYear()}-${pad(fake_start.getMonth() + 1)}-${pad(fake_start.getDate())}T${start[1]}`;
- event_data.end = `${fake_end.getFullYear()}-${pad(fake_end.getMonth() + 1)}-${pad(fake_end.getDate())}T${end[1]}`;
-
- return event_data;
- }
-
-
- static createSemesterCalendarFromNode(node, additional_config = {})
- {
- if (!node) {
- //Ain't no fullcalendar when the node's gone!
- return;
- }
-
- var config = $.extend(
- {},
- $(node).data('config') || {},
- additional_config
- );
-
- if (Array.isArray(config.eventSources)) {
- config.eventSources = config.eventSources.map((s) => {
- if (s.url !== undefined) {
- return s;
- }
- });
- }
-
- return this.createFromNode(node, config);
- }
-
-
- static defaultResizeEventHandler(info)
- {
- if (!info.event.durationEditable || !info.view.viewSpec.options.editable) {
- //Read-only events cannot be resized!
- info.revert();
- return;
- }
-
- if (info.event.extendedProps.studip_api_urls.resize) {
- $.post({
- url: info.event.extendedProps.studip_api_urls.resize,
- async: false,
- data: {
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- } else if (info.event.extendedProps.studip_api_urls.resize_dialog) {
- STUDIP.Dialog.fromURL(
- info.event.extendedProps.studip_api_urls.resize_dialog,
- {
- data: {
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }
- );
- }
- }
-
- static downloadPDF(format = 'landscape', withWeekend = false)
- {
- $('*[data-fullcalendar="1"]').each(function () {
- if (this.calendar != undefined) {
- $(this).addClass('print-view').toggleClass('without-weekend', !withWeekend);
-
- var title = $(this).data('title');
- let print_title = $('<h1>').text(title).prependTo(this);
-
- window.scrollTo(0, 0);
-
- html2canvas(this).then(canvas => {
- var imgData = canvas.toDataURL('image/jpeg');
- var pdf = new jsPDF({
- orientation: format === 'landscape' ? 'landscape' : 'portrait'
- });
- if (format === 'landscape') {
- pdf.addImage(imgData, 'JPEG', 20, 20, 250, 250, 'i1', 'NONE', 0);
- } else {
- pdf.addImage(imgData, 'JPEG', 25, 20, 160, 190, 'i1', 'NONE', 0);
- }
- pdf.save(title + '.pdf');
- });
-
- print_title.remove();
- $(this).removeClass('print-view without-weekend');
- }
- });
- }
-
- static toRFC3339String(date)
- {
- var timezone_offset_min = date.getTimezoneOffset();
- var offset_hrs = parseInt(Math.abs(timezone_offset_min / 60), 10);
- var offset_min = Math.abs(timezone_offset_min%60);
- var timezone_standard;
-
- offset_hrs = pad(offset_hrs);
- offset_min = pad(offset_min);
-
- // Add an opposite sign to the offset
- // If offset is 0, it means timezone is UTC
- if (timezone_offset_min < 0) {
- timezone_standard = `+${offset_hrs}:${offset_min}`;
- } else if (timezone_offset_min > 0) {
- timezone_standard = `-${offset_hrs}:${offset_min}`;
- } else {
- timezone_standard = '+00:00';
- }
-
- var current_date = pad(date.getDate());
- var current_month = pad(date.getMonth() + 1);
- var current_year = date.getFullYear();
- var current_hrs = pad(date.getHours());
- var current_mins = pad(date.getMinutes());
- var current_secs = pad(date.getSeconds());
- var current_datetime;
-
- // Current datetime
- // String such as 2016-07-16T19:20:30
- current_datetime = `${current_year}-${current_month}-${current_date}T${current_hrs}:${current_mins}:${current_secs}`;
-
- return current_datetime + timezone_standard;
- }
-
- static defaultDropEventHandler(info)
- {
- // The logic from fullcalendar is inversed here:
- // If the calendar isn't editable, the event isn't either.
- if (!info.event.startEditable || !info.view.viewSpec.options.editable) {
- //Read-only events cannot be dragged and dropped!
- info.revert();
- return;
- }
-
- var drop_resource_id = info.newResource ? info.newResource.id : info.event.extendedProps.studip_range_id;
-
- if (info.event.extendedProps.studip_api_urls.move || info.event.extendedProps.studip_api_urls.move_dialog) {
- let move_dialog = info.event.extendedProps.studip_api_urls.move_dialog;
- if (info.event.allDay) {
- if (move_dialog) {
- STUDIP.Dialog.fromURL(
- move_dialog,
- {
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)),
- end: this.toRFC3339String(info.event.start.setHours(23, 59, 59))
- },
- size: 'auto'
- }
- );
- } else {
- jQuery.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)),
- end: this.toRFC3339String(info.event.start.setHours(23, 59, 59))
- }
- }).fail(info.revert);
- }
- } else if (info.event.end === null) {
- let real_end = new Date();
- real_end.setTime(info.event.start.getTime());
- real_end.setHours(info.event.start.getHours()+2);
- if (move_dialog) {
- STUDIP.Dialog.fromURL(
- move_dialog,
- {
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(real_end)
- },
- size: 'auto'
- }
- );
- } else {
- jQuery.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(real_end)
- }
- }).fail(info.revert);
- }
- } else {
- if (move_dialog) {
- STUDIP.Dialog.fromURL(
- move_dialog,
- {
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- },
- size: 'auto'
- }
- );
- } else {
- jQuery.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- }
- }
- }
- }
-
- static institutePlanDropEventHandler(info)
- {
- //The logic from fullcalendar is inversed here:
- if (info.newResource) {
- $.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- cycle_id: info.event.id,
- resource_id: info.newResource.id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- } else {
- //If the calendar isn't editable, the event isn't either.
- if (!info.event.startEditable || !info.view.viewSpec.options.editable) {
- //Read-only events cannot be dragged and dropped!
- info.revert();
- return;
- }
-
- $.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- cycle_id: info.event.id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- }
- }
-
- static institutePlanExternalDropEventHandler(info)
- {
- var resourceIds = info.event.getResources().map(resource => resource.id);
-
- $.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.drop,
- data: {
- course_id: info.event.extendedProps.course_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end),
- resource_id: resourceIds[0]
- }
- }).done(data => {
- if (data) {
- info.view.context.calendar.addEvent(JSON.parse(data));
- info.event.remove();
- }
- });
- }
-
- static createFromNode(node, additional_config = {})
- {
- if (!node) {
- //No node? No fullcalendar!
- return;
- }
-
- let config = $(node).data('config');
-
- let defaultView = 'timeGridWeek';
- if (Responsive.isResponsive() && config.responsiveDefaultView !== undefined) {
- defaultView = config.responsiveDefaultView;
- } else if (config.defaultView !== undefined) {
- defaultView = config.defaultView;
- }
-
- //Make sure the default values are set, if they are not found
- //in the additional_config object:
- config = $.extend({
- plugins: [ interactionPlugin, dayGridPlugin, timeGridPlugin, resourceCommonPlugin, resourceTimeGridPlugin, resourceTimelinePlugin ],
- schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
- header: {
- left: 'dayGridMonth,timeGridWeek,timeGridDay'
- },
- minTime: '08:00:00',
- maxTime: '20:00:00',
- validRange: {
- start: '1970-01-01'
- },
- height: 'auto',
- contentHeight: 'auto',
- firstDay: 1,
- weekNumberCalculation: 'ISO',
- locales: [enLocale, deLocale ],
- locale: String.locale === 'de-DE' ? 'de' : 'en-gb',
- timeFormat: 'H:mm',
- slotLabelFormat: {
- hour: 'numeric',
- minute: '2-digit',
- omitZeroMinute: false
- },
- columnHeaderHtml: STUDIP.Fullcalendar.renderDateForColumn,
- nowIndicator: true,
- timeZone: 'local',
- studip_functions: [],
- resourceAreaWidth: '20%',
- select (selectionInfo) {
- let calendar_config = JSON.parse(selectionInfo.view.context.calendar.el.dataset.config);
- let dialog_size = 'auto';
- if (calendar_config.dialog_size !== undefined) {
- dialog_size = calendar_config.dialog_size;
- }
-
- if (!selectionInfo.view.viewSpec.options.editable || !selectionInfo.view.viewSpec.options.studip_urls) {
- //The calendar isn't editable.
- return;
- }
- if (selectionInfo.view.viewSpec.options.studip_urls.add) {
- if (selectionInfo.resource) {
- STUDIP.Dialog.fromURL( selectionInfo.view.viewSpec.options.studip_urls.add, {
- data: {
- begin: selectionInfo.start.getTime()/1000,
- end: selectionInfo.end.getTime()/1000,
- ressource_id: selectionInfo.resource.id,
- all_day: selectionInfo.allDay ? '1' : '0'
- },
- size: dialog_size
- });
- } else {
- STUDIP.Dialog.fromURL(selectionInfo.view.viewSpec.options.studip_urls.add, {
- data: {
- begin: selectionInfo.start.getTime()/1000,
- end: selectionInfo.end.getTime()/1000,
- all_day: selectionInfo.allDay ? '1' : '0'
- },
- size: dialog_size
- });
- }
- }
- },
- eventClick (eventClickInfo) {
- var event = eventClickInfo.event;
- var extended_props = event.extendedProps;
- if ($(eventClickInfo.jsEvent.target).hasClass('event-colorpicker')) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/pick_color/' + extended_props.metadate_id + '/' + config.actionCalled),
- {'size': '400x400'}
- );
- return false;
- }
-
-
- if ($(eventClickInfo.event._calendar.el).hasClass('request-plan')) {
- if (extended_props.request_id && extended_props.studip_view_urls.edit) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit)
- );
- } else if(extended_props.studip_parent_object_class == 'ResourceBooking' && $.inArray('for-course', event._def.ui.classNames) != -1) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL('dispatch.php/resources/room_request/rerequest_booking/' + extended_props.studip_parent_object_id)
- );
- }
- return false;
- }
-
- if (extended_props.studip_view_urls === undefined) {
- return;
- }
- let calendar_config = JSON.parse(eventClickInfo.view.context.calendar.el.dataset.config);
- let dialog_size = 'auto';
- if (calendar_config.dialog_size !== undefined) {
- //Use the configured default dialog size for the fullcalendar instance:
- dialog_size = calendar_config.dialog_size;
- }
- if (extended_props.dialog_size !== undefined) {
- //Use the dialog size of the event:
- dialog_size = extended_props.dialog_size;
- }
- if (!event.startEditable && extended_props.studip_view_urls.show) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show),
- {size: dialog_size}
- );
- } else if (event.startEditable) {
- if (extended_props.studip_view_urls.edit) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit),
- {size: dialog_size}
- );
- } else if (extended_props.studip_view_urls.show) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show),
- {size: dialog_size}
- );
- }
- }
- return false;
- },
- eventResize (info) {
- // The logic from fullcalendar is inversed here:
- // If the calendar isn't editable, the event isn't either.
- if (info.view.viewSpec.options.studip_functions.resize_event) {
- info.view.viewSpec.options.studip_functions.resize_event(info);
- } else {
- STUDIP.Fullcalendar.defaultResizeEventHandler(info);
- }
- info.event.source.refetch();
- },
- eventDrop (info) {
- let handle_drop = function() {
- if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
- var start = info.event.start;
- var cal_start = info.view.activeStart;
- if ((start.getHours() - cal_start.getHours()) % 2 === 1) {
- info.event.moveDates('-01:00');
- }
- STUDIP.Fullcalendar.institutePlanDropEventHandler(info);
- } else {
- if (info.view.viewSpec.options.studip_functions.drop_event) {
- info.view.viewSpec.options.studip_functions.drop_event(info);
- } else {
- STUDIP.Fullcalendar.defaultDropEventHandler(info);
- }
- info.event.source.refetch();
- }
- };
-
- let calendar_config = JSON.parse(info.view.context.calendar.el.dataset.config);
- if (calendar_config.confirm) {
- if (calendar_config.confirm.drop) {
- STUDIP.Dialog.confirm(calendar_config.confirm.drop)
- .done(handle_drop)
- .fail(function() {
- //Revert the dropped element:
- info.revert();
- });
- } else {
- handle_drop();
- }
- } else {
- handle_drop();
- }
- },
- eventRender (info) {
- let event = info.event;
- let eventElement = info.el;
- let iconColor = event.textColor === '#000000' ? 'black' : 'white';
-
- if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
- $(eventElement).attr('title', event.extendedProps.tooltip);
- if (event.extendedProps.content_fields) {
- for (const [css_class, field] of Object.entries(event.extendedProps.content_fields)) {
- $(eventElement).find('.fc-content').append(
- $('<div>').css({
- width: 'calc(100% - 21px)',
- height: '100%',
- wordBreak: 'break-word'
- }).text(field)
- .addClass(css_class + ' fc-title')
- );
- }
- }
- $(eventElement).find('.fc-content').append(
- $('<button class="event-colorpicker">').addClass(iconColor)
- );
- } else {
- $(eventElement).attr('title', event.title);
- }
-
- if (event.extendedProps.icon) {
- //Check if there is more than one icon:
- let event_icons = event.extendedProps.icon.split(',');
- let title = $(eventElement).find('.fc-title');
- for (let icon of event_icons) {
- //Check if the icon is already a URL or just the name of an icon.
- let iconUrl = '';
- if (icon.includes('://')) {
- //The icon is already a URL.
- iconUrl = icon;
- } else {
- //The icon is just referenced by its name. We do not need a specific color here, background-color is currentColor.
- iconUrl = `${STUDIP.ASSETS_URL}images/icons/black/${icon}.svg`
- }
- //Add the icons as spans in front of the content:
- let icon_element = $('<span class="icon"></span>');
- icon_element.css('--icon-url', `url('${iconUrl}')`);
- title.prepend(icon_element);
- }
- }
-
- //If a background event with title shall be rendered, we have to check
- //if an all-day slot is present or not.
- let generate_title = false;
- if (event.rendering === 'background' && event.title && event.allDay) {
- if (info.view.viewSpec.options.allDaySlot === true) {
- //An all-day slot is present in the calendar.
- if (info.isStart === true || info.isEnd === true) {
-
- generate_title = true;
- }
- } else {
- //No all-day slot in the calendar. Display a title
- //at the start of the day in the calendar.
- if (info.isStart === false || info.isEnd === false) {
- generate_title = true;
- }
- }
- }
- if (generate_title) {
- //Generate a visible title for the background element:
- let element = $('<div class="title"></div>');
- $(element).text(event.title);
- if (event.textColor) {
- element.css('color', event.textColor);
- }
- $(eventElement).append(element);
- }
- },
- eventSourceSuccess: function(content) {
- if ($(node).hasClass('semester-plan')) {
- $(content).each(function(i, event_data){
- STUDIP.Fullcalendar.convertSemesterEvents(event_data, config.defaultDate);
- });
- }
- return content;
- },
- loading (isLoading) {
- if (isLoading) {
- if (!$('#loading-spinner').length) {
- jQuery('#content').append(
- $('<div id="loading-spinner" style="position: absolute; top: calc(50% - 55px); left: calc(50% + 135px); z-index: 9001;">').html(
- $('<img>').attr('src', STUDIP.ASSETS_URL + 'images/loading-indicator.svg')
- .css({
- width: 32,
- height: 32
- })
- )
- );
- }
- } else {
- $('#loading-spinner').remove();
- this.updateSize();
- }
- },
- datesRender (info) {
- if ($(node).hasClass('schedule') && info.view.type !== 'timeGridDay') {
- document.querySelector('.fc-right').style.display = 'none';
- }
-
- if ($(node).hasClass('semester-plan')) {
- //Nothing to do in the semester plan, since it already displays
- //all the information on loading.
- return;
- }
- let activeRange = info.view.props.dateProfile.activeRange;
- let timestamp = activeRange.start.getTime() / 1000;
- if ($(info.el).hasClass('institute-plan')) {
- $('.fc-slats tr:odd .fc-widget-content:not(.fc-axis)').remove();
- }
-
- if (document.getElementById('booking-plan-header-semname') === null) {
- return;
- }
-
- $.getJSON(
- STUDIP.URLHelper.getURL(`dispatch.php/resources/ajax/semester_week/${timestamp}`)
- ).done((data) => {
- if (data) {
- $('#booking-plan-header-semname').text(data.semester_name);
- if (data.sem_week) {
- $('#booking-plan-header-semweek').text(data.sem_week);
- $('#booking-plan-header-semweek-part').show();
- } else {
- $('#booking-plan-header-semweek').text('');
- $('#booking-plan-header-semweek-part').hide();
- }
- $('#booking-plan-header-semrow').show();
- $('#booking-plan-header-calweek').text(data.week_number);
- $('#booking-plan-header-calbegin').text('(' + data.current_day + ')');
- } else {
- $('#booking-plan-header-semrow').hide();
- $('#booking-plan-header-semweek-part').hide();
- }
- });
- },
- resourceRender (renderInfo) {
- if ($(renderInfo.view.context.calendar.el).hasClass('room-group-booking-plan')) {
- let action = $(renderInfo.view.context.calendar.el).hasClass('semester-plan') ? 'semester' : 'booking';
- let url = STUDIP.URLHelper.getURL(`dispatch.php/resources/room_planning/${action}_plan/${renderInfo.resource.id}`);
- $(renderInfo.el).find('.fc-cell-text').html(
- $('<a>').attr('href', url).text(renderInfo.resource.title)
- );
- } else if ($("*[data-fullcalendar='1']").hasClass('institute-plan') && renderInfo.resource.id > 0) {
- const icon = '<span class="btn-icon btn-icon--edit icon-role-clickable" aria-label="edit"></span>';
- $(renderInfo.el).append(
- '<a href="'
- + STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/rename_column/'
- + renderInfo.resource.id
- +'/'
- + renderInfo.view.activeStart.getDay())
- + '" data-dialog="size=auto"> '
- + icon
- + '</a>'
- );
- }
- },
- drop (dropInfo) {
- $(dropInfo.draggedEl).remove();
- },
- eventReceive (info) {
- if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
- STUDIP.Fullcalendar.institutePlanExternalDropEventHandler(info);
- }
- }
- }, config);
-
- //Special treatment: If a general column header format is set,
- //in the configuration, it shall be used for all columns in all views
- //by using a special columnHeaderHtml function.
- if (config.columnHeaderFormat) {
- config.columnHeaderHtml = function (date) {
- if ($("*[data-fullcalendar='1']").hasClass('institute-plan')) {
- return '<a href="' + STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/weekday/' + date.getDay()) + '">' + date.toLocaleDateString('de-DE', config.columnHeaderFormat) + '</a>';
- } else {
- return date.toLocaleDateString('de-DE', config.columnHeaderFormat);
- }
- };
- }
-
- config = $.extend({}, config, additional_config);
-
- if (!Array.isArray(config.eventSources)) {
- config.eventSources = [];
- }
- config.eventSources.push({
- events: (fetchInfo, successCallback, failureCallback) => {
- const startYear = fetchInfo.start.getFullYear();
- const endYear = fetchInfo.end.getFullYear();
- const requests = [];
- for (let year = startYear; year <= endYear; year++) {
- requests.push(this.loadHolidays(year));
- requests.push(this.loadVacations(year));
- }
- Promise.all(requests).then(results => {
- const events = [].concat(...results);
- successCallback(events);
- return results;
- }).catch(failureCallback);
- },
- });
-
- config.defaultView = defaultView;
-
- return this.init(node, config);
- }
-
- static submitDatePicker() {
- let picked_date = jQuery('#booking-plan-jmpdate').val();
- if (!picked_date) {
- //Not a booking plan date selector.
- picked_date = jQuery('#date_select').val();
- }
- let iso_date_string = '';
- if (picked_date) {
- if (picked_date.includes('.')) {
- let [day, month, year] = picked_date.split('.');
- iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0');
- } else if (picked_date.includes('/')) {
- let [day, month, year] = picked_date.split('/');
- iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0');
- } else if (picked_date.includes('-')) {
- iso_date_string = picked_date;
- }
- }
- if (iso_date_string) {
- jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () {
- this.calendar.gotoDate(iso_date_string);
- });
- Fullcalendar.updateDateURL();
- }
- }
-
- static updateDateURL() {
- let changedMoment;
- jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () {
- changedMoment = this.calendar.getDate();
- });
- if (changedMoment) {
- let changed_date = STUDIP.Fullcalendar.toRFC3339String(changedMoment).split('T')[0];
- //Get the timestamp:
- let timestamp = changedMoment.getTime() / 1000;
-
- jQuery('a.resource-bookings-actions, a.calendar-action').each(function () {
- const url = new URL(this.href);
- url.searchParams.set('timestamp', timestamp.toString())
- url.searchParams.set('defaultDate', changed_date)
- this.href = url.toString();
- });
- jQuery('.sidebar-widget.calendar-action').each(function() {
- //Each sidebar widget is different. The placement of the defaultDate URL parameter
- //has to reflect that.
- jQuery(this).find('button[formaction]').each(function() {
- //Modify the formaction attribute:
- let url = new URL(jQuery(this).attr('formaction'));
- url.searchParams.set('defaultDate', changed_date);
- jQuery(this).attr('formaction', url.toString());
- });
- jQuery(this).find('form[action]').each(function() {
- //Add a hidden input with the defaultDate:
- let hidden_input = jQuery(this).find('input[name="defaultDate"]')[0];
- if (!hidden_input) {
- hidden_input = jQuery('<input type="hidden" name="defaultDate">');
- jQuery(this).append(hidden_input);
- }
- jQuery(hidden_input).val(changed_date);
- });
- });
-
- // Now change the URL of the window.
- const url = new URL(window.location.href);
- url.searchParams.set('defaultDate', changed_date);
-
- // Update url in history
- history.pushState({}, null, url.toString());
-
- // Adjust links accordingly
- url.searchParams.delete('allday');
- jQuery('.booking-plan-std_view').attr('href', url.toString());
-
- url.searchParams.set('allday', 1);
- jQuery('.booking-plan-allday_view').attr('href', url.toString());
-
- // Update sidebar value
- let element = jQuery('#booking-plan-jmpdate,#date_select').first();
- let padded_date = pad(changedMoment.getDate(), 2, '0')
- + '.' + pad(changedMoment.getMonth() + 1, 2, '0')
- + '.' + changedMoment.getFullYear();
- element.val(padded_date);
- if (element.is('#booking-plan-jmpdate')) {
- //Store the date in the sessionStorage:
- sessionStorage.setItem('booking_plan_date', changed_date);
- }
- }
- }
-
- static renderDateForColumn(date) {
- let format = new Intl.DateTimeFormat(String.locale, {weekday: 'short'});
- let date_html = STUDIP.DateTime.getStudipDate(date, false, true, true);
- return '<span class="dow">' + format.format(date) + '.</span> ' + date_html;
- }
-}
-
-export default Fullcalendar;
diff --git a/resources/assets/javascripts/lib/holiday.ts b/resources/assets/javascripts/lib/holiday.ts
new file mode 100644
index 0000000..2363eb1
--- /dev/null
+++ b/resources/assets/javascripts/lib/holiday.ts
@@ -0,0 +1,254 @@
+import {jsonapi} from "./jsonapi";
+import {datetime} from "./datetime";
+
+/**
+ * The Holiday class represents a holiday in Stud.IP
+ */
+class Holiday
+{
+ /**
+ * The day of the holiday.
+ */
+ day: Date;
+
+ /**
+ * The name of the holiday.
+ */
+ name: string;
+
+ /**
+ * Whether the holiday is an official holiday (true) or not (false).
+ * The latter means that it can be part of a vacation period.
+ */
+ official: boolean;
+
+ constructor(day: Date, name: string, official: boolean = true) {
+ this.day = day;
+ this.name = name;
+ this.official = official;
+ }
+}
+
+
+/**
+ * The HolidayCache class handles retrieving and caching holidays and vacations.
+ */
+class HolidayCache
+{
+ holiday_cache: Map<number, Array<Holiday>>;
+ vacation_cache: Map<number, Array<Holiday>>;
+
+ /**
+ * The constructor has code to load cached holidays and vacations from the session storage.
+ */
+ constructor() {
+ //Attempt to restore the cache from the session:
+ const holiday_cache_str = sessionStorage.getItem('fullcalendar_holidays');
+ this.holiday_cache = new Map<number, Array<Holiday>>();
+ if (holiday_cache_str != null) {
+ for (const [year, raw_array] of Object.entries(JSON.parse(holiday_cache_str))) {
+ const holidays: Array<Holiday> = [];
+ if (Array.isArray(raw_array)) {
+ for (const raw_holiday of raw_array) {
+ holidays.push(
+ new Holiday(new Date(raw_holiday.day), raw_holiday.name, raw_holiday.official)
+ );
+ }
+ this.holiday_cache.set(parseInt(year), holidays);
+ }
+ }
+ }
+
+ const vacation_cache_str = sessionStorage.getItem('fullcalendar_vacations');
+ this.vacation_cache = new Map<number, Array<Holiday>>();
+ if (vacation_cache_str != null) {
+ for (const [year, raw_array] of Object.entries(JSON.parse(vacation_cache_str))) {
+ const vacations: Array<Holiday> = [];
+ if (Array.isArray(raw_array)) {
+ for (const raw_vacation of raw_array) {
+ vacations.push(
+ new Holiday(new Date(raw_vacation.day), raw_vacation.name, raw_vacation.official)
+ );
+ }
+ this.vacation_cache.set(parseInt(year), vacations);
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads the holidays of a year and stores them in the cache.
+ *
+ * @param year The year for which to load holidays.
+ */
+ loadHolidays(year: number) : void {
+ const existing_cache = this.holiday_cache.get(year);
+ if (existing_cache) {
+ return;
+ }
+
+ jsonapi.withPromises().GET('holidays', {
+ data: { 'filter[year]': year }
+ }).then(response => {
+ if (!response) {
+ return;
+ }
+ const events: Array<Holiday> = [];
+ for (const [date, data] of Object.entries(response)) {
+ const day = new Date(date);
+ events.push(new Holiday(day, data.holiday, data.mandatory));
+ }
+
+ this.holiday_cache.set(year, events);
+ //Update the session storage item:
+ sessionStorage.setItem('fullcalendar_holidays', JSON.stringify(Object.fromEntries(this.holiday_cache)));
+ });
+ }
+
+ /**
+ * Loads the vacation days of a year and stores them in the cache.
+ *
+ * @param year The year for which to load vacations.
+ */
+ loadVacations(year: number): void {
+ const existing_cache = this.vacation_cache.get(year);
+ if (existing_cache) {
+ return;
+ }
+ jsonapi.withPromises().get('vacations', {
+ data: {'filter[year]': year}
+ }).then(response => {
+ if (!response) {
+ return;
+ }
+ const events: Array<Holiday> = [];
+ for (const vacation_data of Object.values(response)) {
+ for (let i = parseInt(vacation_data.start); i < parseInt(vacation_data.end); i += 86400) {
+ const day = new Date(i * 1000);
+ events.push(new Holiday(day, vacation_data.name, false));
+ }
+ }
+
+ this.vacation_cache.set(year, events);
+ //Update the session storage item:
+ sessionStorage.setItem('fullcalendar_vacations', JSON.stringify(Object.fromEntries(this.vacation_cache)));
+ });
+ }
+
+ /**
+ * Returns a vacation day from the cache.
+ *
+ * @param date The day of the vacation.
+ *
+ * @returns Holiday|null If a vacation exists on the specified day, a Holiday object
+ * for it is returned. Otherwise, null is returned.
+ */
+ getVacation(date: Date) : Holiday|null {
+ const year_vacation_cache = this.vacation_cache.get(date.getFullYear());
+ if (!year_vacation_cache) {
+ return null;
+ }
+ for (const vacation of year_vacation_cache) {
+ if (datetime.getISODate(date) === datetime.getISODate(vacation.day)) {
+ //A vacation day has been found.
+ return vacation;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks if there is a vacation day on the specified date.
+ *
+ * @param date The date to check.
+ *
+ * @returns boolean True, if the specified day is a vacation day.
+ * Otherwise, false is returned.
+ */
+ isVacation(date: Date) {
+ return this.getVacation(date) !== null;
+ }
+
+ /**
+ * Returns the name of the vacation day on the specified date.
+ *
+ * @param date The date for which to get the vacation name.
+ *
+ * @returns string The name of the vacation, if any.
+ * If there is no vacation day on the specified date, the string is empty.
+ */
+ getVacationName(date: Date) {
+ const vacation = this.getVacation(date);
+ if (vacation) {
+ return vacation.name;
+ }
+ return '';
+ }
+
+ /**
+ * Returns a holiday / vacation day from the cache.
+ *
+ * @param date The date of the holiday.
+ *
+ * @param regard_vacations Whether to regard vacations in addition to holidays (true)
+ * or to just regard holidays (false). Defaults to false.
+ *
+ * @returns Holiday|null If a holiday can be found, it is returned. Otherwise,
+ * null is returned.
+ */
+ getHoliday(date: Date, regard_vacations: boolean = false): Holiday|null {
+ //Check if an entry for the date exists in the holiday or the vacation list:
+ const year_holiday_cache = this.holiday_cache.get(date.getFullYear());
+ if (!year_holiday_cache) {
+ return null;
+ }
+ for (const holiday of year_holiday_cache) {
+ if (datetime.getISODate(date) === datetime.getISODate(holiday.day)) {
+ //A holiday has been found.
+ return holiday;
+ }
+ }
+ if (regard_vacations) {
+ return this.getVacation(date);
+ }
+ return null;
+ }
+
+ /**
+ * Checks if there is a holiday on the specified date.
+ *
+ * @param date The date to check.
+ *
+ * @param regard_vacations Whether to regard vacations in addition to holidays (true)
+ * or to just regard holidays (false). Defaults to false.
+ *
+ * @returns boolean True, if the specified day is a holiday / vacation day.
+ * Otherwise, false is returned.
+ */
+ isHoliday(date: Date, regard_vacations: boolean = false) : boolean {
+ return this.getHoliday(date, regard_vacations) !== null;
+ }
+
+ /**
+ * Returns the name of the holiday on the specified date.
+ *
+ * @param date The date for which to get the holiday name.
+ *
+ * @param regard_vacations Whether to regard vacations in addition to holidays (true)
+ * or to just regard holidays (false). Defaults to false.
+ *
+ * @returns string The name of the holiday / vacation, if any.
+ * If there is no holiday on the specified date, the string is empty.
+ */
+ getHolidayName(date: Date, regard_vacations: boolean = false) : string {
+ const holiday = this.getHoliday(date, regard_vacations);
+ if (holiday) {
+ return holiday.name;
+ }
+ return '';
+ }
+}
+
+
+export {Holiday, HolidayCache};
+export const holiday_cache: HolidayCache = new HolidayCache();