diff --git a/setup.cfg b/setup.cfg index 0fecb67338..69cfdd3d45 100644 --- a/setup.cfg +++ b/setup.cfg @@ -98,8 +98,7 @@ install_requires = kerberos lazy-object-proxy ldap3 - # NOTE: Upper-bound Will be removed once OGC-2780 is merged - libres>=0.9.0,<=0.10.0 + libres>=0.10.0 libsass lingua-language-detector lxml diff --git a/src/onegov/org/assets/js/locale.js b/src/onegov/org/assets/js/locale.js index 5771369e55..84ab2c1e5a 100644 --- a/src/onegov/org/assets/js/locale.js +++ b/src/onegov/org/assets/js/locale.js @@ -1,7 +1,12 @@ var locales = { de: { "Allocation": "Verfügbarkeit", + "Blocker": "Sperrzeit", + "Reason": "Grund für Sperrung", "Add": "Hinzufügen", + "Update": "Aktualisieren", + "Delete": "Löschen", + "Failed to delete": "Löschen fehlgeschlagen", "Count": "Anzahl", "Dates": "Termine", "From": "Von", @@ -34,7 +39,12 @@ var locales = { }, fr: { "Allocation": "Allocation", + "Blocker": "Bloqueur", + "Reason": "Raison", "Add": "Ajouter", + "Update": "Actualiser", + "Delete": "Supprimer", + "Failed to delete": "Impossible de supprimer", "Count": "Nombre", "Dates": "Dates", "From": "De", @@ -67,7 +77,12 @@ var locales = { }, it: { "Allocation": "Allocazione", + "Blocker": "Bloccante", + "Reason": "Motivo", "Add": "Aggiungi", + "Update": "Aggiorna", + "Delete": "Elimina", + "Failed to delete": "Impossibile eliminare", "Count": "Conta", "Dates": "Date", "From": "A partire dal", diff --git a/src/onegov/org/assets/js/occupancycalendar.jsx b/src/onegov/org/assets/js/occupancycalendar.jsx index 730526f378..fd97780c9c 100644 --- a/src/onegov/org/assets/js/occupancycalendar.jsx +++ b/src/onegov/org/assets/js/occupancycalendar.jsx @@ -53,6 +53,8 @@ oc.events = [ 'oc-reservations-changed' ]; +oc.overlappingEvents = {}; + oc.passEventsToCalendar = function(calendar, target) { var cal = $(calendar); @@ -77,7 +79,7 @@ oc.getFullcalendarOptions = function(ocExtendOptions) { snapDuration: '00:15', editable: ocOptions.editable, eventResizableFromStart: ocOptions.editable, - selectable: false, + selectable: ocOptions.editable, initialView: ocOptions.view, locale: window.locale.language, multiMonthMaxColumns: 1 @@ -143,21 +145,81 @@ oc.getFullcalendarOptions = function(ocExtendOptions) { // implements editing if (ocOptions.editable) { + // add blockers on selection + fcOptions.selectMirror = true; + fcOptions.unselectCancel = '.popup'; + fcOptions.selectOverlap = function(event) { + if (event.display === 'background') { + oc.overlappingEvents[event.id] = event; + return true; + } else { + oc.overlappingEvents = {}; + return false; + } + }; + fcOptions.selectAllow = function(info) { + // we only know what to do if we overlap a single valid allocation + // we only allow to add blockers in the future + var keys = Object.keys(oc.overlappingEvents); + if ( + keys.length === 1 + && oc.overlappingEvents[keys[0]].extendedProps.blockable + && oc.overlappingEvents[keys[0]].extendedProps.blockurl + && info.start >= Date.now() + ) { + return true; + } else { + oc.overlappingEvents = {}; + return false; + } + } + // add blockers on selection + fcOptions.select = function(info) { + var keys = Object.keys(oc.overlappingEvents); + if (keys.length !== 1) { + // this shouldn't happen, but when it does just cancel + oc.overlappingEvents = {}; + info.view.calendar.unselect(); + return; + } + var event = oc.overlappingEvents[keys[0]]; + oc.overlappingEvents = {}; + var view = info.view; + var start = moment(info.start); + var end = moment(info.end); + var wholeDay = false; + if (view.type === "dayGridMonth" || view.type === "multiMonthYear") { + end = end.subtract(1, 'days'); + wholeDay = true; + } + oc.showBlockerPopup(view.calendar, $(view.calendar.el).find('.event-' + event.id).get(0) || view.calendar.el, start, end, wholeDay, event); + } + + // edit blocker reason on click + fcOptions.eventClick = function(info) { + if (info.event.extendedProps.kind !== 'blocker') { + return; + } + if (!info.event.extendedProps.seturl) { + return; + } + oc.showBlockerEditPopup(info.view.calendar, info.el, info.event); + }; + // edit events on drag&drop, resize fcOptions.eventOverlap = function(stillEvent, _movingEvent) { return stillEvent.display === 'background'; }; - // edit events on drag&drop, resize fcOptions.eventDrop = fcOptions.eventResize = function(info) { var event = info.event; var url = new Url(event.extendedProps.editurl); url.query.start = event.startStr; url.query.end = event.endStr; - var calendar = $(info.el).closest('.fc'); + var calendar = $(info.el).closest('.fc').get(0) || $('.fc').get(0); oc.post(calendar, url.toString(), function(_evt, _elt, _status, str, _xhr) { info.revert(); - oc.showErrorPopup(calendar, calendar.find('.event-' + event.id), str); + oc.showErrorPopup(calendar, $(calendar).find('.event-' + event.id), str); }); }; @@ -170,6 +232,12 @@ oc.getFullcalendarOptions = function(ocExtendOptions) { // after event rendering options.eventRenderers.push(oc.highlightEvents); options.eventRenderers.push(oc.addEventBackground); + options.eventRenderers.push(oc.addDeleteBlockerHandler); + + // add id to class names so we can easily find the element + fcOptions.eventClassNames = function(info) { + return 'event-' + info.event.id; + } // render additional content lines fcOptions.eventContent = function(info, h) { @@ -177,6 +245,14 @@ oc.getFullcalendarOptions = function(ocExtendOptions) { if (event.display === 'background') { return null; } + if (event.extendedProps.kind === 'blocker') { + return h('div', {title: event.title}, [ + event.title, + h('div', {class: 'delete-blocker', title: locale('Delete')}, [ + h('i', {class: 'fa fas fa-times'}) + ]) + ]); + } var lines = event.title.split('\n'); var attrs = {class: 'fc-title'}; // truncate title when it doesn't fit @@ -401,7 +477,7 @@ oc.setupViewNavigation = function(calendar, element, views, pdf_url) { } ); - rc.showPopup(calendar, pdf_btn, wrapper); + oc.showPopup(calendar, pdf_btn, wrapper); }); pdf_btn.appendTo(view_group); @@ -431,7 +507,7 @@ oc.highlightEvents = function(event, element, view) { // adds a fc-bg div to the views where we need it oc.addEventBackground = function(event, element, view) { - if (event.display === 'background') { + if (event.extendedProps.kind !== 'reservation') { return; } @@ -441,9 +517,29 @@ oc.addEventBackground = function(event, element, view) { $('
').insertAfter($('.fc-content', element)); }; +// adds a click handler to the delete button +oc.addDeleteBlockerHandler = function(event, element, view) { + if (event.extendedProps.kind !== 'blocker' || !event.extendedProps.deleteurl) { + return; + } + + $(element).find('.delete-blocker').click(function(ev) { + ev.stopPropagation(); + $.ajax( + event.extendedProps.deleteurl, + {method: 'DELETE'} + ).done(function() { + view.calendar.refetchEvents(); + }).fail(function() { + oc.showErrorPopup($(element).closest('.fc'), element, locale('Failed to delete')); + }); + }); +}; + oc.setupReservationsRefetch = function(calendar) { $(window).on('oc-reservations-changed', function() { calendar.refetchEvents(); + calendar.unselect(); }); }; @@ -479,6 +575,74 @@ oc.post = function(calendar, url, onerror) { oc.request(calendar, url, 'ic-post-to', onerror); }; +oc.add_blocker = function(calendar, url, start, end, reason, wholeDay) { + url = new Url(url); + url.query.start = start; + url.query.end = end; + url.query.reason = reason; + url.query.whole_day = wholeDay && '1' || '0'; + + oc.post(calendar, url.toString()); +}; + +oc.edit_blocker = function(calendar, url, reason) { + url = new Url(url); + url.query.reason = reason; + + oc.post(calendar, url.toString()); +}; + +// popup handler implementation +oc.showBlockerPopup = function(calendar, element, start, end, wholeDay, event) { + var wrapper = $('