From 483cfc27882a02c822d1c7453090ac3f81bf40d8 Mon Sep 17 00:00:00 2001 From: Andreas Rosenthal Date: Thu, 7 May 2026 13:52:13 +0200 Subject: [PATCH 001/110] feat: complete calendar workflow package Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 11 + amd/build/available_calendar.min.js | 2 +- amd/build/available_calendar.min.js.map | 2 +- amd/build/calendar.min.js | 2 +- amd/build/calendar.min.js.map | 2 +- amd/build/export_modal.min.js | 2 +- amd/build/export_modal.min.js.map | 2 +- .../overview/booking_status_dropdown.min.js | 2 +- .../booking_status_dropdown.min.js.map | 2 +- amd/src/available_calendar.js | 4 +- amd/src/calendar.js | 7 +- amd/src/export_modal.js | 74 +++- amd/src/overview/booking_status_dropdown.js | 29 +- classes/external/get_possible_starttimes.php | 18 + .../external/update_event_booking_status.php | 26 +- classes/form/edit_event_form.php | 376 +++++++++++++----- classes/local/entity/bookit_event.php | 61 ++- .../manager/booking_notification_manager.php | 319 +++++++++++++++ .../local/manager/event_access_manager.php | 305 +++++++++++++- classes/local/manager/event_manager.php | 238 ++++++++++- db/messages.php | 6 + db/services.php | 2 +- events.php | 26 ++ export_events.php | 188 ++++++++- lang/de/bookit.php | 81 +++- lang/en/bookit.php | 79 ++++ overview.php | 220 ++++++++-- settings.php | 61 +++ styles.css | 141 +++++++ templates/view/examiner_overview.mustache | 157 +++++++- templates/view/export_events_modal.mustache | 31 +- tests/behat/behat_mod_bookit.php | 350 ++++++++++++++++ tests/behat/export_modal.feature | 26 ++ tests/behat/open_requests.feature | 50 +++ tests/behat/reporting_and_past_date.feature | 52 +++ tests/behat/role_based_access.feature | 63 +++ .../external/get_possible_starttimes_test.php | 50 +++ tests/generator/lib.php | 62 ++- .../booking_notification_manager_test.php | 145 +++++++ .../manager/event_access_manager_test.php | 252 ++++++++++++ tests/local/manager/event_manager_test.php | 59 +++ thirdpartylibs.xml | 2 +- .../event-calendar/event-calendar.min.css | 7 +- .../event-calendar/event-calendar.min.js | 11 +- .../event-calendar.min.js.map.json | 1 + view.php | 2 +- 46 files changed, 3342 insertions(+), 266 deletions(-) create mode 100644 .gitignore create mode 100644 classes/local/manager/booking_notification_manager.php create mode 100644 tests/behat/export_modal.feature create mode 100644 tests/behat/open_requests.feature create mode 100644 tests/behat/reporting_and_past_date.feature create mode 100644 tests/behat/role_based_access.feature create mode 100644 tests/external/get_possible_starttimes_test.php create mode 100644 tests/local/manager/booking_notification_manager_test.php create mode 100644 tests/local/manager/event_access_manager_test.php create mode 100644 tests/local/manager/event_manager_test.php create mode 100644 thirdpartylibs/event-calendar/event-calendar.min.js.map.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fe7a0b62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +Thumbs.db +*.log +*.tmp +*.swp +*.swo +.env* +.idea/ +.vscode/ +node_modules/ +vendor/ diff --git a/amd/build/available_calendar.min.js b/amd/build/available_calendar.min.js index 66a545a9..2cfa2e9c 100644 --- a/amd/build/available_calendar.min.js +++ b/amd/build/available_calendar.min.js @@ -6,6 +6,6 @@ define("mod_bookit/available_calendar",["exports","core/ajax","core/str","core/m * @copyright 2024 Melanie Treitinger, Justus Dieckmann RUB * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -async function(eventsource,capabilities,lang){await(0,_calendar.theGlobalProperty)("EventCalendar");(0,_prefetch.prefetchStrings)("mod_bookit",["add_blocker"]),(0,_prefetch.prefetchStrings)("core",["today","month","week"]),(0,_prefetch.prefetchStrings)("calendar",["day","upcomingevents"]);const strrequestbooking=await(0,_str.getString)("add_blocker","mod_bookit"),strtoday=await(0,_str.getString)("today"),strmonth=await(0,_str.getString)("month"),strweek=await(0,_str.getString)("week"),strday=await(0,_str.getString)("day","calendar"),strlist=await(0,_str.getString)("upcomingevents","calendar");let viewType="timeGridWeek";window.screen.width<=1e3&&(viewType="listWeek");var calendar;calendar=new window.EventCalendar(document.getElementById("ec"),{locale:lang,view:viewType,firstDay:1,scrollTime:"09:00:00",slotMinTime:"07:00:00",dayMaxEvents:!0,nowIndicator:!0,selectable:!1,eventBackgroundColor:"#035AA3",eventStartEditable:!1,eventDurationEditable:!1,buttonText:function(text){return text.today=strtoday,text.dayGridMonth=strmonth,text.timeGridWeek=strweek,text.timeGridDay=strday,text.listWeek=strlist,text},customButtons:{addButton:{text:strrequestbooking,click:function(){const modalForm=new _modalform.default({formClass:"mod_bookit\\local\\form\\edit_blocker_form",args:{},modalConfig:{title:(0,_str.getString)("add_blocker","mod_bookit")}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.show()}}},dateClick:function(info){let d=new Date,dateoff=new Date(d.setMinutes(d.getMinutes()-d.getTimezoneOffset())),startdate=info.dateStr;if(startdate>dateoff.toISOString()){const modalForm=new _modalform.default({formClass:"mod_bookit\\local\\form\\edit_blocker_form",args:{startdate:startdate},modalConfig:{title:(0,_str.getString)("add_blocker","mod_bookit")}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.show()}},eventClick:function(info){if("blocker"===info.event.extendedProps.type){const modalForm=new _modalform.default({formClass:"mod_bookit\\local\\form\\edit_blocker_form",args:{id:info.event.id},modalConfig:{title:(0,_str.getString)("edit_blocker","mod_bookit")},moduleName:"mod_bookit/modal_delete_save_cancel"});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{modalForm.modal.getRoot().on(_modal_events.default.delete,(async()=>{var blockerId;await(blockerId=info.event.id,Ajax.call([{methodname:"mod_bookit_delete_blocker",args:{blockerid:blockerId}}])[0]),calendar.refetchEvents()}))})),modalForm.show()}},headerToolbar:{start:"prev, next, today, addButton",center:"title",end:"dayGridMonth,timeGridWeek,timeGridDay,listWeek"},resources:[],eventSources:[{url:eventsource}],views:{timeGridWeek:{pointer:!0},resourceTimeGridWeek:{pointer:!0},resourceTimelineWeek:{pointer:!0,slotMinTime:"09:00",slotMaxTime:"21:00",slotWidth:80,resources:[]}}})},Ajax=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Ajax),_modal_events=_interopRequireDefault(_modal_events),_modalform=_interopRequireDefault(_modalform)})); +async function(eventsource,capabilities,lang){await(0,_calendar.theGlobalProperty)("EventCalendar");(0,_prefetch.prefetchStrings)("mod_bookit",["add_blocker"]),(0,_prefetch.prefetchStrings)("core",["today","month","week"]),(0,_prefetch.prefetchStrings)("calendar",["day","upcomingevents"]);const strrequestbooking=await(0,_str.getString)("add_blocker","mod_bookit"),strtoday=await(0,_str.getString)("today"),strmonth=await(0,_str.getString)("month"),strweek=await(0,_str.getString)("week"),strday=await(0,_str.getString)("day","calendar"),strlist=await(0,_str.getString)("upcomingevents","calendar");let viewType="timeGridWeek";window.screen.width<=1e3&&(viewType="listWeek");var calendar;calendar=window.EventCalendar.create(document.getElementById("ec"),{locale:lang,view:viewType,firstDay:1,scrollTime:"09:00:00",slotMinTime:"07:00:00",dayMaxEvents:!0,nowIndicator:!0,selectable:!1,eventBackgroundColor:"#035AA3",eventStartEditable:!1,eventDurationEditable:!1,buttonText:function(text){return text.today=strtoday,text.dayGridMonth=strmonth,text.timeGridWeek=strweek,text.timeGridDay=strday,text.listWeek=strlist,text},customButtons:{addButton:{text:strrequestbooking,click:function(){const modalForm=new _modalform.default({formClass:"mod_bookit\\local\\form\\edit_blocker_form",args:{},modalConfig:{title:(0,_str.getString)("add_blocker","mod_bookit")}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.show()}}},dateClick:function(info){let d=new Date,dateoff=new Date(d.setMinutes(d.getMinutes()-d.getTimezoneOffset())),startdate=info.dateStr;if(startdate>dateoff.toISOString()){const modalForm=new _modalform.default({formClass:"mod_bookit\\local\\form\\edit_blocker_form",args:{startdate:startdate},modalConfig:{title:(0,_str.getString)("add_blocker","mod_bookit")}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.show()}},eventClick:function(info){if("blocker"===info.event.extendedProps.type){const modalForm=new _modalform.default({formClass:"mod_bookit\\local\\form\\edit_blocker_form",args:{id:info.event.id},modalConfig:{title:(0,_str.getString)("edit_blocker","mod_bookit")},moduleName:"mod_bookit/modal_delete_save_cancel"});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{modalForm.modal.getRoot().on(_modal_events.default.delete,(async()=>{var blockerId;await(blockerId=info.event.id,Ajax.call([{methodname:"mod_bookit_delete_blocker",args:{blockerid:blockerId}}])[0]),calendar.refetchEvents()}))})),modalForm.show()}},headerToolbar:{start:"prev, next, today, addButton",center:"title",end:"dayGridMonth,timeGridWeek,timeGridDay,listWeek"},resources:[],eventSources:[{url:eventsource}],views:{timeGridWeek:{pointer:!0},resourceTimeGridWeek:{pointer:!0},resourceTimelineWeek:{pointer:!0,slotMinTime:"09:00",slotMaxTime:"21:00",slotWidth:80,resources:[]}}})},Ajax=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Ajax),_modal_events=_interopRequireDefault(_modal_events),_modalform=_interopRequireDefault(_modalform)})); //# sourceMappingURL=available_calendar.min.js.map \ No newline at end of file diff --git a/amd/build/available_calendar.min.js.map b/amd/build/available_calendar.min.js.map index 95f3b048..7c543f79 100644 --- a/amd/build/available_calendar.min.js.map +++ b/amd/build/available_calendar.min.js.map @@ -1 +1 @@ -{"version":3,"file":"available_calendar.min.js","sources":["../src/available_calendar.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the calendar\n *\n * @module mod_bookit/available_calendar\n * @copyright 2024 Melanie Treitinger, Justus Dieckmann RUB\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Ajax from 'core/ajax';\nimport {getString} from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalForm from 'core_form/modalform';\nimport {prefetchStrings} from 'core/prefetch';\nimport {theGlobalProperty} from \"mod_bookit/calendar\";\n\n/**\n * Initializes the calendar.\n * @param {string} eventsource\n * @param {array} capabilities\n * @param {string} lang\n * @returns {Promise}\n */\nexport async function init(eventsource, capabilities, lang) {\n await theGlobalProperty('EventCalendar');\n\n // Define toolbarbuttons.\n const toolbarbuttons = 'prev, next, today, addButton';\n\n // String variables.\n prefetchStrings('mod_bookit', ['add_blocker']);\n prefetchStrings('core', ['today', 'month', 'week']);\n prefetchStrings('calendar', ['day', 'upcomingevents']);\n const strrequestbooking = await getString('add_blocker', 'mod_bookit');\n const strtoday = await getString('today');\n const strmonth = await getString('month');\n const strweek = await getString('week');\n const strday = await getString('day', 'calendar');\n const strlist = await getString('upcomingevents', 'calendar');\n\n // Define viewtype.\n let viewType = 'timeGridWeek';\n if (window.screen.width <= 1000) {\n viewType = 'listWeek';\n }\n\n var calendar;\n\n calendar = new window.EventCalendar(document.getElementById('ec'), {\n locale: lang,\n view: viewType,\n firstDay: 1,\n scrollTime: '09:00:00',\n slotMinTime: '07:00:00',\n dayMaxEvents: true,\n nowIndicator: true,\n selectable: false,\n eventBackgroundColor: '#035AA3',\n eventStartEditable: false,\n eventDurationEditable: false,\n buttonText: function(text) {\n text.today = strtoday;\n text.dayGridMonth = strmonth;\n text.timeGridWeek = strweek;\n text.timeGridDay = strday;\n text.listWeek = strlist;\n return text;\n },\n customButtons: {\n addButton: {\n text: strrequestbooking,\n click: function() {\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\local\\\\form\\\\edit_blocker_form\",\n args: {\n },\n modalConfig: {title: getString('add_blocker', 'mod_bookit')},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.show();\n }\n }\n },\n dateClick: function(info) {\n let d = new Date();\n let dateoff = new Date(d.setMinutes(d.getMinutes() - d.getTimezoneOffset()));\n let startdate = info.dateStr;\n if (startdate > dateoff.toISOString()) {\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\local\\\\form\\\\edit_blocker_form\",\n args: {\n startdate: startdate,\n },\n modalConfig: {title: getString('add_blocker', 'mod_bookit')},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.show();\n }\n },\n eventClick: function(info) {\n if (info.event.extendedProps.type === 'blocker') {\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\local\\\\form\\\\edit_blocker_form\",\n args: {\n id: info.event.id,\n },\n modalConfig: {\n title: getString('edit_blocker', 'mod_bookit'),\n },\n moduleName: 'mod_bookit/modal_delete_save_cancel',\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n modalForm.modal.getRoot().on(ModalEvents.delete, async() => {\n await deleteBlocker(info.event.id);\n calendar.refetchEvents();\n });\n });\n modalForm.show();\n }\n },\n headerToolbar: {\n start: toolbarbuttons,\n center: 'title',\n end: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'\n },\n resources: [],\n eventSources: [\n {\n url: eventsource,\n },\n ],\n views: {\n timeGridWeek: {pointer: true},\n resourceTimeGridWeek: {pointer: true},\n resourceTimelineWeek: {\n pointer: true,\n slotMinTime: '09:00',\n slotMaxTime: '21:00',\n slotWidth: 80,\n resources: []\n }\n },\n });\n}\n\n/**\n * Deletes a blocker.\n *\n * @param {int} blockerId\n * @returns Promise\n */\nfunction deleteBlocker(blockerId) {\n return Ajax.call([{\n methodname: 'mod_bookit_delete_blocker',\n args: {\n blockerid: blockerId\n }\n }])[0];\n}"],"names":["eventsource","capabilities","lang","strrequestbooking","strtoday","strmonth","strweek","strday","strlist","viewType","window","screen","width","calendar","EventCalendar","document","getElementById","locale","view","firstDay","scrollTime","slotMinTime","dayMaxEvents","nowIndicator","selectable","eventBackgroundColor","eventStartEditable","eventDurationEditable","buttonText","text","today","dayGridMonth","timeGridWeek","timeGridDay","listWeek","customButtons","addButton","click","modalForm","ModalForm","formClass","args","modalConfig","title","addEventListener","events","FORM_SUBMITTED","refetchEvents","show","dateClick","info","d","Date","dateoff","setMinutes","getMinutes","getTimezoneOffset","startdate","dateStr","toISOString","eventClick","event","extendedProps","type","id","moduleName","LOADED","modal","getRoot","on","ModalEvents","delete","async","blockerId","Ajax","call","methodname","blockerid","headerToolbar","start","center","end","resources","eventSources","url","views","pointer","resourceTimeGridWeek","resourceTimelineWeek","slotMaxTime","slotWidth"],"mappings":";;;;;;;;eAqC2BA,YAAaC,aAAcC,YAC5C,+BAAkB,+CAMR,aAAc,CAAC,8CACf,OAAQ,CAAC,QAAS,QAAS,uCAC3B,WAAY,CAAC,MAAO,yBAC9BC,wBAA0B,kBAAU,cAAe,cACnDC,eAAiB,kBAAU,SAC3BC,eAAiB,kBAAU,SAC3BC,cAAgB,kBAAU,QAC1BC,aAAe,kBAAU,MAAO,YAChCC,cAAgB,kBAAU,iBAAkB,gBAG9CC,SAAW,eACXC,OAAOC,OAAOC,OAAS,MACvBH,SAAW,gBAGXI,SAEJA,SAAW,IAAIH,OAAOI,cAAcC,SAASC,eAAe,MAAO,CAC/DC,OAAQf,KACRgB,KAAMT,SACNU,SAAU,EACVC,WAAY,WACZC,YAAa,WACbC,cAAc,EACdC,cAAc,EACdC,YAAY,EACZC,qBAAsB,UACtBC,oBAAoB,EACpBC,uBAAuB,EACvBC,WAAY,SAASC,aACjBA,KAAKC,MAAQ1B,SACbyB,KAAKE,aAAe1B,SACpBwB,KAAKG,aAAe1B,QACpBuB,KAAKI,YAAc1B,OACnBsB,KAAKK,SAAW1B,QACTqB,MAEXM,cAAe,CACXC,UAAW,CACPP,KAAM1B,kBACNkC,MAAO,iBACGC,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,6CACXC,KAAM,GAENC,YAAa,CAACC,OAAO,kBAAU,cAAe,iBAElDL,UAAUM,iBAAiBN,UAAUO,OAAOC,gBAAgB,KACxDjC,SAASkC,mBAEbT,UAAUU,UAItBC,UAAW,SAASC,UACZC,EAAI,IAAIC,KACRC,QAAU,IAAID,KAAKD,EAAEG,WAAWH,EAAEI,aAAeJ,EAAEK,sBACnDC,UAAYP,KAAKQ,WACjBD,UAAYJ,QAAQM,cAAe,OAC7BrB,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,6CACXC,KAAM,CACFgB,UAAWA,WAEff,YAAa,CAACC,OAAO,kBAAU,cAAe,iBAElDL,UAAUM,iBAAiBN,UAAUO,OAAOC,gBAAgB,KACxDjC,SAASkC,mBAEbT,UAAUU,SAGlBY,WAAY,SAASV,SACqB,YAAlCA,KAAKW,MAAMC,cAAcC,KAAoB,OACvCzB,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,6CACXC,KAAM,CACFuB,GAAId,KAAKW,MAAMG,IAEnBtB,YAAa,CACTC,OAAO,kBAAU,eAAgB,eAErCsB,WAAY,wCAEhB3B,UAAUM,iBAAiBN,UAAUO,OAAOC,gBAAgB,KACxDjC,SAASkC,mBAEbT,UAAUM,iBAAiBN,UAAUO,OAAOqB,QAAQ,KAChD5B,UAAU6B,MAAMC,UAAUC,GAAGC,sBAAYC,QAAQC,cAuC9CC,gBAAAA,UAtCqBvB,KAAKW,MAAMG,GAuC5CU,KAAKC,KAAK,CAAC,CACdC,WAAY,4BACZnC,KAAM,CACFoC,UAAWJ,cAEf,IA3CgB5D,SAASkC,sBAGjBT,UAAUU,SAGlB8B,cAAe,CACXC,MArGe,+BAsGfC,OAAQ,QACRC,IAAK,kDAETC,UAAW,GACXC,aAAc,CACV,CACIC,IAAKpF,cAGbqF,MAAO,CACHrD,aAAc,CAACsD,SAAS,GACxBC,qBAAsB,CAACD,SAAS,GAChCE,qBAAsB,CAClBF,SAAS,EACTjE,YAAa,QACboE,YAAa,QACbC,UAAW,GACXR,UAAW"} \ No newline at end of file +{"version":3,"file":"available_calendar.min.js","sources":["../src/available_calendar.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the calendar\n *\n * @module mod_bookit/available_calendar\n * @copyright 2024 Melanie Treitinger, Justus Dieckmann RUB\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Ajax from 'core/ajax';\nimport {getString} from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalForm from 'core_form/modalform';\nimport {prefetchStrings} from 'core/prefetch';\nimport {theGlobalProperty} from \"mod_bookit/calendar\";\n\n/**\n * Initializes the calendar.\n * @param {string} eventsource\n * @param {array} capabilities\n * @param {string} lang\n * @returns {Promise}\n */\nexport async function init(eventsource, capabilities, lang) {\n await theGlobalProperty('EventCalendar');\n\n // Define toolbarbuttons.\n const toolbarbuttons = 'prev, next, today, addButton';\n\n // String variables.\n prefetchStrings('mod_bookit', ['add_blocker']);\n prefetchStrings('core', ['today', 'month', 'week']);\n prefetchStrings('calendar', ['day', 'upcomingevents']);\n const strrequestbooking = await getString('add_blocker', 'mod_bookit');\n const strtoday = await getString('today');\n const strmonth = await getString('month');\n const strweek = await getString('week');\n const strday = await getString('day', 'calendar');\n const strlist = await getString('upcomingevents', 'calendar');\n\n // Define viewtype.\n let viewType = 'timeGridWeek';\n if (window.screen.width <= 1000) {\n viewType = 'listWeek';\n }\n\n var calendar;\n\n calendar = window.EventCalendar.create(document.getElementById('ec'), {\n locale: lang,\n view: viewType,\n firstDay: 1,\n scrollTime: '09:00:00',\n slotMinTime: '07:00:00',\n dayMaxEvents: true,\n nowIndicator: true,\n selectable: false,\n eventBackgroundColor: '#035AA3',\n eventStartEditable: false,\n eventDurationEditable: false,\n buttonText: function(text) {\n text.today = strtoday;\n text.dayGridMonth = strmonth;\n text.timeGridWeek = strweek;\n text.timeGridDay = strday;\n text.listWeek = strlist;\n return text;\n },\n customButtons: {\n addButton: {\n text: strrequestbooking,\n click: function() {\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\local\\\\form\\\\edit_blocker_form\",\n args: {\n },\n modalConfig: {title: getString('add_blocker', 'mod_bookit')},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.show();\n }\n }\n },\n dateClick: function(info) {\n let d = new Date();\n let dateoff = new Date(d.setMinutes(d.getMinutes() - d.getTimezoneOffset()));\n let startdate = info.dateStr;\n if (startdate > dateoff.toISOString()) {\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\local\\\\form\\\\edit_blocker_form\",\n args: {\n startdate: startdate,\n },\n modalConfig: {title: getString('add_blocker', 'mod_bookit')},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.show();\n }\n },\n eventClick: function(info) {\n if (info.event.extendedProps.type === 'blocker') {\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\local\\\\form\\\\edit_blocker_form\",\n args: {\n id: info.event.id,\n },\n modalConfig: {\n title: getString('edit_blocker', 'mod_bookit'),\n },\n moduleName: 'mod_bookit/modal_delete_save_cancel',\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n modalForm.modal.getRoot().on(ModalEvents.delete, async() => {\n await deleteBlocker(info.event.id);\n calendar.refetchEvents();\n });\n });\n modalForm.show();\n }\n },\n headerToolbar: {\n start: toolbarbuttons,\n center: 'title',\n end: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'\n },\n resources: [],\n eventSources: [\n {\n url: eventsource,\n },\n ],\n views: {\n timeGridWeek: {pointer: true},\n resourceTimeGridWeek: {pointer: true},\n resourceTimelineWeek: {\n pointer: true,\n slotMinTime: '09:00',\n slotMaxTime: '21:00',\n slotWidth: 80,\n resources: []\n }\n },\n });\n}\n\n/**\n * Deletes a blocker.\n *\n * @param {int} blockerId\n * @returns Promise\n */\nfunction deleteBlocker(blockerId) {\n return Ajax.call([{\n methodname: 'mod_bookit_delete_blocker',\n args: {\n blockerid: blockerId\n }\n }])[0];\n}\n"],"names":["eventsource","capabilities","lang","strrequestbooking","strtoday","strmonth","strweek","strday","strlist","viewType","window","screen","width","calendar","EventCalendar","create","document","getElementById","locale","view","firstDay","scrollTime","slotMinTime","dayMaxEvents","nowIndicator","selectable","eventBackgroundColor","eventStartEditable","eventDurationEditable","buttonText","text","today","dayGridMonth","timeGridWeek","timeGridDay","listWeek","customButtons","addButton","click","modalForm","ModalForm","formClass","args","modalConfig","title","addEventListener","events","FORM_SUBMITTED","refetchEvents","show","dateClick","info","d","Date","dateoff","setMinutes","getMinutes","getTimezoneOffset","startdate","dateStr","toISOString","eventClick","event","extendedProps","type","id","moduleName","LOADED","modal","getRoot","on","ModalEvents","delete","async","blockerId","Ajax","call","methodname","blockerid","headerToolbar","start","center","end","resources","eventSources","url","views","pointer","resourceTimeGridWeek","resourceTimelineWeek","slotMaxTime","slotWidth"],"mappings":";;;;;;;;eAqC2BA,YAAaC,aAAcC,YAC5C,+BAAkB,+CAMR,aAAc,CAAC,8CACf,OAAQ,CAAC,QAAS,QAAS,uCAC3B,WAAY,CAAC,MAAO,yBAC9BC,wBAA0B,kBAAU,cAAe,cACnDC,eAAiB,kBAAU,SAC3BC,eAAiB,kBAAU,SAC3BC,cAAgB,kBAAU,QAC1BC,aAAe,kBAAU,MAAO,YAChCC,cAAgB,kBAAU,iBAAkB,gBAG9CC,SAAW,eACXC,OAAOC,OAAOC,OAAS,MACvBH,SAAW,gBAGXI,SAEJA,SAAWH,OAAOI,cAAcC,OAAOC,SAASC,eAAe,MAAO,CAClEC,OAAQhB,KACRiB,KAAMV,SACNW,SAAU,EACVC,WAAY,WACZC,YAAa,WACbC,cAAc,EACdC,cAAc,EACdC,YAAY,EACZC,qBAAsB,UACtBC,oBAAoB,EACpBC,uBAAuB,EACvBC,WAAY,SAASC,aACjBA,KAAKC,MAAQ3B,SACb0B,KAAKE,aAAe3B,SACpByB,KAAKG,aAAe3B,QACpBwB,KAAKI,YAAc3B,OACnBuB,KAAKK,SAAW3B,QACTsB,MAEXM,cAAe,CACXC,UAAW,CACPP,KAAM3B,kBACNmC,MAAO,iBACGC,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,6CACXC,KAAM,GAENC,YAAa,CAACC,OAAO,kBAAU,cAAe,iBAElDL,UAAUM,iBAAiBN,UAAUO,OAAOC,gBAAgB,KACxDlC,SAASmC,mBAEbT,UAAUU,UAItBC,UAAW,SAASC,UACZC,EAAI,IAAIC,KACRC,QAAU,IAAID,KAAKD,EAAEG,WAAWH,EAAEI,aAAeJ,EAAEK,sBACnDC,UAAYP,KAAKQ,WACjBD,UAAYJ,QAAQM,cAAe,OAC7BrB,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,6CACXC,KAAM,CACFgB,UAAWA,WAEff,YAAa,CAACC,OAAO,kBAAU,cAAe,iBAElDL,UAAUM,iBAAiBN,UAAUO,OAAOC,gBAAgB,KACxDlC,SAASmC,mBAEbT,UAAUU,SAGlBY,WAAY,SAASV,SACqB,YAAlCA,KAAKW,MAAMC,cAAcC,KAAoB,OACvCzB,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,6CACXC,KAAM,CACFuB,GAAId,KAAKW,MAAMG,IAEnBtB,YAAa,CACTC,OAAO,kBAAU,eAAgB,eAErCsB,WAAY,wCAEhB3B,UAAUM,iBAAiBN,UAAUO,OAAOC,gBAAgB,KACxDlC,SAASmC,mBAEbT,UAAUM,iBAAiBN,UAAUO,OAAOqB,QAAQ,KAChD5B,UAAU6B,MAAMC,UAAUC,GAAGC,sBAAYC,QAAQC,cAuC9CC,gBAAAA,UAtCqBvB,KAAKW,MAAMG,GAuC5CU,KAAKC,KAAK,CAAC,CACdC,WAAY,4BACZnC,KAAM,CACFoC,UAAWJ,cAEf,IA3CgB7D,SAASmC,sBAGjBT,UAAUU,SAGlB8B,cAAe,CACXC,MArGe,+BAsGfC,OAAQ,QACRC,IAAK,kDAETC,UAAW,GACXC,aAAc,CACV,CACIC,IAAKrF,cAGbsF,MAAO,CACHrD,aAAc,CAACsD,SAAS,GACxBC,qBAAsB,CAACD,SAAS,GAChCE,qBAAsB,CAClBF,SAAS,EACTjE,YAAa,QACboE,YAAa,QACbC,UAAW,GACXR,UAAW"} \ No newline at end of file diff --git a/amd/build/calendar.min.js b/amd/build/calendar.min.js index 01cf5c1a..158db78f 100644 --- a/amd/build/calendar.min.js +++ b/amd/build/calendar.min.js @@ -5,6 +5,6 @@ define("mod_bookit/calendar",["exports","core/str","core_form/modalform","core/p * @module mod_bookit/calendar * @copyright 2024 Melanie Treitinger, Justus Dieckmann RUB * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=async function(cmid,eventsource,capabilities,lang,config){await theGlobalProperty("EventCalendar");let textcolor="#ffffff";Object.prototype.hasOwnProperty.call(config,"textcolor")&&(textcolor=config.textcolor);let toolbarbuttons="prev,next today";capabilities.addevent&&(toolbarbuttons="prev,next today addButton");await(0,_prefetch.prefetchStrings)("mod_bookit",["calendar_addbooking","calendar_editevent","calendar_eventlist"]),await(0,_prefetch.prefetchStrings)("core",["today","month","week"]),await(0,_prefetch.prefetchStrings)("calendar",["day"]);const strRequestBooking=await(0,_str.getString)("calendar_addbooking","mod_bookit"),editevent=await(0,_str.getString)("calendar_editevent","mod_bookit"),strToday=await(0,_str.getString)("today"),strMonth=await(0,_str.getString)("month"),strWeek=await(0,_str.getString)("week"),strDay=await(0,_str.getString)("day","calendar"),strList=await(0,_str.getString)("calendar_eventlist","bookit");let viewType="timeGridWeek";window.screen.width<=1e3&&(viewType="listWeek");const allowedWeekdays=window.M&&M.cfg&&Array.isArray(M.cfg.bookit_allowedweekdays)?M.cfg.bookit_allowedweekdays.map((x=>Number(x))):[1,2,3,4,5],hiddenDays=[0,1,2,3,4,5,6].filter((d=>!allowedWeekdays.includes(d)));let extraFilterParams={};const calendar=new window.EventCalendar(document.getElementById("ec"),{locale:lang,view:viewType,firstDay:1,weekends:allowedWeekdays.includes(0)||allowedWeekdays.includes(6),scrollTime:"09:00:00",slotMinTime:"07:00:00",dayMaxEvents:!1,nowIndicator:!0,hiddenDays:hiddenDays,selectable:!1,eventTextColor:textcolor,eventBackgroundColor:"#035AA3",eventStartEditable:!1,eventDurationEditable:!1,buttonText:function(text){return text.today=strToday,text.dayGridMonth=strMonth,text.timeGridWeek=strWeek,text.timeGridDay=strDay,text.listWeek=strList,text},eventsSet:function(events){events.length},customButtons:{addButton:{text:strRequestBooking,click:function(){const modalForm=new _modalform.default({formClass:"mod_bookit\\form\\edit_event_form",args:{cmid:cmid},modalConfig:{title:editevent}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{(0,_possible_slots_refresh.initPossibleStarttimesRefresh)(cmid),_booking_form_resources.default.init(modalForm.modal.getRoot()[0])})),modalForm.show()}}},dateClick:function(info){const weekday=info.date.getUTCDay();if(!allowedWeekdays.includes(weekday))return;let d=new Date,dateoff=new Date(d.setMinutes(d.getMinutes()-d.getTimezoneOffset())),startdate=info.dateStr;if(capabilities.addevent&&startdate>dateoff.toISOString()){const modalForm=new _modalform.default({formClass:"mod_bookit\\form\\edit_event_form",args:{cmid:cmid,timeclicked:startdate},modalConfig:{title:editevent}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{(0,_possible_slots_refresh.initPossibleStarttimesRefresh)(cmid),_booking_form_resources.default.init(modalForm.modal.getRoot()[0])})),modalForm.show()}},eventClick:function(info){let id=info.event.id;if(info.event.extendedProps.reserved)return;const modalForm=new _modalform.default({formClass:"mod_bookit\\form\\edit_event_form",args:{cmid:cmid,id:id},modalConfig:{title:editevent}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{(0,_possible_slots_refresh.initPossibleStarttimesRefresh)(cmid,id),_booking_form_resources.default.init(modalForm.modal.getRoot()[0])})),modalForm.show()},headerToolbar:{start:toolbarbuttons,center:"title",end:"dayGridMonth,timeGridWeek,timeGridDay,listWeek"},resources:[],eventSources:[{url:eventsource,extraParams:()=>extraFilterParams}],views:{timeGridWeek:{pointer:!0},resourceTimeGridWeek:{pointer:!0},resourceTimelineWeek:{pointer:!0,slotMinTime:"09:00",slotMaxTime:"21:00",slotWidth:80,resources:[]}}});window.bookitCalendar=calendar,window.bookitCalendarUpdate=function(){let paramObj=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};extraFilterParams=paramObj,window.currentFilterParams=extraFilterParams,calendar.refetchEvents()}},_exports.theGlobalProperty=void 0,_modalform=_interopRequireDefault(_modalform),_booking_form_resources=_interopRequireDefault(_booking_form_resources);const theGlobalProperty=globalPropertyName=>new Promise((resolve=>function loop(){window[globalPropertyName]?resolve():setTimeout(loop,20)}()));_exports.theGlobalProperty=theGlobalProperty})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=async function(cmid,eventsource,capabilities,lang,config){await theGlobalProperty("EventCalendar");let textcolor="#ffffff";Object.prototype.hasOwnProperty.call(config,"textcolor")&&(textcolor=config.textcolor);let toolbarbuttons="prev,next today";capabilities.addevent&&(toolbarbuttons="prev,next today addButton");await(0,_prefetch.prefetchStrings)("mod_bookit",["calendar_addbooking","calendar_editevent","calendar_eventlist"]),await(0,_prefetch.prefetchStrings)("core",["today","month","week"]),await(0,_prefetch.prefetchStrings)("calendar",["day"]);const strRequestBooking=await(0,_str.getString)("calendar_addbooking","mod_bookit"),editevent=await(0,_str.getString)("calendar_editevent","mod_bookit"),strToday=await(0,_str.getString)("today"),strMonth=await(0,_str.getString)("month"),strWeek=await(0,_str.getString)("week"),strDay=await(0,_str.getString)("day","calendar"),strList=await(0,_str.getString)("calendar_eventlist","bookit");let viewType="timeGridWeek";window.screen.width<=1e3&&(viewType="listWeek");const allowedWeekdays=window.M&&M.cfg&&Array.isArray(M.cfg.bookit_allowedweekdays)?M.cfg.bookit_allowedweekdays.map((x=>Number(x))):[1,2,3,4,5],hiddenDays=[0,1,2,3,4,5,6].filter((d=>!allowedWeekdays.includes(d)));let extraFilterParams={};const calendar=window.EventCalendar.create(document.getElementById("ec"),{locale:lang,view:viewType,firstDay:1,weekends:allowedWeekdays.includes(0)||allowedWeekdays.includes(6),scrollTime:"09:00:00",slotMinTime:"07:00:00",dayMaxEvents:!1,nowIndicator:!0,hiddenDays:hiddenDays,selectable:!1,eventTextColor:textcolor,eventBackgroundColor:"#035AA3",eventStartEditable:!1,eventDurationEditable:!1,buttonText:function(text){return text.today=strToday,text.dayGridMonth=strMonth,text.timeGridWeek=strWeek,text.timeGridDay=strDay,text.listWeek=strList,text},eventsSet:function(events){events.length},customButtons:{addButton:{text:strRequestBooking,click:function(){const modalForm=new _modalform.default({formClass:"mod_bookit\\form\\edit_event_form",args:{cmid:cmid},modalConfig:{title:editevent}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{(0,_possible_slots_refresh.initPossibleStarttimesRefresh)(cmid),_booking_form_resources.default.init(modalForm.modal.getRoot()[0])})),modalForm.show()}}},dateClick:function(info){const weekday=info.date.getUTCDay();if(!allowedWeekdays.includes(weekday))return;let startdate=info.dateStr;const isFutureSlot=info.date.getTime()>Date.now();if(capabilities.addevent&&isFutureSlot){const modalForm=new _modalform.default({formClass:"mod_bookit\\form\\edit_event_form",args:{cmid:cmid,timeclicked:startdate},modalConfig:{title:editevent}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{(0,_possible_slots_refresh.initPossibleStarttimesRefresh)(cmid),_booking_form_resources.default.init(modalForm.modal.getRoot()[0])})),modalForm.show()}},eventClick:function(info){let id=info.event.id;if(info.event.extendedProps.reserved)return;const modalForm=new _modalform.default({formClass:"mod_bookit\\form\\edit_event_form",args:{cmid:cmid,id:id},modalConfig:{title:editevent}});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(()=>{calendar.refetchEvents()})),modalForm.addEventListener(modalForm.events.LOADED,(()=>{(0,_possible_slots_refresh.initPossibleStarttimesRefresh)(cmid,id),_booking_form_resources.default.init(modalForm.modal.getRoot()[0])})),modalForm.show()},headerToolbar:{start:toolbarbuttons,center:"title",end:"dayGridMonth,timeGridWeek,timeGridDay,listWeek"},resources:[],eventSources:[{url:eventsource,extraParams:()=>extraFilterParams}],views:{timeGridWeek:{pointer:!0},resourceTimeGridWeek:{pointer:!0},resourceTimelineWeek:{pointer:!0,slotMinTime:"09:00",slotMaxTime:"21:00",slotWidth:80,resources:[]}}});window.bookitCalendar=calendar,window.bookitCalendarUpdate=function(){let paramObj=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};extraFilterParams=paramObj,window.currentFilterParams=extraFilterParams,calendar.refetchEvents()}},_exports.theGlobalProperty=void 0,_modalform=_interopRequireDefault(_modalform),_booking_form_resources=_interopRequireDefault(_booking_form_resources);const theGlobalProperty=globalPropertyName=>new Promise((resolve=>function loop(){window[globalPropertyName]?resolve():setTimeout(loop,20)}()));_exports.theGlobalProperty=theGlobalProperty})); //# sourceMappingURL=calendar.min.js.map \ No newline at end of file diff --git a/amd/build/calendar.min.js.map b/amd/build/calendar.min.js.map index 4ea9d297..07366f9f 100644 --- a/amd/build/calendar.min.js.map +++ b/amd/build/calendar.min.js.map @@ -1 +1 @@ -{"version":3,"file":"calendar.min.js","sources":["../src/calendar.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the calendar\n *\n * @module mod_bookit/calendar\n * @copyright 2024 Melanie Treitinger, Justus Dieckmann RUB\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString} from 'core/str';\nimport ModalForm from 'core_form/modalform';\nimport {prefetchStrings} from 'core/prefetch';\nimport {initPossibleStarttimesRefresh} from \"mod_bookit/possible_slots_refresh\";\nimport BookingFormResources from \"mod_bookit/booking_form_resources\";\n\n/**\n * Wait until a global property exists (EventCalendar is loaded asynchronously).\n *\n * @param {String} globalPropertyName\n * @returns {Promise}\n */\nexport const theGlobalProperty = (globalPropertyName) =>\n new Promise(resolve => (function loop() {\n if (!window[globalPropertyName]) {\n setTimeout(loop, 20);\n return;\n }\n resolve();\n })());\n\n/**\n * Initializes the calendar.\n * @param {Number} cmid Course-module id\n * @param {String} eventsource URL for JSON feed (events.php)\n * @param {Object} capabilities {addevent: Boolean}\n * @param {String} lang Current UI language code\n * @param {Object} config Extra config (e.g. {textcolor:'#fff'})\n */\nexport async function init(cmid, eventsource, capabilities, lang, config) {\n await theGlobalProperty('EventCalendar');\n\n // Set textcolor.\n let textcolor = '#ffffff';\n if (Object.prototype.hasOwnProperty.call(config, 'textcolor')) {\n textcolor = config.textcolor;\n }\n\n // Define toolbarbuttons.\n let toolbarbuttons = 'prev,next today';\n if (capabilities.addevent) {\n toolbarbuttons = 'prev,next today addButton';\n }\n\n // String variables\n await prefetchStrings('mod_bookit', ['calendar_addbooking', 'calendar_editevent', 'calendar_eventlist']);\n await prefetchStrings('core', ['today', 'month', 'week']);\n await prefetchStrings('calendar', ['day']);\n const strRequestBooking = await getString('calendar_addbooking', 'mod_bookit');\n const editevent = await getString('calendar_editevent', 'mod_bookit');\n const strToday = await getString('today');\n const strMonth = await getString('month');\n const strWeek = await getString('week');\n const strDay = await getString('day', 'calendar');\n const strList = await getString('calendar_eventlist', 'bookit');\n\n // Define viewtype\n let viewType = 'timeGridWeek';\n if (window.screen.width <= 1000) {\n viewType = 'listWeek';\n }\n // Weekday visibility from admin settings (injected by PHP)\n const allowedWeekdays = (window.M && M.cfg && Array.isArray(M.cfg.bookit_allowedweekdays))\n ? M.cfg.bookit_allowedweekdays.map(x => Number(x))\n : [1, 2, 3, 4, 5];\n\n const hiddenDays = [0, 1, 2, 3, 4, 5, 6].filter(d => !allowedWeekdays.includes(d));\n\n // Runtime filter parameters – mutable via bookitCalendarUpdate()\n let extraFilterParams = {}; // {room:123, status:2, faculty:'ENG', …}\n\n const calendar = new window.EventCalendar(document.getElementById('ec'), {\n /* Appearance / behaviour */\n locale: lang,\n view: viewType,\n firstDay: 1,\n weekends: allowedWeekdays.includes(0) || allowedWeekdays.includes(6),\n scrollTime: '09:00:00',\n slotMinTime: '07:00:00',\n dayMaxEvents: false,\n nowIndicator: true,\n hiddenDays: hiddenDays,\n selectable: false,\n eventTextColor: textcolor,\n eventBackgroundColor: '#035AA3',\n eventStartEditable: false,\n eventDurationEditable: false,\n buttonText: function(text) {\n text.today = strToday;\n text.dayGridMonth = strMonth;\n text.timeGridWeek = strWeek;\n text.timeGridDay = strDay;\n text.listWeek = strList;\n return text;\n },\n\n eventsSet: function(events) {\n // Comment console.log('[BookIT] eventsSet: received', events.length, 'events');\n if (events.length) {\n /* Comment\n console.log('[BookIT] sample event', {\n id: events[0].id,\n title: events[0].title,\n start: events[0].startStr || events[0].start,\n end: events[0].endStr || events[0].end,\n room: events[0].extendedProps?.room,\n department: events[0].extendedProps?.department,\n bookingstatus: events[0].extendedProps?.bookingstatus\n });\n */\n }\n },\n\n /* Custom toolbar button (“Add booking”) */\n customButtons: {\n addButton: {\n text: strRequestBooking,\n click: function() {\n const modalForm = new ModalForm({\n formClass: 'mod_bookit\\\\form\\\\edit_event_form',\n args: {\n cmid: cmid\n },\n modalConfig: {title: editevent},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n initPossibleStarttimesRefresh(cmid);\n BookingFormResources.init(modalForm.modal.getRoot()[0]);\n });\n modalForm.show();\n }\n }\n },\n\n /* Date click (create new event) */\n dateClick: function(info) {\n const weekday = info.date.getUTCDay(); // 0=Sun … 6=Sat\n if (!allowedWeekdays.includes(weekday)) {\n return;\n }\n\n let d = new Date();\n let dateoff = new Date(d.setMinutes(d.getMinutes() - d.getTimezoneOffset()));\n let startdate = info.dateStr;\n\n if (capabilities.addevent && startdate > dateoff.toISOString()) {\n const modalForm = new ModalForm({\n formClass: 'mod_bookit\\\\form\\\\edit_event_form',\n args: {\n cmid: cmid,\n timeclicked: startdate,\n },\n modalConfig: {title: editevent},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n initPossibleStarttimesRefresh(cmid);\n BookingFormResources.init(modalForm.modal.getRoot()[0]);\n });\n modalForm.show();\n }\n },\n\n /* Event click (edit) */\n eventClick: function(info) {\n let id = info.event.id;\n if (info.event.extendedProps.reserved) {\n return;\n }\n\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\form\\\\edit_event_form\",\n args: {\n cmid: cmid,\n id: id\n },\n modalConfig: {title: editevent},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n initPossibleStarttimesRefresh(cmid, id);\n BookingFormResources.init(modalForm.modal.getRoot()[0]);\n });\n modalForm.show();\n },\n\n // Toolbar configuration\n headerToolbar: {\n start: toolbarbuttons,\n center: 'title',\n end: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'\n },\n\n resources: [],\n\n // Feed with logged extra params\n eventSources: [{\n url: eventsource,\n extraParams: () => {\n // Console.log('[BookIT] extraParams sent →', extraFilterParams);\n return extraFilterParams;\n }\n }],\n\n views: {\n timeGridWeek: {pointer: true},\n resourceTimeGridWeek: {pointer: true},\n resourceTimelineWeek: {\n pointer: true,\n slotMinTime: '09:00',\n slotMaxTime: '21:00',\n slotWidth: 80,\n resources: []\n }\n }\n });\n\n window.bookitCalendar = calendar;\n\n /* Expose update for the filter form (called from view.php) */\n window.bookitCalendarUpdate = function(paramObj = {}) {\n extraFilterParams = paramObj;\n window.currentFilterParams = extraFilterParams;\n calendar.refetchEvents();\n };\n}\n"],"names":["cmid","eventsource","capabilities","lang","config","theGlobalProperty","textcolor","Object","prototype","hasOwnProperty","call","toolbarbuttons","addevent","strRequestBooking","editevent","strToday","strMonth","strWeek","strDay","strList","viewType","window","screen","width","allowedWeekdays","M","cfg","Array","isArray","bookit_allowedweekdays","map","x","Number","hiddenDays","filter","d","includes","extraFilterParams","calendar","EventCalendar","document","getElementById","locale","view","firstDay","weekends","scrollTime","slotMinTime","dayMaxEvents","nowIndicator","selectable","eventTextColor","eventBackgroundColor","eventStartEditable","eventDurationEditable","buttonText","text","today","dayGridMonth","timeGridWeek","timeGridDay","listWeek","eventsSet","events","length","customButtons","addButton","click","modalForm","ModalForm","formClass","args","modalConfig","title","addEventListener","FORM_SUBMITTED","refetchEvents","LOADED","init","modal","getRoot","show","dateClick","info","weekday","date","getUTCDay","Date","dateoff","setMinutes","getMinutes","getTimezoneOffset","startdate","dateStr","toISOString","timeclicked","eventClick","id","event","extendedProps","reserved","headerToolbar","start","center","end","resources","eventSources","url","extraParams","views","pointer","resourceTimeGridWeek","resourceTimelineWeek","slotMaxTime","slotWidth","bookitCalendar","bookitCalendarUpdate","paramObj","currentFilterParams","globalPropertyName","Promise","resolve","loop","setTimeout"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"calendar.min.js","sources":["../src/calendar.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the calendar\n *\n * @module mod_bookit/calendar\n * @copyright 2024 Melanie Treitinger, Justus Dieckmann RUB\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString} from 'core/str';\nimport ModalForm from 'core_form/modalform';\nimport {prefetchStrings} from 'core/prefetch';\nimport {initPossibleStarttimesRefresh} from \"mod_bookit/possible_slots_refresh\";\nimport BookingFormResources from \"mod_bookit/booking_form_resources\";\n\n/**\n * Wait until a global property exists (EventCalendar is loaded asynchronously).\n *\n * @param {String} globalPropertyName\n * @returns {Promise}\n */\nexport const theGlobalProperty = (globalPropertyName) =>\n new Promise(resolve => (function loop() {\n if (!window[globalPropertyName]) {\n setTimeout(loop, 20);\n return;\n }\n resolve();\n })());\n\n/**\n * Initializes the calendar.\n * @param {Number} cmid Course-module id\n * @param {String} eventsource URL for JSON feed (events.php)\n * @param {Object} capabilities {addevent: Boolean}\n * @param {String} lang Current UI language code\n * @param {Object} config Extra config (e.g. {textcolor:'#fff'})\n */\nexport async function init(cmid, eventsource, capabilities, lang, config) {\n await theGlobalProperty('EventCalendar');\n\n // Set textcolor.\n let textcolor = '#ffffff';\n if (Object.prototype.hasOwnProperty.call(config, 'textcolor')) {\n textcolor = config.textcolor;\n }\n\n // Define toolbarbuttons.\n let toolbarbuttons = 'prev,next today';\n if (capabilities.addevent) {\n toolbarbuttons = 'prev,next today addButton';\n }\n\n // String variables\n await prefetchStrings('mod_bookit', ['calendar_addbooking', 'calendar_editevent', 'calendar_eventlist']);\n await prefetchStrings('core', ['today', 'month', 'week']);\n await prefetchStrings('calendar', ['day']);\n const strRequestBooking = await getString('calendar_addbooking', 'mod_bookit');\n const editevent = await getString('calendar_editevent', 'mod_bookit');\n const strToday = await getString('today');\n const strMonth = await getString('month');\n const strWeek = await getString('week');\n const strDay = await getString('day', 'calendar');\n const strList = await getString('calendar_eventlist', 'bookit');\n\n // Define viewtype\n let viewType = 'timeGridWeek';\n if (window.screen.width <= 1000) {\n viewType = 'listWeek';\n }\n // Weekday visibility from admin settings (injected by PHP)\n const allowedWeekdays = (window.M && M.cfg && Array.isArray(M.cfg.bookit_allowedweekdays))\n ? M.cfg.bookit_allowedweekdays.map(x => Number(x))\n : [1, 2, 3, 4, 5];\n\n const hiddenDays = [0, 1, 2, 3, 4, 5, 6].filter(d => !allowedWeekdays.includes(d));\n\n // Runtime filter parameters – mutable via bookitCalendarUpdate()\n let extraFilterParams = {}; // {room:123, status:2, faculty:'ENG', …}\n\n const calendar = window.EventCalendar.create(document.getElementById('ec'), {\n /* Appearance / behaviour */\n locale: lang,\n view: viewType,\n firstDay: 1,\n weekends: allowedWeekdays.includes(0) || allowedWeekdays.includes(6),\n scrollTime: '09:00:00',\n slotMinTime: '07:00:00',\n dayMaxEvents: false,\n nowIndicator: true,\n hiddenDays: hiddenDays,\n selectable: false,\n eventTextColor: textcolor,\n eventBackgroundColor: '#035AA3',\n eventStartEditable: false,\n eventDurationEditable: false,\n buttonText: function(text) {\n text.today = strToday;\n text.dayGridMonth = strMonth;\n text.timeGridWeek = strWeek;\n text.timeGridDay = strDay;\n text.listWeek = strList;\n return text;\n },\n\n eventsSet: function(events) {\n // Comment console.log('[BookIT] eventsSet: received', events.length, 'events');\n if (events.length) {\n /* Comment\n console.log('[BookIT] sample event', {\n id: events[0].id,\n title: events[0].title,\n start: events[0].startStr || events[0].start,\n end: events[0].endStr || events[0].end,\n room: events[0].extendedProps?.room,\n department: events[0].extendedProps?.department,\n bookingstatus: events[0].extendedProps?.bookingstatus\n });\n */\n }\n },\n\n /* Custom toolbar button (“Add booking”) */\n customButtons: {\n addButton: {\n text: strRequestBooking,\n click: function() {\n const modalForm = new ModalForm({\n formClass: 'mod_bookit\\\\form\\\\edit_event_form',\n args: {\n cmid: cmid\n },\n modalConfig: {title: editevent},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n initPossibleStarttimesRefresh(cmid);\n BookingFormResources.init(modalForm.modal.getRoot()[0]);\n });\n modalForm.show();\n }\n }\n },\n\n /* Date click (create new event) */\n dateClick: function(info) {\n const weekday = info.date.getUTCDay(); // 0=Sun … 6=Sat\n if (!allowedWeekdays.includes(weekday)) {\n return;\n }\n\n let startdate = info.dateStr;\n const isFutureSlot = info.date.getTime() > Date.now();\n\n if (capabilities.addevent && isFutureSlot) {\n const modalForm = new ModalForm({\n formClass: 'mod_bookit\\\\form\\\\edit_event_form',\n args: {\n cmid: cmid,\n timeclicked: startdate,\n },\n modalConfig: {title: editevent},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n initPossibleStarttimesRefresh(cmid);\n BookingFormResources.init(modalForm.modal.getRoot()[0]);\n });\n modalForm.show();\n }\n },\n\n /* Event click (edit) */\n eventClick: function(info) {\n let id = info.event.id;\n if (info.event.extendedProps.reserved) {\n return;\n }\n\n const modalForm = new ModalForm({\n formClass: \"mod_bookit\\\\form\\\\edit_event_form\",\n args: {\n cmid: cmid,\n id: id\n },\n modalConfig: {title: editevent},\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, () => {\n calendar.refetchEvents();\n });\n modalForm.addEventListener(modalForm.events.LOADED, () => {\n initPossibleStarttimesRefresh(cmid, id);\n BookingFormResources.init(modalForm.modal.getRoot()[0]);\n });\n modalForm.show();\n },\n\n // Toolbar configuration\n headerToolbar: {\n start: toolbarbuttons,\n center: 'title',\n end: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'\n },\n\n resources: [],\n\n // Feed with logged extra params\n eventSources: [{\n url: eventsource,\n extraParams: () => {\n // Console.log('[BookIT] extraParams sent →', extraFilterParams);\n return extraFilterParams;\n }\n }],\n\n views: {\n timeGridWeek: {pointer: true},\n resourceTimeGridWeek: {pointer: true},\n resourceTimelineWeek: {\n pointer: true,\n slotMinTime: '09:00',\n slotMaxTime: '21:00',\n slotWidth: 80,\n resources: []\n }\n }\n });\n\n window.bookitCalendar = calendar;\n\n /* Expose update for the filter form (called from view.php) */\n window.bookitCalendarUpdate = function(paramObj = {}) {\n extraFilterParams = paramObj;\n window.currentFilterParams = extraFilterParams;\n calendar.refetchEvents();\n };\n}\n"],"names":["cmid","eventsource","capabilities","lang","config","theGlobalProperty","textcolor","Object","prototype","hasOwnProperty","call","toolbarbuttons","addevent","strRequestBooking","editevent","strToday","strMonth","strWeek","strDay","strList","viewType","window","screen","width","allowedWeekdays","M","cfg","Array","isArray","bookit_allowedweekdays","map","x","Number","hiddenDays","filter","d","includes","extraFilterParams","calendar","EventCalendar","create","document","getElementById","locale","view","firstDay","weekends","scrollTime","slotMinTime","dayMaxEvents","nowIndicator","selectable","eventTextColor","eventBackgroundColor","eventStartEditable","eventDurationEditable","buttonText","text","today","dayGridMonth","timeGridWeek","timeGridDay","listWeek","eventsSet","events","length","customButtons","addButton","click","modalForm","ModalForm","formClass","args","modalConfig","title","addEventListener","FORM_SUBMITTED","refetchEvents","LOADED","init","modal","getRoot","show","dateClick","info","weekday","date","getUTCDay","startdate","dateStr","isFutureSlot","getTime","Date","now","timeclicked","eventClick","id","event","extendedProps","reserved","headerToolbar","start","center","end","resources","eventSources","url","extraParams","views","pointer","resourceTimeGridWeek","resourceTimelineWeek","slotMaxTime","slotWidth","bookitCalendar","bookitCalendarUpdate","paramObj","currentFilterParams","globalPropertyName","Promise","resolve","loop","setTimeout"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/export_modal.min.js b/amd/build/export_modal.min.js index f7e05537..75987142 100644 --- a/amd/build/export_modal.min.js +++ b/amd/build/export_modal.min.js @@ -1,3 +1,3 @@ -define("mod_bookit/export_modal",["jquery","core/str"],(function($,str){return{init:function(cmId){str.get_strings([{key:"noevents",component:"mod_bookit"},{key:"chooseevent",component:"mod_bookit"}]).done((function(strings){const noEventsStr=strings[0];function toLocalDateValue(dateObj){const d=new Date(dateObj),offMin=d.getTimezoneOffset();return new Date(d.getTime()-6e4*offMin).toISOString().slice(0,10)}function getCalendarDateRangeOrFallback(){if(window.bookitCalendar&&window.bookitCalendar.view){const view=window.bookitCalendar.view;return{startDate:toLocalDateValue(view.activeStart),endDate:toLocalDateValue(new Date(view.activeEnd.getTime()-1))}}return{startDate:"1970-01-01",endDate:"2100-01-01"}}function filterExportList(){const val=($("#bookit-modal-search").val()||"").toLowerCase().trim();$("#bookit-export-list label").each((function(){const $row=$(this),show=$row.text().toLowerCase().includes(val);$row.toggleClass("d-flex",show).toggleClass("d-none",!show)}))}function fetchExportList(){const qs={id:cmId},startDate=($("#bookit-export-start").val()||"").trim(),endDate=($("#bookit-export-end").val()||"").trim(),fallback=getCalendarDateRangeOrFallback(),s=startDate||fallback.startDate,e=endDate||fallback.endDate;qs.start=s+"T00:00",qs.end=e+"T23:59",window.currentFilterParams&&Object.keys(window.currentFilterParams).forEach((function(k){"start"!==k&&"end"!==k&&(qs[k]=window.currentFilterParams[k])}));const list=$("#bookit-export-list");list.html('
'),$.getJSON(M.cfg.wwwroot+"/mod/bookit/events.php",qs,(function(data){if(list.empty(),data=(data||[]).filter((e=>!(e.extendedProps&&(!0===e.extendedProps.reserved||1===e.extendedProps.reserved)))),!data.length)return void list.append('
'+noEventsStr+"
");const statusMap={0:"New",1:"In progress",2:"Accepted",3:"Cancelled",4:"Rejected"};data.forEach((function(e){var _e$bookingstatus,_e$title;const roomTxt=(e.location||e.room||e.roomname||"").trim(),faculty=(e.department||e.faculty||"").trim(),statusTxt=statusMap[String(null!==(_e$bookingstatus=e.bookingstatus)&&void 0!==_e$bookingstatus?_e$bookingstatus:"")]||"",startStr=e.start||"",dateTxt=startStr?String(startStr).substr(0,16).replace("T"," "):"",metaLine=roomTxt?roomTxt+" "+dateTxt:dateTxt,checkbox='',row=$('");list.append(row)})),filterExportList()})).fail((function(xhr){list.empty(),list.append('
events.php failed: '+(xhr.responseText||xhr.status)+"
")}))}$(document).on("click","#bookit-export",(function(){const r=getCalendarDateRangeOrFallback();$("#bookit-export-start").val(r.startDate),$("#bookit-export-end").val(r.endDate),$("#bookit-export-modal").modal("show"),fetchExportList()})),$(document).on("change input","#bookit-export-start, #bookit-export-end",(function(){$("#bookit-export-modal").hasClass("show")&&fetchExportList()})),$(document).on("click","#bookit-export-reset-range",(function(){const r=getCalendarDateRangeOrFallback();$("#bookit-export-start").val(r.startDate),$("#bookit-export-end").val(r.endDate),fetchExportList()})),$(document).on("input","#bookit-modal-search",filterExportList),$(document).on("click","#bookit-check-all",(function(){$("#bookit-export-list label:visible input[type=checkbox]:enabled").prop("checked",!0)})),$(document).on("click","#bookit-uncheck-all",(function(){$("#bookit-export-list label:visible input[type=checkbox]:enabled").prop("checked",!1)})),$(document).on("click","#bookit-export-confirm",(function(){const ids=$("#bookit-export-list input[type=checkbox]:enabled:checked").map((function(){return this.value})).get();if(!ids.length)return;const qs=new URLSearchParams({id:cmId});window.currentFilterParams&&Object.entries(window.currentFilterParams).forEach((_ref=>{let[k,v]=_ref;return qs.append(k,v)})),ids.forEach((id=>qs.append("ids[]",id))),window.location=M.cfg.wwwroot+"/mod/bookit/export_events.php?"+qs.toString(),$("#bookit-export-modal").modal("hide")}))}))}}})); +define("mod_bookit/export_modal",["jquery","core/notification","core/str"],(function($,Notification,str){return{init:function(cmId){str.get_strings([{key:"noevents",component:"mod_bookit"},{key:"chooseevent",component:"mod_bookit"},{key:"exportevents_selected",component:"mod_bookit"},{key:"event_bookingstatus_0",component:"mod_bookit"},{key:"event_bookingstatus_1",component:"mod_bookit"},{key:"event_bookingstatus_2",component:"mod_bookit"},{key:"event_bookingstatus_3",component:"mod_bookit"},{key:"event_bookingstatus_4",component:"mod_bookit"}]).done((function(strings){const noEventsStr=strings[0],chooseEventStr=strings[1],selectedLabel=strings[2],statusMap={0:strings[3],1:strings[4],2:strings[5],3:strings[6],4:strings[7]};function updateSelectionState(){const selected=$("#bookit-export-list input[type=checkbox]:enabled:checked").length;$("#bookit-export-selection-count").text(selectedLabel+": "+String(selected)),$("#bookit-export-confirm").prop("disabled",0===selected)}function toLocalDateValue(dateObj){const d=new Date(dateObj),offMin=d.getTimezoneOffset();return new Date(d.getTime()-6e4*offMin).toISOString().slice(0,10)}function getCalendarDateRangeOrFallback(){if(window.bookitCalendar&&window.bookitCalendar.view){const view=window.bookitCalendar.view;return{startDate:toLocalDateValue(view.activeStart),endDate:toLocalDateValue(new Date(view.activeEnd.getTime()-1))}}return{startDate:"1970-01-01",endDate:"2100-01-01"}}function filterExportList(){const val=($("#bookit-modal-search").val()||"").toLowerCase().trim();$("#bookit-export-list label").each((function(){const $row=$(this),show=$row.text().toLowerCase().includes(val);$row.toggleClass("d-flex",show).toggleClass("d-none",!show)}))}function fetchExportList(){const qs={id:cmId},startDate=($("#bookit-export-start").val()||"").trim(),endDate=($("#bookit-export-end").val()||"").trim(),fallback=getCalendarDateRangeOrFallback(),s=startDate||fallback.startDate,e=endDate||fallback.endDate;qs.start=s+"T00:00",qs.end=e+"T23:59",window.currentFilterParams&&Object.keys(window.currentFilterParams).forEach((function(k){"start"!==k&&"end"!==k&&(qs[k]=window.currentFilterParams[k])}));const list=$("#bookit-export-list");list.html('
'),$.getJSON(M.cfg.wwwroot+"/mod/bookit/events.php",qs,(function(data){list.empty(),data=(data||[]).filter((e=>!(e.extendedProps&&(!0===e.extendedProps.reserved||1===e.extendedProps.reserved)))),data.length?(data.forEach((function(e){var _e$bookingstatus,_e$title;const roomTxt=(e.location||e.room||e.roomname||"").trim(),faculty=(e.department||e.faculty||"").trim(),statusTxt=statusMap[String(null!==(_e$bookingstatus=e.bookingstatus)&&void 0!==_e$bookingstatus?_e$bookingstatus:"")]||"",startStr=e.start||"",metaParts=[startStr?String(startStr).substr(0,16).replace("T"," "):""];roomTxt&&metaParts.push(roomTxt),faculty&&metaParts.push(faculty);const metaLine=metaParts.filter(Boolean).join(" | "),checkbox='',statusBadge=statusTxt?''+statusTxt+"":"",row=$('");list.append(row)})),filterExportList(),updateSelectionState()):list.append('
'+noEventsStr+"
")})).fail((function(xhr){list.empty(),list.append('
events.php failed: '+(xhr.responseText||xhr.status)+"
"),updateSelectionState()}))}$(document).on("click","#bookit-export",(function(){const r=getCalendarDateRangeOrFallback();$("#bookit-export-start").val(r.startDate),$("#bookit-export-end").val(r.endDate),$("#bookit-export-modal").modal("show"),fetchExportList()})),$(document).on("change input","#bookit-export-start, #bookit-export-end",(function(){$("#bookit-export-modal").hasClass("show")&&fetchExportList()})),$(document).on("click","#bookit-export-reset-range",(function(){const r=getCalendarDateRangeOrFallback();$("#bookit-export-start").val(r.startDate),$("#bookit-export-end").val(r.endDate),fetchExportList()})),$(document).on("input","#bookit-modal-search",filterExportList),$(document).on("click","#bookit-check-all",(function(){$("#bookit-export-list label:visible input[type=checkbox]:enabled").prop("checked",!0),updateSelectionState()})),$(document).on("click","#bookit-uncheck-all",(function(){$("#bookit-export-list label:visible input[type=checkbox]:enabled").prop("checked",!1),updateSelectionState()})),$(document).on("change","#bookit-export-list input[type=checkbox]",updateSelectionState),$(document).on("click","#bookit-export-confirm",(function(){const ids=$("#bookit-export-list input[type=checkbox]:enabled:checked").map((function(){return this.value})).get();if(!ids.length)return void Notification.alert("",chooseEventStr);const qs=new URLSearchParams({id:cmId});window.currentFilterParams&&Object.entries(window.currentFilterParams).forEach((_ref=>{let[k,v]=_ref;return qs.append(k,v)})),ids.forEach((id=>qs.append("ids[]",id))),window.location=M.cfg.wwwroot+"/mod/bookit/export_events.php?"+qs.toString(),$("#bookit-export-modal").modal("hide")})),updateSelectionState()}))}}})); //# sourceMappingURL=export_modal.min.js.map \ No newline at end of file diff --git a/amd/build/export_modal.min.js.map b/amd/build/export_modal.min.js.map index 19e480fb..ccd0a37d 100644 --- a/amd/build/export_modal.min.js.map +++ b/amd/build/export_modal.min.js.map @@ -1 +1 @@ -{"version":3,"file":"export_modal.min.js","sources":["../src/export_modal.js"],"sourcesContent":["define(['jquery', 'core/str'], function($, str) {\n return {\n init: function(cmId) {\n\n const stringKeys = [\n {key: 'noevents', component: 'mod_bookit'},\n {key: 'chooseevent', component: 'mod_bookit'}\n ];\n\n str.get_strings(stringKeys).done(function(strings) {\n const noEventsStr = strings[0];\n\n /**\n * Convert a date-like value to a local (browser timezone) `YYYY-MM-DD` string.\n *\n * This normalises the date to local time before formatting, avoiding the common\n * off-by-one issue caused by `toISOString()` using UTC.\n *\n * @param {Date|string|number} dateObj A `Date` instance or any value accepted by `new Date(...)`.\n * @returns {string} Date formatted as `YYYY-MM-DD` in local time.\n */\n function toLocalDateValue(dateObj) {\n // Date -> 'YYYY-MM-DD' in local time.\n const d = new Date(dateObj);\n const offMin = d.getTimezoneOffset();\n const local = new Date(d.getTime() - offMin * 60000);\n return local.toISOString().slice(0, 10);\n }\n\n /**\n * Get the currently visible calendar date range from the global Bookit calendar.\n *\n * If the calendar instance is not available, returns a wide fallback range so\n * the export modal can still function (and the backend can apply its own limits).\n *\n * Notes:\n * - `activeStart` is inclusive.\n * - `activeEnd` is typically exclusive, so we convert it to an inclusive end date\n * by subtracting 1ms.\n *\n * @returns {{startDate: string, endDate: string}} Object with `YYYY-MM-DD` start and end dates.\n */\n function getCalendarDateRangeOrFallback() {\n if (window.bookitCalendar && window.bookitCalendar.view) {\n const view = window.bookitCalendar.view;\n\n const startDate = toLocalDateValue(view.activeStart);\n\n const endInclusive = new Date(view.activeEnd.getTime() - 1);\n const endDate = toLocalDateValue(endInclusive);\n\n return {startDate, endDate};\n }\n return {startDate: '1970-01-01', endDate: '2100-01-01'};\n }\n\n /**\n * Filter the export list entries by the export modal search input.\n *\n * This performs a case-insensitive substring match against each label's text and\n * toggles visibility using Bootstrap utility classes (`d-flex` / `d-none`).\n *\n * Expects:\n * - `#bookit-modal-search` input to exist.\n * - Each export entry to be represented by a `label` element within\n * `#bookit-export-list`.\n *\n * @returns {void}\n */\n function filterExportList() {\n const val = ($('#bookit-modal-search').val() || '').toLowerCase().trim();\n $('#bookit-export-list label').each(function() {\n const $row = $(this);\n const show = $row.text().toLowerCase().includes(val);\n $row.toggleClass('d-flex', show).toggleClass('d-none', !show);\n });\n }\n\n\n /**\n * Fetch events for the export modal and render them as a checkbox list.\n *\n * Uses the modal date range if set, otherwise falls back to the current calendar view range.\n * Merges `window.currentFilterParams` except `start`/`end` (modal range wins).\n *\n * @returns {void}\n */\n function fetchExportList() {\n const qs = {id: cmId};\n\n const startDate = ($('#bookit-export-start').val() || '').trim();\n const endDate = ($('#bookit-export-end').val() || '').trim();\n const fallback = getCalendarDateRangeOrFallback();\n\n const s = startDate || fallback.startDate;\n const e = endDate || fallback.endDate;\n\n qs.start = s + 'T00:00';\n qs.end = e + 'T23:59';\n\n if (window.currentFilterParams) {\n Object.keys(window.currentFilterParams).forEach(function(k) {\n if (k === 'start' || k === 'end') {\n return; // Never allow filters to override modal range.\n }\n qs[k] = window.currentFilterParams[k];\n });\n }\n\n\n const list = $('#bookit-export-list');\n list.html('
');\n\n $.getJSON(M.cfg.wwwroot + '/mod/bookit/events.php', qs, function(data) {\n list.empty();\n\n data = (data || []).filter(e => !(e.extendedProps &&\n (e.extendedProps.reserved === true || e.extendedProps.reserved === 1)));\n\n if (!data.length) {\n list.append('
' + noEventsStr + '
');\n return;\n }\n\n const statusMap = {'0': 'New', '1': 'In progress', '2': 'Accepted', '3': 'Cancelled', '4': 'Rejected'};\n\n data.forEach(function(e) {\n const roomTxt = (e.location || e.room || e.roomname || '').trim();\n const faculty = (e.department || e.faculty || '').trim();\n const statusTxt = statusMap[String(e.bookingstatus ?? '')] || '';\n const startStr = (e.start || '');\n const dateTxt = startStr ? String(startStr).substr(0, 16).replace('T', ' ') : '';\n const metaLine = roomTxt ? (roomTxt + ' ' + dateTxt) : dateTxt;\n\n const checkbox = '';\n\n const row = $(\n ''\n );\n list.append(row);\n });\n\n filterExportList();\n }).fail(function(xhr) {\n list.empty();\n list.append('
events.php failed: ' + (xhr.responseText || xhr.status) + '
');\n });\n }\n /**\n * Open the export modal and load the initial list for the current calendar range.\n */\n $(document).on('click', '#bookit-export', function() {\n const r = getCalendarDateRangeOrFallback();\n $('#bookit-export-start').val(r.startDate);\n $('#bookit-export-end').val(r.endDate);\n\n $('#bookit-export-modal').modal('show');\n fetchExportList();\n });\n\n /**\n * Refresh export list when the modal date range changes (only while modal is open).\n */\n $(document).on('change input', '#bookit-export-start, #bookit-export-end', function() {\n if ($('#bookit-export-modal').hasClass('show')) {\n fetchExportList();\n }\n });\n\n /**\n * Reset modal date range to the current calendar view and refresh list.\n */\n $(document).on('click', '#bookit-export-reset-range', function() {\n const r = getCalendarDateRangeOrFallback();\n $('#bookit-export-start').val(r.startDate);\n $('#bookit-export-end').val(r.endDate);\n fetchExportList();\n });\n\n // Apply live text filter to the export list.\n $(document).on('input', '#bookit-modal-search', filterExportList);\n\n // Select all visible, enabled event checkboxes.\n $(document).on('click', '#bookit-check-all', function() {\n $('#bookit-export-list label:visible input[type=checkbox]:enabled').prop('checked', true);\n });\n\n // Deselect all visible, enabled event checkboxes.\n $(document).on('click', '#bookit-uncheck-all', function() {\n $('#bookit-export-list label:visible input[type=checkbox]:enabled').prop('checked', false);\n });\n\n /**\n * Start export for selected event ids and close the modal.\n *\n * Builds a query containing `ids[]` plus the current filter params and navigates to export endpoint.\n */\n $(document).on('click', '#bookit-export-confirm', function() {\n const ids = $('#bookit-export-list input[type=checkbox]:enabled:checked')\n .map(function() {\n return this.value;\n }).get();\n\n if (!ids.length) {\n return;\n }\n\n const qs = new URLSearchParams({id: cmId});\n if (window.currentFilterParams) {\n Object.entries(window.currentFilterParams).forEach(([k, v]) => qs.append(k, v));\n }\n ids.forEach(id => qs.append('ids[]', id));\n\n window.location = M.cfg.wwwroot + '/mod/bookit/export_events.php?' + qs.toString();\n $('#bookit-export-modal').modal('hide');\n });\n });\n }\n };\n});\n"],"names":["define","$","str","init","cmId","get_strings","key","component","done","strings","noEventsStr","toLocalDateValue","dateObj","d","Date","offMin","getTimezoneOffset","getTime","toISOString","slice","getCalendarDateRangeOrFallback","window","bookitCalendar","view","startDate","activeStart","endDate","activeEnd","filterExportList","val","toLowerCase","trim","each","$row","this","show","text","includes","toggleClass","fetchExportList","qs","id","fallback","s","e","start","end","currentFilterParams","Object","keys","forEach","k","list","html","getJSON","M","cfg","wwwroot","data","empty","filter","extendedProps","reserved","length","append","statusMap","roomTxt","location","room","roomname","faculty","department","statusTxt","String","bookingstatus","startStr","dateTxt","substr","replace","metaLine","checkbox","row","title","fail","xhr","responseText","status","document","on","r","modal","hasClass","prop","ids","map","value","get","URLSearchParams","entries","_ref","v","toString"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"export_modal.min.js","sources":["../src/export_modal.js"],"sourcesContent":["define(['jquery', 'core/notification', 'core/str'], function($, Notification, str) {\n return {\n init: function(cmId) {\n\n const stringKeys = [\n {key: 'noevents', component: 'mod_bookit'},\n {key: 'chooseevent', component: 'mod_bookit'},\n {key: 'exportevents_selected', component: 'mod_bookit'},\n {key: 'event_bookingstatus_0', component: 'mod_bookit'},\n {key: 'event_bookingstatus_1', component: 'mod_bookit'},\n {key: 'event_bookingstatus_2', component: 'mod_bookit'},\n {key: 'event_bookingstatus_3', component: 'mod_bookit'},\n {key: 'event_bookingstatus_4', component: 'mod_bookit'}\n ];\n\n str.get_strings(stringKeys).done(function(strings) {\n const noEventsStr = strings[0];\n const chooseEventStr = strings[1];\n const selectedLabel = strings[2];\n const statusMap = {\n '0': strings[3],\n '1': strings[4],\n '2': strings[5],\n '3': strings[6],\n '4': strings[7]\n };\n\n /**\n * Render the localized selected-count label for the export modal.\n *\n * @param {Number} count Number of currently selected events.\n * @returns {String}\n */\n function formatSelectedCount(count) {\n return selectedLabel + ': ' + String(count);\n }\n\n /**\n * Synchronise the export button state and selected-count badge.\n *\n * @returns {void}\n */\n function updateSelectionState() {\n const selected = $('#bookit-export-list input[type=checkbox]:enabled:checked').length;\n $('#bookit-export-selection-count').text(formatSelectedCount(selected));\n $('#bookit-export-confirm').prop('disabled', selected === 0);\n }\n\n /**\n * Convert a date-like value to a local (browser timezone) `YYYY-MM-DD` string.\n *\n * This normalises the date to local time before formatting, avoiding the common\n * off-by-one issue caused by `toISOString()` using UTC.\n *\n * @param {Date|string|number} dateObj A `Date` instance or any value accepted by `new Date(...)`.\n * @returns {string} Date formatted as `YYYY-MM-DD` in local time.\n */\n function toLocalDateValue(dateObj) {\n // Date -> 'YYYY-MM-DD' in local time.\n const d = new Date(dateObj);\n const offMin = d.getTimezoneOffset();\n const local = new Date(d.getTime() - offMin * 60000);\n return local.toISOString().slice(0, 10);\n }\n\n /**\n * Get the currently visible calendar date range from the global Bookit calendar.\n *\n * If the calendar instance is not available, returns a wide fallback range so\n * the export modal can still function (and the backend can apply its own limits).\n *\n * Notes:\n * - `activeStart` is inclusive.\n * - `activeEnd` is typically exclusive, so we convert it to an inclusive end date\n * by subtracting 1ms.\n *\n * @returns {{startDate: string, endDate: string}} Object with `YYYY-MM-DD` start and end dates.\n */\n function getCalendarDateRangeOrFallback() {\n if (window.bookitCalendar && window.bookitCalendar.view) {\n const view = window.bookitCalendar.view;\n\n const startDate = toLocalDateValue(view.activeStart);\n\n const endInclusive = new Date(view.activeEnd.getTime() - 1);\n const endDate = toLocalDateValue(endInclusive);\n\n return {startDate, endDate};\n }\n return {startDate: '1970-01-01', endDate: '2100-01-01'};\n }\n\n /**\n * Filter the export list entries by the export modal search input.\n *\n * This performs a case-insensitive substring match against each label's text and\n * toggles visibility using Bootstrap utility classes (`d-flex` / `d-none`).\n *\n * Expects:\n * - `#bookit-modal-search` input to exist.\n * - Each export entry to be represented by a `label` element within\n * `#bookit-export-list`.\n *\n * @returns {void}\n */\n function filterExportList() {\n const val = ($('#bookit-modal-search').val() || '').toLowerCase().trim();\n $('#bookit-export-list label').each(function() {\n const $row = $(this);\n const show = $row.text().toLowerCase().includes(val);\n $row.toggleClass('d-flex', show).toggleClass('d-none', !show);\n });\n }\n\n\n /**\n * Fetch events for the export modal and render them as a checkbox list.\n *\n * Uses the modal date range if set, otherwise falls back to the current calendar view range.\n * Merges `window.currentFilterParams` except `start`/`end` (modal range wins).\n *\n * @returns {void}\n */\n function fetchExportList() {\n const qs = {id: cmId};\n\n const startDate = ($('#bookit-export-start').val() || '').trim();\n const endDate = ($('#bookit-export-end').val() || '').trim();\n const fallback = getCalendarDateRangeOrFallback();\n\n const s = startDate || fallback.startDate;\n const e = endDate || fallback.endDate;\n\n qs.start = s + 'T00:00';\n qs.end = e + 'T23:59';\n\n if (window.currentFilterParams) {\n Object.keys(window.currentFilterParams).forEach(function(k) {\n if (k === 'start' || k === 'end') {\n return; // Never allow filters to override modal range.\n }\n qs[k] = window.currentFilterParams[k];\n });\n }\n\n\n const list = $('#bookit-export-list');\n list.html('
');\n\n $.getJSON(M.cfg.wwwroot + '/mod/bookit/events.php', qs, function(data) {\n list.empty();\n\n data = (data || []).filter(e => !(e.extendedProps &&\n (e.extendedProps.reserved === true || e.extendedProps.reserved === 1)));\n\n if (!data.length) {\n list.append('
' + noEventsStr + '
');\n return;\n }\n\n data.forEach(function(e) {\n const roomTxt = (e.location || e.room || e.roomname || '').trim();\n const faculty = (e.department || e.faculty || '').trim();\n const statusTxt = statusMap[String(e.bookingstatus ?? '')] || '';\n const startStr = (e.start || '');\n const dateTxt = startStr ? String(startStr).substr(0, 16).replace('T', ' ') : '';\n const metaParts = [dateTxt];\n if (roomTxt) {\n metaParts.push(roomTxt);\n }\n if (faculty) {\n metaParts.push(faculty);\n }\n const metaLine = metaParts.filter(Boolean).join(' | ');\n\n const checkbox = '';\n const statusBadge = statusTxt\n ? '' + statusTxt + ''\n : '';\n\n const row = $(\n ''\n );\n list.append(row);\n });\n\n filterExportList();\n updateSelectionState();\n }).fail(function(xhr) {\n list.empty();\n list.append('
events.php failed: ' + (xhr.responseText || xhr.status) + '
');\n updateSelectionState();\n });\n }\n /**\n * Open the export modal and load the initial list for the current calendar range.\n */\n $(document).on('click', '#bookit-export', function() {\n const r = getCalendarDateRangeOrFallback();\n $('#bookit-export-start').val(r.startDate);\n $('#bookit-export-end').val(r.endDate);\n\n $('#bookit-export-modal').modal('show');\n fetchExportList();\n });\n\n /**\n * Refresh export list when the modal date range changes (only while modal is open).\n */\n $(document).on('change input', '#bookit-export-start, #bookit-export-end', function() {\n if ($('#bookit-export-modal').hasClass('show')) {\n fetchExportList();\n }\n });\n\n /**\n * Reset modal date range to the current calendar view and refresh list.\n */\n $(document).on('click', '#bookit-export-reset-range', function() {\n const r = getCalendarDateRangeOrFallback();\n $('#bookit-export-start').val(r.startDate);\n $('#bookit-export-end').val(r.endDate);\n fetchExportList();\n });\n\n // Apply live text filter to the export list.\n $(document).on('input', '#bookit-modal-search', filterExportList);\n\n // Select all visible, enabled event checkboxes.\n $(document).on('click', '#bookit-check-all', function() {\n $('#bookit-export-list label:visible input[type=checkbox]:enabled').prop('checked', true);\n updateSelectionState();\n });\n\n // Deselect all visible, enabled event checkboxes.\n $(document).on('click', '#bookit-uncheck-all', function() {\n $('#bookit-export-list label:visible input[type=checkbox]:enabled').prop('checked', false);\n updateSelectionState();\n });\n\n $(document).on('change', '#bookit-export-list input[type=checkbox]', updateSelectionState);\n\n /**\n * Start export for selected event ids and close the modal.\n *\n * Builds a query containing `ids[]` plus the current filter params and navigates to export endpoint.\n */\n $(document).on('click', '#bookit-export-confirm', function() {\n const ids = $('#bookit-export-list input[type=checkbox]:enabled:checked')\n .map(function() {\n return this.value;\n }).get();\n\n if (!ids.length) {\n void Notification.alert('', chooseEventStr);\n return;\n }\n\n const qs = new URLSearchParams({id: cmId});\n if (window.currentFilterParams) {\n Object.entries(window.currentFilterParams).forEach(([k, v]) => qs.append(k, v));\n }\n ids.forEach(id => qs.append('ids[]', id));\n\n window.location = M.cfg.wwwroot + '/mod/bookit/export_events.php?' + qs.toString();\n $('#bookit-export-modal').modal('hide');\n });\n\n updateSelectionState();\n });\n }\n };\n});\n"],"names":["define","$","Notification","str","init","cmId","get_strings","key","component","done","strings","noEventsStr","chooseEventStr","selectedLabel","statusMap","updateSelectionState","selected","length","text","String","prop","toLocalDateValue","dateObj","d","Date","offMin","getTimezoneOffset","getTime","toISOString","slice","getCalendarDateRangeOrFallback","window","bookitCalendar","view","startDate","activeStart","endDate","activeEnd","filterExportList","val","toLowerCase","trim","each","$row","this","show","includes","toggleClass","fetchExportList","qs","id","fallback","s","e","start","end","currentFilterParams","Object","keys","forEach","k","list","html","getJSON","M","cfg","wwwroot","data","empty","filter","extendedProps","reserved","roomTxt","location","room","roomname","faculty","department","statusTxt","bookingstatus","startStr","metaParts","substr","replace","push","metaLine","Boolean","join","checkbox","statusBadge","row","titleHTML","title","_e$title","append","fail","xhr","responseText","status","document","on","r","modal","hasClass","ids","map","value","get","alert","URLSearchParams","entries","_ref","v","toString"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/overview/booking_status_dropdown.min.js b/amd/build/overview/booking_status_dropdown.min.js index bd3cdf7f..394b3f1f 100644 --- a/amd/build/overview/booking_status_dropdown.min.js +++ b/amd/build/overview/booking_status_dropdown.min.js @@ -8,6 +8,6 @@ define("mod_bookit/overview/booking_status_dropdown",["exports","core/ajax","cor * @module mod_bookit/overview/booking_status_dropdown * @copyright 2026 ssystems GmbH * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);const SELECTOR='select[data-action="update-booking-status"]',applyColor=select=>{const opt=select.options[select.selectedIndex],bg=opt&&opt.dataset.bg?opt.dataset.bg:"#ffffff",fg=opt&&opt.dataset.fg?opt.dataset.fg:"#000000";select.style.backgroundColor=bg,select.style.color=fg;const td=select.closest("td");td&&(td.style.backgroundColor=bg)};_exports.init=()=>{document.querySelectorAll(SELECTOR).forEach((select=>{applyColor(select)})),document.addEventListener("change",(e=>{const select=e.target.closest(SELECTOR);if(!select)return;const cmid=parseInt(select.dataset.cmid,10),eventid=parseInt(select.dataset.eventid,10),status=parseInt(select.value,10);select.disabled=!0,_ajax.default.call([{methodname:"mod_bookit_update_event_booking_status",args:{cmid:cmid,eventid:eventid,status:status}}])[0].then((()=>{applyColor(select),select.disabled=!1})).catch((err=>{select.disabled=!1,_notification.default.exception(err)}))}))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);const SELECTOR='select[data-action="update-booking-status"]',applyColor=select=>{const opt=select.options[select.selectedIndex],bg=opt&&opt.dataset.bg?opt.dataset.bg:"#ffffff",fg=opt&&opt.dataset.fg?opt.dataset.fg:"#000000";select.style.backgroundColor=bg,select.style.color=fg;const td=select.closest("td");td&&(td.style.backgroundColor=bg)};_exports.init=()=>{document.querySelectorAll(SELECTOR).forEach((select=>{applyColor(select)})),document.addEventListener("change",(e=>{const select=e.target.closest(SELECTOR);if(!select)return;const cmid=parseInt(select.dataset.cmid,10),eventid=parseInt(select.dataset.eventid,10),status=parseInt(select.value,10);select.disabled=!0,_ajax.default.call([{methodname:"mod_bookit_update_event_booking_status",args:{cmid:cmid,eventid:eventid,status:status}}])[0].then((()=>(applyColor(select),window.location.reload(),null))).catch((err=>{select.disabled=!1,_notification.default.exception(err)}))})),document.addEventListener("click",(e=>{const button=e.target.closest('button[data-action="set-booking-status"]');if(!button)return;const cmid=parseInt(button.dataset.cmid,10),eventid=parseInt(button.dataset.eventid,10),status=parseInt(button.dataset.status,10);button.disabled=!0,_ajax.default.call([{methodname:"mod_bookit_update_event_booking_status",args:{cmid:cmid,eventid:eventid,status:status}}])[0].then((()=>(window.location.reload(),null))).catch((err=>{button.disabled=!1,_notification.default.exception(err)}))}))}})); //# sourceMappingURL=booking_status_dropdown.min.js.map \ No newline at end of file diff --git a/amd/build/overview/booking_status_dropdown.min.js.map b/amd/build/overview/booking_status_dropdown.min.js.map index c8cbac2d..93f91715 100644 --- a/amd/build/overview/booking_status_dropdown.min.js.map +++ b/amd/build/overview/booking_status_dropdown.min.js.map @@ -1 +1 @@ -{"version":3,"file":"booking_status_dropdown.min.js","sources":["../../src/overview/booking_status_dropdown.js"],"sourcesContent":["// This file is part of Moodle - https://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive booking status dropdown for the event overview table.\n *\n * Listens for changes on select[data-action=\"update-booking-status\"] and\n * persists the new status via the update_event_booking_status web service.\n *\n * @module mod_bookit/overview/booking_status_dropdown\n * @copyright 2026 ssystems GmbH \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nconst SELECTOR = 'select[data-action=\"update-booking-status\"]';\n\n/**\n * Apply status colour to a select element by reading data attributes from the selected option.\n *\n * @param {HTMLSelectElement} select\n */\nconst applyColor = (select) => {\n const opt = select.options[select.selectedIndex];\n const bg = (opt && opt.dataset.bg) ? opt.dataset.bg : '#ffffff';\n const fg = (opt && opt.dataset.fg) ? opt.dataset.fg : '#000000';\n select.style.backgroundColor = bg;\n select.style.color = fg;\n const td = select.closest('td');\n if (td) {\n td.style.backgroundColor = bg;\n }\n};\n\n/**\n * Initialise the dropdown listener on the overview table.\n */\nexport const init = () => {\n // Apply initial colours to all existing dropdowns on the page.\n document.querySelectorAll(SELECTOR).forEach((select) => {\n applyColor(select);\n });\n\n document.addEventListener('change', (e) => {\n const select = e.target.closest(SELECTOR);\n if (!select) {\n return;\n }\n\n const cmid = parseInt(select.dataset.cmid, 10);\n const eventid = parseInt(select.dataset.eventid, 10);\n const status = parseInt(select.value, 10);\n\n select.disabled = true;\n\n Ajax.call([{\n methodname: 'mod_bookit_update_event_booking_status',\n args: {cmid, eventid, status},\n }])[0]\n .then(() => {\n applyColor(select);\n select.disabled = false;\n return;\n })\n .catch((err) => {\n select.disabled = false;\n Notification.exception(err);\n });\n });\n};\n"],"names":["SELECTOR","applyColor","select","opt","options","selectedIndex","bg","dataset","fg","style","backgroundColor","color","td","closest","document","querySelectorAll","forEach","addEventListener","e","target","cmid","parseInt","eventid","status","value","disabled","call","methodname","args","then","catch","err","exception"],"mappings":";;;;;;;;;;gLA6BMA,SAAW,8CAOXC,WAAcC,eACVC,IAAMD,OAAOE,QAAQF,OAAOG,eAC5BC,GAAMH,KAAOA,IAAII,QAAQD,GAAMH,IAAII,QAAQD,GAAK,UAChDE,GAAML,KAAOA,IAAII,QAAQC,GAAML,IAAII,QAAQC,GAAK,UACtDN,OAAOO,MAAMC,gBAAkBJ,GAC/BJ,OAAOO,MAAME,MAAQH,SACfI,GAAKV,OAAOW,QAAQ,MACtBD,KACAA,GAAGH,MAAMC,gBAAkBJ,mBAOf,KAEhBQ,SAASC,iBAAiBf,UAAUgB,SAASd,SACzCD,WAAWC,WAGfY,SAASG,iBAAiB,UAAWC,UAC3BhB,OAASgB,EAAEC,OAAON,QAAQb,cAC3BE,oBAICkB,KAAOC,SAASnB,OAAOK,QAAQa,KAAM,IACrCE,QAAUD,SAASnB,OAAOK,QAAQe,QAAS,IAC3CC,OAASF,SAASnB,OAAOsB,MAAO,IAEtCtB,OAAOuB,UAAW,gBAEbC,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,CAACR,KAAAA,KAAME,QAAAA,QAASC,OAAAA,WACtB,GACHM,MAAK,KACF5B,WAAWC,QACXA,OAAOuB,UAAW,KAGrBK,OAAOC,MACJ7B,OAAOuB,UAAW,wBACLO,UAAUD"} \ No newline at end of file +{"version":3,"file":"booking_status_dropdown.min.js","sources":["../../src/overview/booking_status_dropdown.js"],"sourcesContent":["// This file is part of Moodle - https://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive booking status dropdown for the event overview table.\n *\n * Listens for changes on select[data-action=\"update-booking-status\"] and\n * persists the new status via the update_event_booking_status web service.\n *\n * @module mod_bookit/overview/booking_status_dropdown\n * @copyright 2026 ssystems GmbH \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nconst SELECTOR = 'select[data-action=\"update-booking-status\"]';\nconst BUTTON_SELECTOR = 'button[data-action=\"set-booking-status\"]';\n\n/**\n * Apply status colour to a select element by reading data attributes from the selected option.\n *\n * @param {HTMLSelectElement} select\n */\nconst applyColor = (select) => {\n const opt = select.options[select.selectedIndex];\n const bg = (opt && opt.dataset.bg) ? opt.dataset.bg : '#ffffff';\n const fg = (opt && opt.dataset.fg) ? opt.dataset.fg : '#000000';\n select.style.backgroundColor = bg;\n select.style.color = fg;\n const td = select.closest('td');\n if (td) {\n td.style.backgroundColor = bg;\n }\n};\n\n/**\n * Initialise the dropdown listener on the overview table.\n */\nexport const init = () => {\n // Apply initial colours to all existing dropdowns on the page.\n document.querySelectorAll(SELECTOR).forEach((select) => {\n applyColor(select);\n });\n\n document.addEventListener('change', (e) => {\n const select = e.target.closest(SELECTOR);\n if (!select) {\n return;\n }\n\n const cmid = parseInt(select.dataset.cmid, 10);\n const eventid = parseInt(select.dataset.eventid, 10);\n const status = parseInt(select.value, 10);\n\n select.disabled = true;\n\n Ajax.call([{\n methodname: 'mod_bookit_update_event_booking_status',\n args: {cmid, eventid, status},\n }])[0]\n .then(() => {\n applyColor(select);\n window.location.reload();\n return null;\n })\n .catch((err) => {\n select.disabled = false;\n Notification.exception(err);\n });\n });\n\n document.addEventListener('click', (e) => {\n const button = e.target.closest(BUTTON_SELECTOR);\n if (!button) {\n return;\n }\n\n const cmid = parseInt(button.dataset.cmid, 10);\n const eventid = parseInt(button.dataset.eventid, 10);\n const status = parseInt(button.dataset.status, 10);\n\n button.disabled = true;\n\n Ajax.call([{\n methodname: 'mod_bookit_update_event_booking_status',\n args: {cmid, eventid, status},\n }])[0]\n .then(() => {\n window.location.reload();\n return null;\n })\n .catch((err) => {\n button.disabled = false;\n Notification.exception(err);\n });\n });\n};\n"],"names":["SELECTOR","applyColor","select","opt","options","selectedIndex","bg","dataset","fg","style","backgroundColor","color","td","closest","document","querySelectorAll","forEach","addEventListener","e","target","cmid","parseInt","eventid","status","value","disabled","call","methodname","args","then","window","location","reload","catch","err","exception","button"],"mappings":";;;;;;;;;;gLA6BMA,SAAW,8CAQXC,WAAcC,eACVC,IAAMD,OAAOE,QAAQF,OAAOG,eAC5BC,GAAMH,KAAOA,IAAII,QAAQD,GAAMH,IAAII,QAAQD,GAAK,UAChDE,GAAML,KAAOA,IAAII,QAAQC,GAAML,IAAII,QAAQC,GAAK,UACtDN,OAAOO,MAAMC,gBAAkBJ,GAC/BJ,OAAOO,MAAME,MAAQH,SACfI,GAAKV,OAAOW,QAAQ,MACtBD,KACAA,GAAGH,MAAMC,gBAAkBJ,mBAOf,KAEhBQ,SAASC,iBAAiBf,UAAUgB,SAASd,SACzCD,WAAWC,WAGfY,SAASG,iBAAiB,UAAWC,UAC3BhB,OAASgB,EAAEC,OAAON,QAAQb,cAC3BE,oBAICkB,KAAOC,SAASnB,OAAOK,QAAQa,KAAM,IACrCE,QAAUD,SAASnB,OAAOK,QAAQe,QAAS,IAC3CC,OAASF,SAASnB,OAAOsB,MAAO,IAEtCtB,OAAOuB,UAAW,gBAEbC,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,CAACR,KAAAA,KAAME,QAAAA,QAASC,OAAAA,WACtB,GACHM,MAAK,KACF5B,WAAWC,QACX4B,OAAOC,SAASC,SACT,QAEVC,OAAOC,MACJhC,OAAOuB,UAAW,wBACLU,UAAUD,WAI/BpB,SAASG,iBAAiB,SAAUC,UAC1BkB,OAASlB,EAAEC,OAAON,QAxDR,gDAyDXuB,oBAIChB,KAAOC,SAASe,OAAO7B,QAAQa,KAAM,IACrCE,QAAUD,SAASe,OAAO7B,QAAQe,QAAS,IAC3CC,OAASF,SAASe,OAAO7B,QAAQgB,OAAQ,IAE/Ca,OAAOX,UAAW,gBAEbC,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,CAACR,KAAAA,KAAME,QAAAA,QAASC,OAAAA,WACtB,GACHM,MAAK,KACFC,OAAOC,SAASC,SACT,QAEVC,OAAOC,MACJE,OAAOX,UAAW,wBACLU,UAAUD"} \ No newline at end of file diff --git a/amd/src/available_calendar.js b/amd/src/available_calendar.js index 47cc3d15..8adb548f 100644 --- a/amd/src/available_calendar.js +++ b/amd/src/available_calendar.js @@ -60,7 +60,7 @@ export async function init(eventsource, capabilities, lang) { var calendar; - calendar = new window.EventCalendar(document.getElementById('ec'), { + calendar = window.EventCalendar.create(document.getElementById('ec'), { locale: lang, view: viewType, firstDay: 1, @@ -177,4 +177,4 @@ function deleteBlocker(blockerId) { blockerid: blockerId } }])[0]; -} \ No newline at end of file +} diff --git a/amd/src/calendar.js b/amd/src/calendar.js index e0b77914..7f547b5e 100644 --- a/amd/src/calendar.js +++ b/amd/src/calendar.js @@ -92,7 +92,7 @@ export async function init(cmid, eventsource, capabilities, lang, config) { // Runtime filter parameters – mutable via bookitCalendarUpdate() let extraFilterParams = {}; // {room:123, status:2, faculty:'ENG', …} - const calendar = new window.EventCalendar(document.getElementById('ec'), { + const calendar = window.EventCalendar.create(document.getElementById('ec'), { /* Appearance / behaviour */ locale: lang, view: viewType, @@ -165,11 +165,10 @@ export async function init(cmid, eventsource, capabilities, lang, config) { return; } - let d = new Date(); - let dateoff = new Date(d.setMinutes(d.getMinutes() - d.getTimezoneOffset())); let startdate = info.dateStr; + const isFutureSlot = info.date.getTime() > Date.now(); - if (capabilities.addevent && startdate > dateoff.toISOString()) { + if (capabilities.addevent && isFutureSlot) { const modalForm = new ModalForm({ formClass: 'mod_bookit\\form\\edit_event_form', args: { diff --git a/amd/src/export_modal.js b/amd/src/export_modal.js index e950a4a4..73e3b81d 100644 --- a/amd/src/export_modal.js +++ b/amd/src/export_modal.js @@ -1,14 +1,50 @@ -define(['jquery', 'core/str'], function($, str) { +define(['jquery', 'core/notification', 'core/str'], function($, Notification, str) { return { init: function(cmId) { const stringKeys = [ {key: 'noevents', component: 'mod_bookit'}, - {key: 'chooseevent', component: 'mod_bookit'} + {key: 'chooseevent', component: 'mod_bookit'}, + {key: 'exportevents_selected', component: 'mod_bookit'}, + {key: 'event_bookingstatus_0', component: 'mod_bookit'}, + {key: 'event_bookingstatus_1', component: 'mod_bookit'}, + {key: 'event_bookingstatus_2', component: 'mod_bookit'}, + {key: 'event_bookingstatus_3', component: 'mod_bookit'}, + {key: 'event_bookingstatus_4', component: 'mod_bookit'} ]; str.get_strings(stringKeys).done(function(strings) { const noEventsStr = strings[0]; + const chooseEventStr = strings[1]; + const selectedLabel = strings[2]; + const statusMap = { + '0': strings[3], + '1': strings[4], + '2': strings[5], + '3': strings[6], + '4': strings[7] + }; + + /** + * Render the localized selected-count label for the export modal. + * + * @param {Number} count Number of currently selected events. + * @returns {String} + */ + function formatSelectedCount(count) { + return selectedLabel + ': ' + String(count); + } + + /** + * Synchronise the export button state and selected-count badge. + * + * @returns {void} + */ + function updateSelectionState() { + const selected = $('#bookit-export-list input[type=checkbox]:enabled:checked').length; + $('#bookit-export-selection-count').text(formatSelectedCount(selected)); + $('#bookit-export-confirm').prop('disabled', selected === 0); + } /** * Convert a date-like value to a local (browser timezone) `YYYY-MM-DD` string. @@ -122,35 +158,50 @@ define(['jquery', 'core/str'], function($, str) { return; } - const statusMap = {'0': 'New', '1': 'In progress', '2': 'Accepted', '3': 'Cancelled', '4': 'Rejected'}; - data.forEach(function(e) { const roomTxt = (e.location || e.room || e.roomname || '').trim(); const faculty = (e.department || e.faculty || '').trim(); const statusTxt = statusMap[String(e.bookingstatus ?? '')] || ''; const startStr = (e.start || ''); const dateTxt = startStr ? String(startStr).substr(0, 16).replace('T', ' ') : ''; - const metaLine = roomTxt ? (roomTxt + ' ' + dateTxt) : dateTxt; + const metaParts = [dateTxt]; + if (roomTxt) { + metaParts.push(roomTxt); + } + if (faculty) { + metaParts.push(faculty); + } + const metaLine = metaParts.filter(Boolean).join(' | '); const checkbox = ''; + const statusBadge = statusTxt + ? '' + statusTxt + '' + : ''; const row = $( - '' ); list.append(row); }); filterExportList(); + updateSelectionState(); }).fail(function(xhr) { list.empty(); list.append('
events.php failed: ' + (xhr.responseText || xhr.status) + '
'); + updateSelectionState(); }); } /** @@ -190,13 +241,17 @@ define(['jquery', 'core/str'], function($, str) { // Select all visible, enabled event checkboxes. $(document).on('click', '#bookit-check-all', function() { $('#bookit-export-list label:visible input[type=checkbox]:enabled').prop('checked', true); + updateSelectionState(); }); // Deselect all visible, enabled event checkboxes. $(document).on('click', '#bookit-uncheck-all', function() { $('#bookit-export-list label:visible input[type=checkbox]:enabled').prop('checked', false); + updateSelectionState(); }); + $(document).on('change', '#bookit-export-list input[type=checkbox]', updateSelectionState); + /** * Start export for selected event ids and close the modal. * @@ -209,6 +264,7 @@ define(['jquery', 'core/str'], function($, str) { }).get(); if (!ids.length) { + void Notification.alert('', chooseEventStr); return; } @@ -221,6 +277,8 @@ define(['jquery', 'core/str'], function($, str) { window.location = M.cfg.wwwroot + '/mod/bookit/export_events.php?' + qs.toString(); $('#bookit-export-modal').modal('hide'); }); + + updateSelectionState(); }); } }; diff --git a/amd/src/overview/booking_status_dropdown.js b/amd/src/overview/booking_status_dropdown.js index 4926b3de..715d0a29 100644 --- a/amd/src/overview/booking_status_dropdown.js +++ b/amd/src/overview/booking_status_dropdown.js @@ -28,6 +28,7 @@ import Ajax from 'core/ajax'; import Notification from 'core/notification'; const SELECTOR = 'select[data-action="update-booking-status"]'; +const BUTTON_SELECTOR = 'button[data-action="set-booking-status"]'; /** * Apply status colour to a select element by reading data attributes from the selected option. @@ -73,11 +74,37 @@ export const init = () => { }])[0] .then(() => { applyColor(select); + window.location.reload(); + return null; + }) + .catch((err) => { select.disabled = false; + Notification.exception(err); + }); + }); + + document.addEventListener('click', (e) => { + const button = e.target.closest(BUTTON_SELECTOR); + if (!button) { return; + } + + const cmid = parseInt(button.dataset.cmid, 10); + const eventid = parseInt(button.dataset.eventid, 10); + const status = parseInt(button.dataset.status, 10); + + button.disabled = true; + + Ajax.call([{ + methodname: 'mod_bookit_update_event_booking_status', + args: {cmid, eventid, status}, + }])[0] + .then(() => { + window.location.reload(); + return null; }) .catch((err) => { - select.disabled = false; + button.disabled = false; Notification.exception(err); }); }); diff --git a/classes/external/get_possible_starttimes.php b/classes/external/get_possible_starttimes.php index 95a2e235..cef00e4d 100644 --- a/classes/external/get_possible_starttimes.php +++ b/classes/external/get_possible_starttimes.php @@ -49,6 +49,18 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_possible_starttimes extends external_api { + /** + * Check whether a booking start timestamp lies in the past. + * + * @param int $timestamp + * @param int|null $referencetime + * @return bool + */ + public static function is_starttime_in_past(int $timestamp, ?int $referencetime = null): bool { + $referencetime ??= time(); + return $timestamp < $referencetime; + } + /** * Description for get_possible_slots parameters. * @@ -198,6 +210,12 @@ public static function list_possible_starttimes(DateTime $date, int $duration, i } } + $starttimes = array_filter( + $starttimes, + static fn(int $time): bool => !self::is_starttime_in_past($time), + ARRAY_FILTER_USE_KEY + ); + if (empty($starttimes)) { return [[], 0]; } diff --git a/classes/external/update_event_booking_status.php b/classes/external/update_event_booking_status.php index 3142bf10..a0c7b118 100644 --- a/classes/external/update_event_booking_status.php +++ b/classes/external/update_event_booking_status.php @@ -29,7 +29,8 @@ use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; -use mod_bookit\local\entity\bookit_event; +use mod_bookit\local\manager\booking_notification_manager; +use mod_bookit\local\manager\event_access_manager; defined('MOODLE_INTERNAL') || die; @@ -61,7 +62,7 @@ public static function execute_parameters(): external_function_parameters { * @return array */ public static function execute(int $cmid, int $eventid, int $status): array { - global $DB; + global $DB, $USER; $params = self::validate_parameters(self::execute_parameters(), [ 'cmid' => $cmid, @@ -74,14 +75,29 @@ public static function execute(int $cmid, int $eventid, int $status): array { self::validate_context($context); require_capability('mod/bookit:managebasics', $context); - $validstatuses = [0, 1, 2, 3, 4]; + $validstatuses = [ + event_access_manager::BOOKINGSTATUS_NEW, + event_access_manager::BOOKINGSTATUS_IN_PROGRESS, + event_access_manager::BOOKINGSTATUS_ACCEPTED, + event_access_manager::BOOKINGSTATUS_CANCELED, + event_access_manager::BOOKINGSTATUS_REJECTED, + ]; if (!in_array($params['status'], $validstatuses, true)) { throw new \invalid_parameter_exception('Invalid booking status: ' . $params['status']); } - $event = bookit_event::from_database($params['eventid']); + $event = $DB->get_record('bookit_event', ['id' => $params['eventid']], '*', MUST_EXIST); + $oldstatus = (int)($event->bookingstatus ?? event_access_manager::BOOKINGSTATUS_NEW); + + if (!event_access_manager::can_transition_booking_status($oldstatus, $params['status'])) { + throw new \invalid_parameter_exception('Invalid booking workflow transition.'); + } + $event->bookingstatus = $params['status']; - $event->save(); + $event->usermodified = (int)$USER->id; + $event->timemodified = time(); + $DB->update_record('bookit_event', $event); + booking_notification_manager::notify_status_changed($params['cmid'], $params['eventid'], $oldstatus, $params['status']); return ['status' => $params['status']]; } diff --git a/classes/form/edit_event_form.php b/classes/form/edit_event_form.php index f26629db..0d4e91f6 100644 --- a/classes/form/edit_event_form.php +++ b/classes/form/edit_event_form.php @@ -35,6 +35,7 @@ use dml_exception; use mod_bookit\local\entity\bookit_event; use mod_bookit\local\entity\resource\bookit_resource_status; +use mod_bookit\local\manager\event_access_manager; use mod_bookit\local\manager\event_manager; use mod_bookit\local\manager\resource_manager; use mod_bookit\local\persistent\institution; @@ -63,7 +64,7 @@ class edit_event_form extends dynamic_form { * Define the form */ public function definition(): void { - global $DB, $CFG, $PAGE; + global $DB, $CFG, $PAGE, $USER; $mform =& $this->_form; // Get the plugin config. @@ -75,6 +76,26 @@ public function definition(): void { // Define variables. $context = $this->get_context_for_dynamic_submission(); $caneditinternal = has_capability('mod/bookit:editinternal', $context); + $eventid = $this->optional_param('id', null, PARAM_INT); + $existingevent = !empty($eventid) ? event_manager::get_event($eventid) : null; + $caneditevent = has_capability('mod/bookit:editevent', $context) + || empty($eventid) + || ($existingevent && event_access_manager::can_participant_edit_event($existingevent, (int)$USER->id)); + $canviewrestrictedfields = $caneditinternal + || ($existingevent && event_access_manager::can_supportperson_view_internal_fields( + $existingevent, + $context, + (int)$USER->id + )); + $caneditinternalnotes = $caneditinternal + || ($existingevent && event_access_manager::can_supportperson_edit_internal_notes( + $existingevent, + $context, + (int)$USER->id + )); + $cancancelonly = $existingevent + && event_access_manager::can_participant_cancel_only($existingevent, $context, (int)$USER->id); + $showbookingstatus = $caneditinternal || $canviewrestrictedfields || $cancancelonly; $cmid = $this->_ajaxformdata['cmid'] ?? false; $course = get_course_and_cm_from_cmid($cmid); $contextcourse = context_course::instance($course[0]->id); @@ -87,6 +108,20 @@ public function definition(): void { $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); + // Hidden permission flags used by dynamic visibility rules. + $mform->addElement('hidden', 'editevent', (int)$caneditevent); + $mform->setType('editevent', PARAM_BOOL); + $mform->addElement('hidden', 'editinternal', (int)$caneditinternal); + $mform->setType('editinternal', PARAM_BOOL); + $mform->addElement('hidden', 'viewrestrictedfields', (int)$canviewrestrictedfields); + $mform->setType('viewrestrictedfields', PARAM_BOOL); + $mform->addElement('hidden', 'editinternalnotes', (int)$caneditinternalnotes); + $mform->setType('editinternalnotes', PARAM_BOOL); + $mform->addElement('hidden', 'editbookingstatus', (int)($caneditinternal || $cancancelonly)); + $mform->setType('editbookingstatus', PARAM_BOOL); + $mform->addElement('hidden', 'cancelonly', (int)$cancancelonly); + $mform->setType('cancelonly', PARAM_BOOL); + // Show the user who created the entry. $mform->addElement('text', 'usermodified', get_string('event_usermodified', 'mod_bookit')); $mform->setType('usermodified', PARAM_TEXT); @@ -134,6 +169,7 @@ public function definition(): void { } $mform->addElement('select', 'institutionid', get_string('event_department', 'mod_bookit'), $institutionoptions); + $mform->disabledIf('institutionid', 'editevent', 'neq'); $mform->addRule('institutionid', null, 'required', null, 'client'); $mform->addHelpButton('institutionid', 'event_department', 'mod_bookit'); @@ -191,10 +227,12 @@ public function definition(): void { $starttimearray['stopyear'] = $config->eventmaxyear ?? (date("Y") + 1); $mform->addElement('date_selector', 'startdate', get_string('event_start', 'mod_bookit'), $starttimearray); + $mform->disabledIf('startdate', 'editevent', 'neq'); $mform->addRule('startdate', null, 'required', null, 'client'); $mform->addHelpButton('startdate', 'event_start', 'mod_bookit'); $mform->addElement('select', 'starttime'); + $mform->disabledIf('starttime', 'editevent', 'neq'); $mform->addRule('starttime', null, 'required', null, 'client'); $mform->addElement('static', 'starttime_explanation', '', ''); @@ -331,7 +369,7 @@ public function definition(): void { $mform->hideIf('refcourseid', 'editinternal', 'neq'); $mform->addHelpButton('refcourseid', 'event_refcourseid', 'mod_bookit'); - if ($caneditinternal) { + if ($canviewrestrictedfields) { // Add the "supportpersons" field. $supportpersons = []; // ...@TODO: Find better query to select users! @@ -355,44 +393,94 @@ public function definition(): void { ); $mform->setType('supportpersons', PARAM_TEXT); $mform->addHelpButton('supportpersons', 'event_supportperson', 'mod_bookit'); + $mform->disabledIf('supportpersons', 'editinternal', 'neq', 1); } else { $mform->addElement('hidden', 'supportpersons'); $mform->setType('supportpersons', PARAM_TEXT); } - if ($caneditinternal) { + if ($canviewrestrictedfields) { // Don't use PARAM_INT, because it converts an empty text field to 0. // In our case, an empty field should mean be the inherited default. $mform->addElement('text', 'extratimebefore', get_string('settings_extratime_before', 'mod_bookit')); $mform->setType('extratimebefore', PARAM_ALPHANUM); $mform->addRule('extratimebefore', null, 'numeric', null, 'client'); + $mform->disabledIf('extratimebefore', 'editinternal', 'neq', 1); $mform->addElement('text', 'extratimeafter', get_string('settings_extratime_after', 'mod_bookit')); $mform->setType('extratimeafter', PARAM_ALPHANUM); $mform->addRule('extratimeafter', null, 'numeric', null, 'client'); + $mform->disabledIf('extratimeafter', 'editinternal', 'neq', 1); } else { $mform->addElement('hidden', 'extratimebefore'); $mform->addElement('hidden', 'extratimeafter'); } + $bookingstatusoptions = []; + $currentbookingstatus = (int)($existingevent->bookingstatus ?? self::BOOKINGSTATUS_NEW); + if ($caneditinternal) { + $bookingstatusoptions = explode(',', get_string('event_bookingstatus_list', 'mod_bookit')); + } else if ($showbookingstatus) { + $bookingstatusoptions[$currentbookingstatus] = get_string('event_bookingstatus_' . $currentbookingstatus, 'mod_bookit'); + if ( + $cancancelonly + && event_access_manager::can_transition_booking_status( + $currentbookingstatus, + event_access_manager::BOOKINGSTATUS_CANCELED + ) + ) { + $bookingstatusoptions[event_access_manager::BOOKINGSTATUS_CANCELED] = get_string( + 'event_bookingstatus_' . event_access_manager::BOOKINGSTATUS_CANCELED, + 'mod_bookit' + ); + } + } + // Add the "bookingstatus" field. - $mform->addElement( - 'select', - 'bookingstatus', - get_string('event_bookingstatus', 'mod_bookit'), - explode(',', get_string('event_bookingstatus_list', 'mod_bookit')) - ); - $mform->hideIf('bookingstatus', 'editinternal', 'neq'); - $mform->addHelpButton('bookingstatus', 'event_bookingstatus', 'mod_bookit'); + if ($showbookingstatus) { + $mform->addElement( + 'select', + 'bookingstatus', + get_string('event_bookingstatus', 'mod_bookit'), + $bookingstatusoptions + ); + $mform->disabledIf('bookingstatus', 'editbookingstatus', 'neq', 1); + $mform->addHelpButton('bookingstatus', 'event_bookingstatus', 'mod_bookit'); + } else { + $mform->addElement('hidden', 'bookingstatus'); + $mform->setType('bookingstatus', PARAM_INT); + } + + if ($cancancelonly) { + $mform->addElement( + 'static', + 'cancelonlynotice', + '', + get_string('event_cancel_only_notice', 'mod_bookit') + ); + } // Add the "internalnotes" field. - $mform->addElement( - 'textarea', - 'internalnotes', - get_string("event_internalnotes", "mod_bookit"), - 'wrap="virtual" rows="5" cols="50"' - ); - $mform->hideIf('internalnotes', 'editinternal', 'neq'); - $mform->addHelpButton('internalnotes', 'event_internalnotes', 'mod_bookit'); + if ($canviewrestrictedfields || $caneditinternalnotes) { + $mform->addElement( + 'textarea', + 'internalnotes', + get_string("event_internalnotes", "mod_bookit"), + 'wrap="virtual" rows="5" cols="50"' + ); + $mform->disabledIf('internalnotes', 'editinternalnotes', 'neq', 1); + $mform->addHelpButton('internalnotes', 'event_internalnotes', 'mod_bookit'); + if (!$caneditinternal && $caneditinternalnotes) { + $mform->addElement( + 'static', + 'supportpersoninternalnotesnotice', + '', + get_string('event_supportperson_internalnotes_notice', 'mod_bookit') + ); + } + } else { + $mform->addElement('hidden', 'internalnotes'); + $mform->setType('internalnotes', PARAM_TEXT); + } $timeclicked = $this->optional_param('timeclicked', null, PARAM_TEXT); $possiblestarttimes = []; @@ -462,56 +550,15 @@ public function definition_after_data(): void { global $DB, $USER, $PAGE; // The $PAGE is needed for JS injection. $mform =& $this->_form; $data = $this->get_submitted_data() ?? $this->event; + $caneditinternal = (bool)($mform->getElementValue('editinternal')[0] ?? 0); $context = $this->get_context_for_dynamic_submission(); - $usermodified = $this->_form->getElementValue('usermodified'); - $examiner = $this->_form->getElementValue('personinchargeid'); - $otherexaminers = $this->_form->getElementValue('otherexaminers') ?? []; - array_push($otherexaminers, $usermodified, $examiner); - - // Show the user who created the entry. - $user = $DB->get_record('user', ['id' => $usermodified]); + $creatorid = $this->_form->getElementValue('usermodified'); + $user = $DB->get_record('user', ['id' => $creatorid]); $mform->getElement('usermodified')->setValue( fullname($user, has_capability('moodle/site:viewfullnames', $context)) // ...TODO: find better way? ); - // Derive current booking-status & capability flags. - $rawstatus = $mform->getElementValue('bookingstatus'); - $bookingstat = is_array($rawstatus) ? (int) $rawstatus[0] : self::BOOKINGSTATUS_NEW; - - $id = $mform->getElementValue('id'); - $usermodified = $mform->getElementValue('usermodified'); - $examiner = $mform->getElementValue('personinchargeid'); - $otherexaminers = array_filter(array_merge( - $mform->getElementValue('otherexaminers') ?? [], - [$usermodified, $examiner] - )); - - $context = $this->get_context_for_dynamic_submission(); - $caneditevent = has_capability('mod/bookit:editevent', $context) - || !$id - || (self::BOOKINGSTATUS_NEW == $bookingstat && in_array($USER->id, $otherexaminers, true)); - $caneditinternal = has_capability('mod/bookit:editinternal', $context); - - // Store capability flags as hidden elements. - $mform->insertElementBefore( - $mform->createElement( - 'hidden', - 'editevent', - $caneditevent - ), - 'name' - )->setType('editevent', PARAM_BOOL); - - $mform->insertElementBefore( - $mform->createElement( - 'hidden', - 'editinternal', - $caneditinternal - ), - 'name' - )->setType('editinternal', PARAM_BOOL); - // Week-day validation – server side. $mform->addRule( 'starttime', @@ -567,7 +614,8 @@ function ($val): bool { } } - if ($data && $data->roomid && !is_null($data->duration)) { + if ($data && $data->roomid && !is_null($data->duration) && $DB->record_exists('bookit_room', ['id' => $data->roomid])) { + $excepteventid = (int)($data->id ?? $this->event->id ?? 0) ?: null; /** @var \MoodleQuickForm_select $starttimeel */ $starttimeel = $mform->getElement('starttime'); $starttimeel->removeOptions(); @@ -575,7 +623,7 @@ function ($val): bool { (new \DateTime())->setTimestamp($data->startdate), $data->duration, $data->roomid, - $id + $excepteventid ); $starttimeel->loadArray($possiblestarttimes); } @@ -615,7 +663,22 @@ protected function get_context_for_dynamic_submission(): context { * Checks if current user has access to this form, otherwise throws exception */ protected function check_access_for_dynamic_submission(): void { - // ...@TODO. Does this look like Code codechecker?! + global $USER; + + $context = $this->get_context_for_dynamic_submission(); + $eventid = $this->optional_param('id', null, PARAM_INT); + if (empty($eventid)) { + return; + } + + $event = event_manager::get_event($eventid); + if ( + !event_access_manager::can_user_view_event_details($event, $context, (int)$USER->id) + && !event_access_manager::can_manage_open_requests($context) + && !has_capability('mod/bookit:editevent', $context) + ) { + throw new moodle_exception('nopermissions', 'error', '', 'view event details'); + } } /** @@ -625,38 +688,97 @@ protected function check_access_for_dynamic_submission(): void { * @throws dml_exception|coding_exception */ public function process_dynamic_submission(): array { + global $USER; $formdata = $this->get_data(); + $context = $this->get_context_for_dynamic_submission(); + $currentevent = null; + $submittedstarttime = $this->optional_param('starttime', null, PARAM_INT); + $submittedduration = $this->optional_param('duration', null, PARAM_INT); + $caneditpublic = empty($formdata->id) || has_capability('mod/bookit:editevent', $context); + $caneditinternal = has_capability('mod/bookit:editinternal', $context); + $caneditinternalnotes = $caneditinternal; + $caneditbookingstatus = $caneditinternal; + + if (!empty($formdata->id)) { + $currentevent = bookit_event::from_database((int)$formdata->id); + $currentrecord = event_manager::get_event((int)$formdata->id); + $caneditpublic = $caneditpublic + || event_access_manager::can_participant_edit_event($currentrecord, (int)$USER->id); + $caneditinternalnotes = $caneditinternal + || event_access_manager::can_supportperson_edit_internal_notes($currentrecord, $context, (int)$USER->id); + $caneditbookingstatus = $caneditinternal + || event_access_manager::can_participant_cancel_only($currentrecord, $context, (int)$USER->id); + + if (!$caneditpublic && !$caneditinternalnotes && !$caneditbookingstatus) { + throw new moodle_exception('nopermissions', 'error', '', 'update event'); + } + } $mappings = []; - foreach (resource_manager::get_active_resources_grouped() as $categorygroup) { - // Rooms. - foreach ($categorygroup['resources'] as $resource) { - $id = $resource['id']; - if ($categorygroup['category']['name'] === 'Rooms') { - if ($formdata->room == $id) { - $mappings[] = (object) [ - 'resourceid' => $formdata->room, - 'amount' => 1, - ]; - } - } else { - // Other Resources. - $checkboxname = 'checkbox_' . $id; - if ($formdata->$checkboxname ?? false) { - // Amountirrelevant resources have no amount input; store 1 as neutral value. - $amount = $resource['amountirrelevant'] ? 1 : (int)($formdata->{'resource_' . $id} ?? 1); - $mappings[] = (object) [ - 'resourceid' => $id, - 'amount' => $amount, - ]; + if ($caneditpublic) { + foreach (resource_manager::get_active_resources_grouped() as $categorygroup) { + // Rooms. + foreach ($categorygroup['resources'] as $resource) { + $id = $resource['id']; + if ($categorygroup['category']['name'] === 'Rooms') { + if (($formdata->roomid ?? $formdata->room ?? null) == $id) { + $mappings[] = (object) [ + 'resourceid' => (int)($formdata->roomid ?? $formdata->room), + 'amount' => 1, + ]; + } + } else { + // Other Resources. + $checkboxname = 'checkbox_' . $id; + if ($formdata->$checkboxname ?? false) { + // Amountirrelevant resources have no amount input; store 1 as neutral value. + $amount = $resource['amountirrelevant'] ? 1 : (int)($formdata->{'resource_' . $id} ?? 1); + $mappings[] = (object) [ + 'resourceid' => $id, + 'amount' => $amount, + ]; + } } } } + } else if ($currentevent) { + $mappings = $currentevent->resources; } $formdata->resources = $mappings; - // Calculate endtime. - $formdata->endtime = $formdata->starttime + $formdata->duration * 60; + if ($caneditpublic) { + $resolvedstarttime = $submittedstarttime ?? ($formdata->starttime ?? null); + $resolvedduration = $submittedduration ?? ($formdata->duration ?? null) ?? $currentevent?->duration; + + if ($resolvedstarttime === null || $resolvedduration === null) { + throw new moodle_exception('missingrequiredfield', 'error'); + } + + if (get_possible_starttimes::is_starttime_in_past((int)$resolvedstarttime)) { + throw new moodle_exception('event_error_mintime', 'mod_bookit'); + } + + $formdata->starttime = (int)$resolvedstarttime; + $formdata->duration = (int)$resolvedduration; + // Calculate endtime. + $formdata->endtime = $formdata->starttime + $formdata->duration * 60; + } else if ($currentevent) { + $formdata->starttime = $currentevent->starttime; + $formdata->endtime = $currentevent->endtime; + $formdata->duration = $currentevent->duration; + $formdata->semester = $currentevent->semester; + $formdata->institutionid = $currentevent->institutionid; + $formdata->roomid = $currentevent->roomid; + $formdata->participantsamount = $currentevent->participantsamount; + $formdata->timecompensation = $currentevent->timecompensation; + $formdata->compensationfordisadvantages = $currentevent->compensationfordisadvantages; + $formdata->personinchargeid = $currentevent->personinchargeid; + $formdata->otherexaminers = $currentevent->otherexaminers; + $formdata->coursetemplate = $currentevent->coursetemplate; + $formdata->notes = $currentevent->notes; + $formdata->refcourseid = $currentevent->refcourseid; + $formdata->name = $currentevent->name; + } if (is_array($formdata->supportpersons)) { $formdata->supportpersons = implode(',', array_filter($formdata->supportpersons)); @@ -678,6 +800,40 @@ public function process_dynamic_submission(): array { $formdata->extratimeafter = null; } + if ($currentevent && !$caneditinternal) { + $formdata->supportpersons = $currentevent->supportpersons; + $formdata->extratimebefore = $currentevent->extratimebefore; + $formdata->extratimeafter = $currentevent->extratimeafter; + if (!$caneditbookingstatus) { + $formdata->bookingstatus = $currentevent->bookingstatus; + } + } + + if ($currentevent && !$caneditinternalnotes) { + $formdata->internalnotes = $currentevent->internalnotes; + } + + if ($currentevent && $caneditbookingstatus && !$caneditinternal) { + $requestedstatus = (int)($formdata->bookingstatus ?? $currentevent->bookingstatus); + $allowedstatuses = [ + (int)$currentevent->bookingstatus, + ]; + if ( + event_access_manager::can_transition_booking_status( + (int)$currentevent->bookingstatus, + event_access_manager::BOOKINGSTATUS_CANCELED + ) + ) { + $allowedstatuses[] = event_access_manager::BOOKINGSTATUS_CANCELED; + } + + if (!in_array($requestedstatus, $allowedstatuses, true)) { + throw new moodle_exception('nopermissions', 'error', '', 'change booking status'); + } + + $formdata->bookingstatus = $requestedstatus; + } + $event = bookit_event::from_record($formdata); $event->save(); @@ -873,10 +1029,48 @@ private function add_resources_fields( */ #[\Override] public function validation($data, $files): array { + global $USER; $errors = parent::validation($data, $files); - $room = room::get_record(['id' => $data['roomid']]); - if ($room->get('seats') != 0 && $room->get('seats') < $data['participantsamount']) { + $context = $this->get_context_for_dynamic_submission(); + $existingevent = null; + $submittedstarttime = $this->_ajaxformdata['starttime'] ?? null; + if (!empty($data['id'])) { + $existingevent = event_manager::get_event((int)$data['id']); + $caneditpublic = has_capability('mod/bookit:editevent', $context) + || event_access_manager::can_participant_edit_event($existingevent, (int)$USER->id); + $caneditinternal = has_capability('mod/bookit:editinternal', $context); + $caneditinternalnotes = $caneditinternal + || event_access_manager::can_supportperson_edit_internal_notes($existingevent, $context, (int)$USER->id); + $cancancelonly = event_access_manager::can_participant_cancel_only($existingevent, $context, (int)$USER->id); + + if (!$caneditpublic && !$caneditinternalnotes && !$cancancelonly) { + $errors['name'] = get_string('nopermissions', 'error'); + return $errors; + } + + if (!$caneditpublic && $cancancelonly) { + $requestedstatus = (int)($data['bookingstatus'] ?? $existingevent->bookingstatus); + if ( + !in_array( + $requestedstatus, + [(int)$existingevent->bookingstatus, event_access_manager::BOOKINGSTATUS_CANCELED], + true + ) + ) { + $errors['bookingstatus'] = get_string('event_cancel_only_notice', 'mod_bookit'); + } + } + } + + $roomid = $data['roomid'] ?? $existingevent->roomid ?? null; + $participantsamount = $data['participantsamount'] ?? $existingevent->participantsamount ?? null; + $room = !empty($roomid) ? room::get_record(['id' => $roomid], IGNORE_MISSING) : null; + $starttime = $submittedstarttime ?? ($data['starttime'] ?? null); + if ($starttime !== null && get_possible_starttimes::is_starttime_in_past((int)$starttime)) { + $errors['starttime'] = get_string('event_error_mintime', 'mod_bookit'); + } + if ($room && $participantsamount !== null && $room->get('seats') != 0 && $room->get('seats') < $participantsamount) { $errors['roomid'] = get_string('room_doesnt_have_enough_seats', 'mod_bookit'); } diff --git a/classes/local/entity/bookit_event.php b/classes/local/entity/bookit_event.php index 95382980..aea66148 100644 --- a/classes/local/entity/bookit_event.php +++ b/classes/local/entity/bookit_event.php @@ -43,7 +43,7 @@ class bookit_event { * * @param int $id * @param string $name - * @param int $semester + * @param int|null $semester * @param int $institutionid * @param int $starttime * @param int $endtime @@ -68,56 +68,55 @@ class bookit_event { * @param array $resources */ public function __construct( - /** @var int id */ + /** @var int $id */ public int $id, - /** @var string name */ + /** @var string $name */ public string $name, - /** @var ?int semester */ - public int $semester, - /** @var int institutionid */ + /** @var int|null $semester */ + public ?int $semester, + /** @var int $institutionid */ public int $institutionid, - /** @var int starttime */ + /** @var int $starttime */ public int $starttime, - /** @var int $endtime endtime */ + /** @var int $endtime */ public int $endtime, - /** @var int $duration duration */ + /** @var int|null $duration */ public ?int $duration, - /** @var int roomid */ + /** @var int $roomid */ public int $roomid, - /** @var int $participantsamount participantsamount */ + /** @var int|null $participantsamount */ public ?int $participantsamount, - /** @var int $timecompensation timecompensation */ + /** @var int|null $timecompensation */ public ?int $timecompensation, - /** @var string $compensationfordisadvantages compensationfordisadvantages */ + /** @var string|null $compensationfordisadvantages */ public ?string $compensationfordisadvantages, - /** @var int $bookingstatus bookingstatus */ + /** @var int|null $bookingstatus */ public ?int $bookingstatus, - /** @var int $personinchargeid personinchargeid */ + /** @var int|null $personinchargeid */ public ?int $personinchargeid, - /** @var string $otherexaminers otherexaminers */ + /** @var string|null $otherexaminers */ public ?string $otherexaminers, - /** @var int $coursetemplate coursetemplate */ + /** @var int|null $coursetemplate */ public ?int $coursetemplate, - /** @var string $notes notes */ + /** @var string|null $notes */ public ?string $notes, - /** @var string $internalnotes internalnotes */ + /** @var string|null $internalnotes */ public ?string $internalnotes, - /** @var string $supportpersons supportpersons */ + /** @var string|null $supportpersons */ public ?string $supportpersons, - /** @var int $extratimebefore extratimebefore*/ + /** @var int $extratimebefore */ public int $extratimebefore, - /** @var int $extratimeafter extratimeafter*/ + /** @var int $extratimeafter */ public int $extratimeafter, - /** @var mixed $refcourseid refcourseid */ + /** @var mixed $refcourseid */ public mixed $refcourseid, - /** @var int $usermodified usermodified */ - /** @var ?int usermodified */ + /** @var int|null $usermodified */ public ?int $usermodified, - /** @var int $timecreated timecreated */ + /** @var int|null $timecreated */ public ?int $timecreated, - /** @var int $timemodified timemodified */ + /** @var int|null $timemodified */ public ?int $timemodified, - /** @var array $resources resources */ + /** @var array $resources */ public array $resources, ) { } @@ -157,7 +156,7 @@ public static function from_database(int $id): self { public static function from_record(array|object $record): self { $record = (object) $record; - $room = room::get_record(['id' => $record->roomid], MUST_EXIST); + $room = !empty($record->roomid) ? room::get_record(['id' => $record->roomid], IGNORE_MISSING) : null; return new self( $record->id ?? null, @@ -180,8 +179,8 @@ public static function from_record(array|object $record): self { $record->notes ?? null, $record->internalnotes ?? null, $record->supportpersons ?? null, - $record->extratimebefore ?? $room->get('extratimebefore') ?? get_config('mod_bookit', 'extratimebefore'), - $record->extratimeafter ?? $room->get('extratimeafter') ?? get_config('mod_bookit', 'extratimeafter'), + $record->extratimebefore ?? $room?->get('extratimebefore') ?? get_config('mod_bookit', 'extratimebefore'), + $record->extratimeafter ?? $room?->get('extratimeafter') ?? get_config('mod_bookit', 'extratimeafter'), $record->refcourseid ?? null, $record->usermodified ?? null, $record->timecreated ?? null, diff --git a/classes/local/manager/booking_notification_manager.php b/classes/local/manager/booking_notification_manager.php new file mode 100644 index 00000000..12df252b --- /dev/null +++ b/classes/local/manager/booking_notification_manager.php @@ -0,0 +1,319 @@ +. + +/** + * Notification manager for booking status changes. + * + * @package mod_bookit + * @copyright 2026 ssystems GmbH + * @author Andreas Rosenthal + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_bookit\local\manager; + +use core_user; +use dml_exception; +use moodle_url; +use stdClass; + +/** + * Sends Moodle messages for booking status changes. + */ +class booking_notification_manager { + /** + * Notify configured recipients after a booking status change. + * + * @param int $cmid + * @param int $eventid + * @param int $oldstatus + * @param int $newstatus + * @return int + * @throws dml_exception + */ + public static function notify_status_changed(int $cmid, int $eventid, int $oldstatus, int $newstatus): int { + global $DB; + + $event = $DB->get_record('bookit_event', ['id' => $eventid]); + if (!$event) { + return 0; + } + + $recipientids = self::collect_recipient_ids($event); + $template = self::get_template_data($event, $cmid, $oldstatus, $newstatus); + $sent = 0; + + if (!empty($recipientids)) { + $sent += self::send_user_notifications($recipientids, $template, $cmid); + } + + $serviceaddresses = self::get_service_addresses(); + if (!empty($serviceaddresses)) { + $sent += self::send_service_notifications($serviceaddresses, $template); + } + + return $sent; + } + + /** + * Collect all configured user recipients from the event. + * + * @param stdClass $event + * @return int[] + */ + private static function collect_recipient_ids(stdClass $event): array { + $recipientids = []; + + if ((int)get_config('mod_bookit', 'bookingstatus_notify_bookingperson') !== 0 && !empty($event->usermodified)) { + $recipientids[] = (int)$event->usermodified; + } + + if ((int)get_config('mod_bookit', 'bookingstatus_notify_personincharge') !== 0 && !empty($event->personinchargeid)) { + $recipientids[] = (int)$event->personinchargeid; + } + + if ((int)get_config('mod_bookit', 'bookingstatus_notify_otherexaminers') !== 0 && !empty($event->otherexaminers)) { + foreach (explode(',', $event->otherexaminers) as $rawid) { + $id = (int)trim($rawid); + if ($id > 0) { + $recipientids[] = $id; + } + } + } + + return array_values(array_unique($recipientids)); + } + + /** + * Get configured service email addresses. + * + * @return string[] + */ + private static function get_service_addresses(): array { + $raw = trim((string)get_config('mod_bookit', 'bookingstatus_service_addresses')); + if ($raw === '') { + return []; + } + + $addresses = preg_split('/[\s,;]+/', $raw); + $addresses = array_filter(array_map('trim', $addresses)); + return array_values(array_unique($addresses)); + } + + /** + * Prepare replacement data and fallback templates. + * + * @param stdClass $event + * @param int $cmid + * @param int $oldstatus + * @param int $newstatus + * @return stdClass + */ + private static function get_template_data(stdClass $event, int $cmid, int $oldstatus, int $newstatus): stdClass { + $url = new moodle_url('/mod/bookit/view.php', ['id' => $cmid, 'eventid' => $event->id]); + + $data = new stdClass(); + $data->eventname = $event->name ?? ''; + $data->bookingstatus = get_string('event_bookingstatus_' . $newstatus, 'mod_bookit'); + $data->oldbookingstatus = get_string('event_bookingstatus_' . $oldstatus, 'mod_bookit'); + $data->eventurl = $url->out(false); + $data->room = self::get_room_name((int)($event->roomid ?? 0)); + $data->starttime = userdate((int)($event->starttime ?? 0)); + $data->endtime = userdate((int)($event->endtime ?? 0)); + $data->bookingperson = self::get_user_name((int)($event->usermodified ?? 0)); + $data->personincharge = self::get_user_name((int)($event->personinchargeid ?? 0)); + $data->otherexaminers = self::get_user_names_csv((string)($event->otherexaminers ?? '')); + $data->subject = trim((string)get_config('mod_bookit', 'bookingstatus_subject_' . $newstatus)); + $data->body = trim((string)get_config('mod_bookit', 'bookingstatus_body_' . $newstatus)); + + if ($data->subject === '') { + $data->subject = get_string('bookingstatus_notification_subject_default', 'mod_bookit', $data); + } + + if ($data->body === '') { + $data->body = get_string('bookingstatus_notification_body_default', 'mod_bookit', $data); + } + + return $data; + } + + /** + * Send notifications to user accounts via Moodle messages. + * + * @param int[] $recipientids + * @param stdClass $template + * @param int $cmid + * @return int + */ + private static function send_user_notifications(array $recipientids, stdClass $template, int $cmid): int { + $userfrom = core_user::get_noreply_user(); + $sent = 0; + + foreach ($recipientids as $recipientid) { + $userto = core_user::get_user($recipientid); + if (!$userto || !$userto->id || $userto->deleted || $userto->suspended) { + continue; + } + + $oldlang = force_current_language($userto->lang); + $subject = self::replace_placeholders($template->subject, $template); + $body = self::replace_placeholders($template->body, $template); + force_current_language($oldlang); + + $message = new \core\message\message(); + $message->component = 'mod_bookit'; + $message->name = 'bookit_booking_status_changed'; + $message->userfrom = $userfrom; + $message->userto = $userto; + $message->subject = $subject; + $message->fullmessage = $body; + $message->fullmessageformat = FORMAT_PLAIN; + $message->fullmessagehtml = text_to_html($body, false, false, true); + $message->smallmessage = $subject; + $message->notification = 1; + $message->contexturl = $template->eventurl; + $message->contexturlname = $template->eventname; + $message->courseid = self::get_course_id($cmid); + + message_send($message); + $sent++; + } + + return $sent; + } + + /** + * Send notifications to external service addresses. + * + * @param string[] $serviceaddresses + * @param stdClass $template + * @return int + */ + private static function send_service_notifications(array $serviceaddresses, stdClass $template): int { + $userfrom = core_user::get_noreply_user(); + $subject = self::replace_placeholders($template->subject, $template); + $body = self::replace_placeholders($template->body, $template); + $sent = 0; + + foreach ($serviceaddresses as $address) { + if (!validate_email($address)) { + continue; + } + + $recipient = clone $userfrom; + $recipient->id = -1; + $recipient->email = $address; + $recipient->firstname = get_string('bookingstatus_service_recipient_firstname', 'mod_bookit'); + $recipient->lastname = get_string('bookingstatus_service_recipient_lastname', 'mod_bookit'); + + if (email_to_user($recipient, $userfrom, $subject, $body, text_to_html($body, false, false, true))) { + $sent++; + } + } + + return $sent; + } + + /** + * Replace supported placeholders in the configured template text. + * + * @param string $template + * @param stdClass $data + * @return string + */ + private static function replace_placeholders(string $template, stdClass $data): string { + $replacements = [ + '###EVENTNAME###' => $data->eventname, + '###BOOKINGSTATUS###' => $data->bookingstatus, + '###OLDBOOKINGSTATUS###' => $data->oldbookingstatus, + '###EVENTURL###' => $data->eventurl, + '###ROOM###' => $data->room, + '###STARTTIME###' => $data->starttime, + '###ENDTIME###' => $data->endtime, + '###BOOKINGPERSON###' => $data->bookingperson, + '###PERSONINCHARGE###' => $data->personincharge, + '###OTHEREXAMINERS###' => $data->otherexaminers, + ]; + + return strtr($template, $replacements); + } + + /** + * Resolve a user name by ID. + * + * @param int $userid + * @return string + */ + private static function get_user_name(int $userid): string { + if ($userid <= 0) { + return ''; + } + + $user = core_user::get_user($userid); + return $user ? fullname($user) : ''; + } + + /** + * Resolve a comma-separated user list to names. + * + * @param string $ids + * @return string + */ + private static function get_user_names_csv(string $ids): string { + $names = []; + foreach (explode(',', $ids) as $rawid) { + $id = (int)trim($rawid); + if ($id > 0) { + $name = self::get_user_name($id); + if ($name !== '') { + $names[] = $name; + } + } + } + + return implode(', ', array_unique($names)); + } + + /** + * Resolve the room label for the notification. + * + * @param int $roomid + * @return string + */ + private static function get_room_name(int $roomid): string { + global $DB; + + if ($roomid <= 0) { + return ''; + } + + $room = $DB->get_record('bookit_room', ['id' => $roomid], 'name', IGNORE_MISSING); + return $room->name ?? ''; + } + + /** + * Resolve the course ID for a given cmid. + * + * @param int $cmid + * @return int + */ + private static function get_course_id(int $cmid): int { + global $DB; + + $cm = $DB->get_record('course_modules', ['id' => $cmid], 'course', IGNORE_MISSING); + return $cm ? (int)$cm->course : 0; + } +} diff --git a/classes/local/manager/event_access_manager.php b/classes/local/manager/event_access_manager.php index 4e01b2ff..6712508f 100644 --- a/classes/local/manager/event_access_manager.php +++ b/classes/local/manager/event_access_manager.php @@ -32,11 +32,46 @@ * Centralises booking-state and participant checks for event-level views. */ class event_access_manager { + /** Booking status: new request. */ + public const BOOKINGSTATUS_NEW = 0; + /** Booking status: in progress (being processed by service team). */ public const BOOKINGSTATUS_IN_PROGRESS = 1; - /** Confirmed booking status. */ - public const BOOKINGSTATUS_CONFIRMED = 2; + /** Booking status: accepted by service team. */ + public const BOOKINGSTATUS_ACCEPTED = 2; + + /** Booking status: canceled. */ + public const BOOKINGSTATUS_CANCELED = 3; + + /** Booking status: rejected. */ + public const BOOKINGSTATUS_REJECTED = 4; + + /** @var int[] booking statuses that still require service-team action. */ + public const OPEN_BOOKING_STATUSES = [ + self::BOOKINGSTATUS_NEW, + self::BOOKINGSTATUS_IN_PROGRESS, + ]; + + /** @var array allowed workflow transitions keyed by current status. */ + private const ALLOWED_TRANSITIONS = [ + self::BOOKINGSTATUS_NEW => [ + self::BOOKINGSTATUS_IN_PROGRESS, + self::BOOKINGSTATUS_ACCEPTED, + self::BOOKINGSTATUS_CANCELED, + self::BOOKINGSTATUS_REJECTED, + ], + self::BOOKINGSTATUS_IN_PROGRESS => [ + self::BOOKINGSTATUS_ACCEPTED, + self::BOOKINGSTATUS_CANCELED, + self::BOOKINGSTATUS_REJECTED, + ], + self::BOOKINGSTATUS_ACCEPTED => [ + self::BOOKINGSTATUS_CANCELED, + ], + self::BOOKINGSTATUS_CANCELED => [], + self::BOOKINGSTATUS_REJECTED => [], + ]; /** * Check whether the event booking has been confirmed by the service team. @@ -45,7 +80,7 @@ class event_access_manager { * @return bool */ public static function is_booking_confirmed(stdClass $event): bool { - return (int)($event->bookingstatus ?? -1) === self::BOOKINGSTATUS_CONFIRMED; + return (int)($event->bookingstatus ?? -1) === self::BOOKINGSTATUS_ACCEPTED; } /** @@ -57,7 +92,257 @@ public static function is_booking_confirmed(stdClass $event): bool { */ public static function is_booking_accessible(stdClass $event): bool { $status = (int)($event->bookingstatus ?? -1); - return $status === self::BOOKINGSTATUS_IN_PROGRESS || $status === self::BOOKINGSTATUS_CONFIRMED; + return $status === self::BOOKINGSTATUS_IN_PROGRESS || $status === self::BOOKINGSTATUS_ACCEPTED; + } + + /** + * Check whether the event is still an open request for service-team processing. + * + * @param stdClass $event + * @return bool + */ + public static function is_open_request(stdClass $event): bool { + return in_array((int)($event->bookingstatus ?? -1), self::OPEN_BOOKING_STATUSES, true); + } + + /** + * Get the statuses that represent open requests. + * + * @return int[] + */ + public static function get_open_request_statuses(): array { + return self::OPEN_BOOKING_STATUSES; + } + + /** + * Return the allowed transitions for a given current booking status. + * + * @param int $status + * @param bool $includecurrent + * @return int[] + */ + public static function get_allowed_booking_status_transitions(int $status, bool $includecurrent = false): array { + $transitions = self::ALLOWED_TRANSITIONS[$status] ?? []; + if ($includecurrent) { + array_unshift($transitions, $status); + } + return array_values(array_unique(array_map('intval', $transitions))); + } + + /** + * Check whether a status transition is allowed by the workflow. + * + * @param int $fromstatus + * @param int $tostatus + * @return bool + */ + public static function can_transition_booking_status(int $fromstatus, int $tostatus): bool { + if ($fromstatus === $tostatus) { + return true; + } + return in_array($tostatus, self::get_allowed_booking_status_transitions($fromstatus), true); + } + + /** + * Check whether the current user may manage open requests. + * + * @param context_module $context + * @return bool + */ + public static function can_manage_open_requests(context_module $context): bool { + return has_capability('mod/bookit:managebasics', $context) + || has_capability('mod/bookit:viewalldetailsofevent', $context); + } + + /** + * Return all BookIt participant roles that the user has on the event. + * + * @param stdClass $event + * @param int $userid + * @return string[] + */ + public static function get_user_roles_for_event(stdClass $event, int $userid): array { + $roles = []; + + if ((int)($event->personinchargeid ?? 0) === $userid) { + $roles[] = 'personincharge'; + } + + if ((int)($event->usermodified ?? 0) === $userid) { + $roles[] = 'bookingperson'; + } + + if (in_array($userid, self::parse_csv_ids($event->otherexaminers ?? ''), true)) { + $roles[] = 'otherexaminer'; + } + + if (in_array($userid, self::parse_csv_ids($event->supportpersons ?? ''), true)) { + $roles[] = 'supportperson'; + } + + return $roles; + } + + /** + * Check whether the event can still be freely edited by a participant. + * + * @param stdClass $event + * @param int $userid + * @return bool + */ + public static function can_participant_edit_event(stdClass $event, int $userid): bool { + if ((int)($event->bookingstatus ?? self::BOOKINGSTATUS_NEW) !== self::BOOKINGSTATUS_NEW) { + return false; + } + + $roles = self::get_user_roles_for_event($event, $userid); + return !empty(array_intersect($roles, ['bookingperson', 'personincharge', 'otherexaminer'])); + } + + /** + * Check whether the user may open the event details with full event data. + * + * Service-team users may always access the event. Other users need the own-event + * detail capability and must participate in the booking. Support persons may access + * the booking only once it has been accepted unless another participant role grants + * wider visibility. + * + * @param stdClass $event + * @param context_module $context + * @param int $userid + * @return bool + */ + public static function can_user_view_event_details(stdClass $event, context_module $context, int $userid): bool { + if (self::can_manage_open_requests($context) || has_capability('mod/bookit:viewalldetailsofevent', $context)) { + return true; + } + + if (!has_capability('mod/bookit:viewalldetailsofownevent', $context)) { + return false; + } + + $roles = self::get_user_roles_for_event($event, $userid); + if (empty($roles)) { + return false; + } + + if (!empty(array_intersect($roles, ['bookingperson', 'personincharge', 'otherexaminer']))) { + return true; + } + + return in_array('supportperson', $roles, true) && self::is_booking_confirmed($event); + } + + /** + * Check whether the user may see the event in participant-facing overviews. + * + * @param stdClass $event + * @param context_module $context + * @param int $userid + * @return bool + */ + public static function can_user_view_event_in_overview(stdClass $event, context_module $context, int $userid): bool { + if (self::can_manage_open_requests($context) || has_capability('mod/bookit:viewalldetailsofevent', $context)) { + return true; + } + + if (!has_capability('mod/bookit:viewownoverview', $context)) { + return false; + } + + return self::can_user_view_event_details($event, $context, $userid); + } + + /** + * Check whether the user may see the detailed calendar event instead of a reserved placeholder. + * + * @param stdClass $event + * @param context_module $context + * @param int $userid + * @return bool + */ + public static function can_user_view_event_in_calendar(stdClass $event, context_module $context, int $userid): bool { + return self::can_user_view_event_details($event, $context, $userid); + } + + /** + * Check whether support-on-site fields may be shown to the user. + * + * @param stdClass $event + * @param context_module $context + * @param int $userid + * @return bool + */ + public static function can_supportperson_view_internal_fields( + stdClass $event, + context_module $context, + int $userid + ): bool { + return in_array('supportperson', self::get_user_roles_for_event($event, $userid), true) + && self::can_user_view_event_details($event, $context, $userid); + } + + /** + * Check whether the user may edit only the internal notes field. + * + * @param stdClass $event + * @param context_module $context + * @param int $userid + * @return bool + */ + public static function can_supportperson_edit_internal_notes( + stdClass $event, + context_module $context, + int $userid + ): bool { + return self::can_supportperson_view_internal_fields($event, $context, $userid); + } + + /** + * Check whether the user may only cancel the event after the workflow left "New". + * + * @param stdClass $event + * @param context_module $context + * @param int $userid + * @return bool + */ + public static function can_participant_cancel_only(stdClass $event, context_module $context, int $userid): bool { + return self::can_cancel_event($event, $context, $userid) + && !self::can_participant_edit_event($event, $userid) + && !has_capability('mod/bookit:editevent', $context) + && !has_capability('mod/bookit:editinternal', $context); + } + + /** + * Check whether the current user may cancel the event. + * + * @param stdClass $event + * @param context_module $context + * @param int $userid + * @return bool + */ + public static function can_cancel_event(stdClass $event, context_module $context, int $userid): bool { + if (self::can_manage_open_requests($context)) { + return true; + } + + if ( + in_array( + (int)($event->bookingstatus ?? -1), + [ + self::BOOKINGSTATUS_CANCELED, + self::BOOKINGSTATUS_REJECTED, + ], + true + ) + ) { + return false; + } + + return !empty(array_intersect( + self::get_user_roles_for_event($event, $userid), + ['bookingperson', 'personincharge', 'otherexaminer'] + )); } /** @@ -96,11 +381,7 @@ public static function can_view_event_checklist(stdClass $event, context_module return true; } - if (!self::is_booking_accessible($event)) { - return false; - } - - if (!has_capability('mod/bookit:viewalldetailsofownevent', $context)) { + if (!self::is_booking_accessible($event) || !self::can_user_view_event_details($event, $context, $userid)) { return false; } @@ -120,11 +401,7 @@ public static function can_view_event_resources(stdClass $event, context_module return true; } - if (!self::is_booking_accessible($event)) { - return false; - } - - if (!has_capability('mod/bookit:viewalldetailsofownevent', $context)) { + if (!self::is_booking_accessible($event) || !self::can_user_view_event_details($event, $context, $userid)) { return false; } diff --git a/classes/local/manager/event_manager.php b/classes/local/manager/event_manager.php index 2393e6eb..6647f032 100644 --- a/classes/local/manager/event_manager.php +++ b/classes/local/manager/event_manager.php @@ -84,7 +84,7 @@ public static function get_events_in_timerange(string $starttime, string $endtim $viewalldetailsofownevent = has_capability('mod/bookit:viewalldetailsofownevent', $context); $sqlreserved = - 'SELECT e.id, NULL as name, e.starttime, e.endtime, e.extratimebefore, e.extratimeafter, r.eventcolor, + 'SELECT e.id, NULL as name, e.bookingstatus, e.starttime, e.endtime, e.extratimebefore, e.extratimeafter, r.eventcolor, r.name as roomname, r.shortname, r.location ' . 'FROM {bookit_event} e ' . 'JOIN {bookit_room} r ON r.id = e.roomid ' . @@ -93,7 +93,7 @@ public static function get_events_in_timerange(string $starttime, string $endtim // Service-Team: can view all events in detail. if ($viewalldetailsofevent) { $sql = - 'SELECT e.id, e.name, e.starttime, e.endtime, e.extratimebefore, e.extratimeafter, r.eventcolor, + 'SELECT e.id, e.name, e.bookingstatus, e.starttime, e.endtime, e.extratimebefore, e.extratimeafter, r.eventcolor, r.name as roomname, r.shortname, r.location ' . 'FROM {bookit_event} e ' . 'JOIN {bookit_room} r ON r.id = e.roomid ' . @@ -102,26 +102,35 @@ public static function get_events_in_timerange(string $starttime, string $endtim } else if ($viewalldetailsofownevent) { $otherexaminers = $DB->sql_like('otherexaminers', ':otherexaminers'); $otherexaminers1 = $DB->sql_like('otherexaminers', ':otherexaminers1'); + $supportpersons = $DB->sql_like('supportpersons', ':supportpersons'); + $supportpersons1 = $DB->sql_like('supportpersons', ':supportpersons1'); // Every user: can view own events in detail. - $sql = 'SELECT e.id, e.name, e.starttime, e.endtime, e.extratimebefore, e.extratimeafter, r.eventcolor, + $sql = 'SELECT e.id, e.name, e.bookingstatus, e.starttime, e.endtime, e.extratimebefore, e.extratimeafter, r.eventcolor, r.name as roomname, r.shortname, r.location FROM {bookit_event} e JOIN {bookit_room} r ON r.id = e.roomid WHERE endtime >= :starttime1 AND starttime <= :endtime1 - AND (e.usermodified = :usermodified1 OR personinchargeid = :personinchargeid1 OR ' . $otherexaminers1 . ') + AND (e.usermodified = :usermodified1 OR personinchargeid = :personinchargeid1 OR ' + . $otherexaminers1 . ' OR (' . $supportpersons1 . ' AND e.bookingstatus = :acceptedstatus1)) UNION ' . $sqlreserved . ' - AND e.usermodified != :usermodified AND personinchargeid != :personinchargeid AND NOT ' . $otherexaminers; + AND e.usermodified != :usermodified AND personinchargeid != :personinchargeid + AND NOT ' . $otherexaminers . ' AND NOT (' . $supportpersons . ' AND e.bookingstatus = :acceptedstatus)'; $params = [ 'starttime1' => $starttimestamp, 'endtime1' => $endtimestamp, 'usermodified1' => $USER->id, 'personinchargeid1' => $USER->id, 'otherexaminers1' => $USER->id, + 'supportpersons1' => $USER->id, + 'acceptedstatus1' => event_access_manager::BOOKINGSTATUS_ACCEPTED, 'starttime' => $starttimestamp, 'endtime' => $endtimestamp, 'usermodified' => $USER->id, 'personinchargeid' => $USER->id, - 'otherexaminers' => $USER->id]; + 'otherexaminers' => $USER->id, + 'supportpersons' => $USER->id, + 'acceptedstatus' => event_access_manager::BOOKINGSTATUS_ACCEPTED, + ]; } else { // Every user: can view no details. $sql = $sqlreserved; @@ -149,17 +158,23 @@ public static function get_events_in_timerange(string $starttime, string $endtim $events[] = [ 'id' => $record->id, - 'title' => [ - 'html' => '
' . date('H:i', $record->starttime) . '-' . - date('H:i', $record->endtime) . '
' . - ($record->name ?? $reserved) . " ($roominfo)", - ], + 'title' => ($record->name ?? $reserved) . " ($roominfo)", + 'titleHTML' => '
' . date('H:i', $record->starttime) . '-' . + date('H:i', $record->endtime) . '
' . + ($record->name ?? $reserved) . " ($roominfo)", 'start' => date('Y-m-d H:i', $record->starttime - $record->extratimebefore * 60), 'end' => date('Y-m-d H:i', $record->endtime + $record->extratimeafter * 60), 'backgroundColor' => $record->eventcolor, 'textColor' => color_manager::get_textcolor_for_background($record->eventcolor), - 'extendedProps' => (object) ['reserved' => !$record->name], - 'classNames' => 'hide-event-time', + 'extendedProps' => (object) [ + 'reserved' => !$record->name, + 'bookingstatus' => (int)($record->bookingstatus ?? event_access_manager::BOOKINGSTATUS_ACCEPTED), + 'roomname' => $record->roomname, + ], + 'classNames' => [ + 'hide-event-time', + self::get_booking_status_class((int)($record->bookingstatus ?? event_access_manager::BOOKINGSTATUS_ACCEPTED)), + ], ]; } return $events; @@ -223,6 +238,51 @@ public static function get_events_for_examiner(int $userid): array { return $DB->get_records_sql($sql, $params); } + /** + * Return all open booking requests for service-team overview. + * + * @return array + * @throws dml_exception + */ + public static function get_open_requests(): array { + global $DB; + + [$statussql, $params] = $DB->get_in_or_equal(event_access_manager::get_open_request_statuses(), SQL_PARAMS_NAMED); + + $sql = " + SELECT + e.id, + e.name, + e.bookingstatus, + e.starttime, + e.endtime, + e.personinchargeid, + e.otherexaminers, + e.supportpersons, + e.usermodified, + r.name AS room + FROM {bookit_event} e + LEFT JOIN {bookit_room} r ON r.id = e.roomid + WHERE e.bookingstatus $statussql + ORDER BY e.starttime ASC + "; + + return $DB->get_records_sql($sql, $params); + } + + /** + * Return the number of open booking requests. + * + * @return int + * @throws dml_exception + */ + public static function count_open_requests(): int { + global $DB; + + [$statussql, $params] = $DB->get_in_or_equal(event_access_manager::get_open_request_statuses(), SQL_PARAMS_NAMED); + return (int)$DB->count_records_select('bookit_event', "bookingstatus $statussql", $params); + } + /** * Get all institutions that should appear in the filter dropdown. * @@ -274,6 +334,133 @@ public static function get_faculties(): array { return $result; } + /** + * Return the semester value that corresponds to the given reference time. + * + * Winter terms bridge the year boundary, so January through March still belong to the + * previous winter semester. + * + * @param int|null $referencetime + * @return int + */ + public static function get_current_semester(?int $referencetime = null): int { + $date = (new DateTime())->setTimestamp($referencetime ?? time()); + $year = (int)$date->format('Y'); + $month = (int)$date->format('n'); + + if ($month >= 4 && $month <= 9) { + return ($year * 10) + 1; + } + + if ($month >= 10) { + return ($year * 10) + 2; + } + + return (($year - 1) * 10) + 2; + } + + /** + * Return the default reporting range for the year of the given reference time. + * + * @param int|null $referencetime + * @return int[] + */ + public static function get_reporting_default_range(?int $referencetime = null): array { + $date = (new DateTime())->setTimestamp($referencetime ?? time()); + $year = (int)$date->format('Y'); + + $start = (new DateTime())->setDate($year, 1, 1)->setTime(0, 0, 0)->getTimestamp(); + $end = (new DateTime())->setDate($year, 12, 31)->setTime(23, 59, 59)->getTimestamp(); + + return [$start, $end]; + } + + /** + * Build semester filter options around the current semester. + * + * @param int|null $referencetime + * @return array + */ + public static function get_semester_filter_options(?int $referencetime = null): array { + $currentsemester = self::get_current_semester($referencetime); + $baseyear = (int)floor($currentsemester / 10); + $options = []; + + for ($year = $baseyear - 1; $year <= $baseyear + 1; $year++) { + $options[($year * 10) + 1] = get_string('summer_semester', 'mod_bookit') . ' ' . $year; + $options[($year * 10) + 2] = get_string('winter_semester', 'mod_bookit') . ' ' . $year; + } + + return $options; + } + + /** + * Return reporting events filtered by range and semester and then reduced by the current user's access. + * + * @param context_module $context + * @param int $userid + * @param int $starttime + * @param int $endtime + * @param int[] $semesterids + * @return array + * @throws dml_exception + */ + public static function get_events_for_reporting( + context_module $context, + int $userid, + int $starttime, + int $endtime, + array $semesterids = [] + ): array { + global $DB; + + $params = [ + 'starttime' => $starttime, + 'endtime' => $endtime, + ]; + $conditions = [ + 'e.endtime >= :starttime', + 'e.starttime <= :endtime', + ]; + + $semesterids = array_values(array_filter(array_map('intval', $semesterids))); + if (!empty($semesterids)) { + [$semestersql, $semesterparams] = $DB->get_in_or_equal($semesterids, SQL_PARAMS_NAMED); + $conditions[] = "(e.semester $semestersql OR e.semester IS NULL OR e.semester = 0)"; + $params = array_merge($params, $semesterparams); + } + + $sql = " + SELECT + e.id, + e.name, + e.semester, + e.bookingstatus, + e.starttime, + e.endtime, + e.personinchargeid, + e.otherexaminers, + e.supportpersons, + e.usermodified, + r.name AS room + FROM {bookit_event} e + LEFT JOIN {bookit_room} r ON r.id = e.roomid + WHERE " . implode(' AND ', $conditions) . " + ORDER BY e.starttime ASC + "; + + $records = $DB->get_records_sql($sql, $params); + $events = []; + foreach ($records as $record) { + if (!event_access_manager::can_user_view_event_in_overview($record, $context, $userid)) { + continue; + } + $events[] = $record; + } + + return $events; + } + @@ -389,9 +576,22 @@ public static function get_booking_status_colors(): array { * @return array */ public static function get_booking_status_options(int $current): array { + return self::get_booking_status_transition_options($current, true); + } + + /** + * Return workflow-aware booking status options. + * + * @param int $current + * @param bool $includecurrent + * @return array + */ + public static function get_booking_status_transition_options(int $current, bool $includecurrent = false): array { $colors = self::get_booking_status_colors(); $options = []; - foreach ($colors as $value => $color) { + $allowedvalues = event_access_manager::get_allowed_booking_status_transitions($current, $includecurrent); + foreach ($allowedvalues as $value) { + $color = $colors[$value]; $options[] = [ 'value' => $value, 'label' => get_string('event_bookingstatus_' . $value, 'mod_bookit'), @@ -402,4 +602,14 @@ public static function get_booking_status_options(int $current): array { } return $options; } + + /** + * Return a CSS class name for the current booking status. + * + * @param int $status + * @return string + */ + public static function get_booking_status_class(int $status): string { + return 'bookit-bookingstatus-' . $status; + } } diff --git a/db/messages.php b/db/messages.php index 6f8086ca..168fa783 100644 --- a/db/messages.php +++ b/db/messages.php @@ -26,6 +26,12 @@ defined('MOODLE_INTERNAL') || die(); $messageproviders = [ + 'bookit_booking_status_changed' => [ + 'defaults' => [ + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, + 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, + ], + ], 'bookit_resource_status_changed' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, diff --git a/db/services.php b/db/services.php index 90f96d49..4a7894fa 100644 --- a/db/services.php +++ b/db/services.php @@ -39,7 +39,7 @@ ], 'mod_bookit_update_event_booking_status' => [ 'classname' => 'mod_bookit\external\update_event_booking_status', - 'description' => 'Updates the booking status of an event.', + 'description' => 'Updates the booking status of an event using the configured workflow transitions.', 'type' => 'write', 'ajax' => true, ], diff --git a/events.php b/events.php index 1ebde210..ce31ba4d 100644 --- a/events.php +++ b/events.php @@ -39,9 +39,11 @@ require_once(__DIR__ . '/../../config.php'); require_once(__DIR__ . '/lib.php'); +use mod_bookit\local\manager\event_access_manager; use mod_bookit\local\manager\event_manager; require_login(); // User must be logged-in. +global $USER; // ...@TODO: capability check, check for sesskey! // ...@TODO: The id of the instance should become required in future! @@ -152,6 +154,10 @@ SELECT e.bookingstatus, e.institutionid, e.roomid, + e.personinchargeid, + e.otherexaminers, + e.supportpersons, + e.usermodified, r.name AS roomname FROM {bookit_event} e LEFT JOIN {bookit_room} r ON r.id = e.roomid @@ -168,17 +174,37 @@ $ev['institutionid'] = (string)($row->institutionid ?? ''); $ev['roomid'] = (int)($row->roomid ?? 0); $ev['roomname'] = (string)($row->roomname ?? ''); + $ev['personinchargeid'] = (int)($row->personinchargeid ?? 0); + $ev['otherexaminers'] = (string)($row->otherexaminers ?? ''); + $ev['supportpersons'] = (string)($row->supportpersons ?? ''); + $ev['usermodified'] = (int)($row->usermodified ?? 0); } else { $ev->bookingstatus = (int)($row->bookingstatus ?? 0); $ev->institutionid = (string)($row->institutionid ?? ''); $ev->roomid = (int)($row->roomid ?? 0); $ev->roomname = (string)($row->roomname ?? ''); + $ev->personinchargeid = (int)($row->personinchargeid ?? 0); + $ev->otherexaminers = (string)($row->otherexaminers ?? ''); + $ev->supportpersons = (string)($row->supportpersons ?? ''); + $ev->usermodified = (int)($row->usermodified ?? 0); } } unset($ev); +$events = array_filter($events, static function ($ev) use ($context, $USER) { + $reserved = false; + if (is_array($ev) && isset($ev['extendedProps']) && is_object($ev['extendedProps'])) { + $reserved = (bool)($ev['extendedProps']->reserved ?? false); + } else if (is_object($ev) && isset($ev->extendedProps) && is_object($ev->extendedProps)) { + $reserved = (bool)($ev->extendedProps->reserved ?? false); + } + if ($reserved) { + return true; + } + return event_access_manager::can_user_view_event_in_calendar((object)$ev, $context, (int)$USER->id); +}); // Apply in-memory filters (only if parameter present). For Filter user story. $events = array_filter($events, function ($ev) use ($roomids, $faculties, $statuses, $search) { diff --git a/export_events.php b/export_events.php index 1d8301e3..774bfb26 100644 --- a/export_events.php +++ b/export_events.php @@ -29,6 +29,107 @@ use mod_bookit\local\manager\event_manager; +/** + * Extract integer user IDs from a CSV field. + * + * @param string|null $csv + * @return int[] + */ +function export_events_csv_user_ids(?string $csv): array { + if ($csv === null || $csv === '') { + return []; + } + + return array_values(array_filter(array_map('intval', explode(',', $csv)))); +} + +/** + * Resolve user IDs to display names. + * + * @param int[] $userids + * @return string[] + */ +function export_events_user_names(array $userids): array { + global $DB; + + $userids = array_values(array_unique(array_filter(array_map('intval', $userids)))); + if (empty($userids)) { + return []; + } + + $fields = 'id, firstname, lastname, middlename, alternatename, firstnamephonetic, lastnamephonetic'; + $records = $DB->get_records_list('user', 'id', $userids, '', $fields); + $names = []; + foreach ($records as $record) { + $names[(int)$record->id] = fullname($record); + } + + return $names; +} + +/** + * Resolve institution IDs to display names. + * + * @param int[] $institutionids + * @return string[] + */ +function export_events_institution_names(array $institutionids): array { + global $DB; + + $institutionids = array_values(array_unique(array_filter(array_map('intval', $institutionids)))); + if (empty($institutionids)) { + return []; + } + + $records = $DB->get_records_list('bookit_institution', 'id', $institutionids, '', 'id, name'); + $names = []; + foreach ($records as $record) { + $names[(int)$record->id] = $record->name; + } + + return $names; +} + +/** + * Format semester integer values into the user-facing term label. + * + * @param int|null $semester + * @return string + */ +function export_events_semester_label(?int $semester): string { + if (empty($semester)) { + return ''; + } + + $year = (int)floor($semester / 10); + $term = ((int)$semester % 10) === 1 ? 'summer_semester' : 'winter_semester'; + + return get_string($term, 'mod_bookit') . ' ' . $year; +} + +/** + * Format a duration from event start/end timestamps. + * + * @param int $starttime + * @param int $endtime + * @return string + */ +function export_events_duration_label(int $starttime, int $endtime): string { + $seconds = max(0, $endtime - $starttime); + $hours = intdiv($seconds, HOURSECS); + $minutes = intdiv($seconds % HOURSECS, MINSECS); + $parts = []; + + if ($hours > 0) { + $parts[] = $hours . 'h'; + } + if ($minutes > 0 || empty($parts)) { + $parts[] = $minutes . 'm'; + } + + return implode(' ', $parts); +} + /* ------------------------------------------------------------------ 0. Parameters & capability check ------------------------------------------------------------------ */ @@ -141,28 +242,55 @@ /* add the event room */ if ($events) { - $eventids = array_keys($events); - $inorequal = $DB->get_in_or_equal($eventids, SQL_PARAMS_NAMED); - $in = $inorequal[0]; - $p = $inorequal[1]; - $sql = "SELECT er.eventid, MIN(r.name) AS room - FROM {bookit_event_resource} er - JOIN {bookit_resource} r ON r.id = er.resourceid - WHERE er.eventid $in - GROUP BY er.eventid"; + $roomids = array_values(array_unique(array_filter(array_map( + static fn($event) => (int)($event->roomid ?? 0), + $events + )))); + if (!empty($roomids)) { + $roomrecords = $DB->get_records_list('bookit_room', 'id', $roomids, '', 'id, name'); + foreach ($events as $eventid => $event) { + $roomid = (int)($event->roomid ?? 0); + if ($roomid && isset($roomrecords[$roomid])) { + $events[$eventid]->room = $roomrecords[$roomid]->name; + } + } + } - foreach ($DB->get_records_sql($sql, $p) as $rec) { - $events[$rec->eventid]->room = $rec->room ?? ''; + $missingroomeventids = array_keys(array_filter($events, static function ($event): bool { + return empty($event->room); + })); + if (!empty($missingroomeventids)) { + [$in, $p] = $DB->get_in_or_equal($missingroomeventids, SQL_PARAMS_NAMED); + $sql = "SELECT er.eventid, MIN(r.name) AS room + FROM {bookit_event_resource} er + JOIN {bookit_resource} r ON r.id = er.resourceid + WHERE er.eventid $in + GROUP BY er.eventid"; + + foreach ($DB->get_records_sql($sql, $p) as $rec) { + $events[$rec->eventid]->room = $rec->room ?? ''; + } } } // Apply room filter after enrichment. if ($room) { - $events = array_filter($events, function ($ev) use ($room) { - return (int)$ev->resourceid === (int)$room; + $events = array_filter($events, static function ($ev) use ($room): bool { + return (int)($ev->roomid ?? 0) === (int)$room; }); } +$alluserids = []; +$allinstitutionids = []; +foreach ($events as $event) { + $alluserids[] = (int)($event->usermodified ?? 0); + $alluserids[] = (int)($event->personinchargeid ?? 0); + $alluserids = array_merge($alluserids, export_events_csv_user_ids($event->otherexaminers ?? '')); + $allinstitutionids[] = (int)($event->institutionid ?? 0); +} +$usernames = export_events_user_names($alluserids); +$institutionnames = export_events_institution_names($allinstitutionids); + /* ------------------------------------------------------------------ 2. Build VCALENDAR ------------------------------------------------------------------ */ @@ -182,14 +310,40 @@ /* ------- human‑readable description ---------------------------- */ $descrrows = []; - if (!empty($ev->institutionid)) { - $descrrows[] = 'Faculty: ' . $ev->institutionid; + $bookingpersonid = (int)($ev->usermodified ?? 0); + $personinchargeid = (int)($ev->personinchargeid ?? 0); + $institutionid = (int)($ev->institutionid ?? 0); + $otherexaminerids = export_events_csv_user_ids($ev->otherexaminers ?? ''); + $otherexaminernames = array_values(array_filter(array_map( + static fn($userid) => $usernames[$userid] ?? null, + $otherexaminerids + ))); + + if ($bookingpersonid && isset($usernames[$bookingpersonid])) { + $descrrows[] = get_string('exportevents_ics_bookingperson', 'mod_bookit') . ': ' . $usernames[$bookingpersonid]; + } + if ($personinchargeid && isset($usernames[$personinchargeid])) { + $descrrows[] = get_string('exportevents_ics_personincharge', 'mod_bookit') . ': ' . $usernames[$personinchargeid]; + } + if (!empty($otherexaminernames)) { + $descrrows[] = get_string('exportevents_ics_otherexaminers', 'mod_bookit') . ': ' + . implode(', ', $otherexaminernames); + } + if (!empty($ev->semester)) { + $descrrows[] = get_string('exportevents_ics_semester', 'mod_bookit') . ': ' + . export_events_semester_label((int)$ev->semester); + } + $descrrows[] = get_string('exportevents_ics_duration', 'mod_bookit') . ': ' + . export_events_duration_label((int)$ev->starttime, (int)$ev->endtime); + if ($institutionid && isset($institutionnames[$institutionid])) { + $descrrows[] = get_string('exportevents_ics_faculty', 'mod_bookit') . ': ' + . $institutionnames[$institutionid]; } if (!empty($ev->technicalneeds)) { - $descrrows[] = '| Requirements: ' . $ev->technicalneeds; + $descrrows[] = get_string('exportevents_ics_requirements', 'mod_bookit') . ': ' . $ev->technicalneeds; } if (!empty($ev->participantsamount)) { - $descrrows[] = '| Participants: ' . $ev->participantsamount; + $descrrows[] = get_string('exportevents_ics_participants', 'mod_bookit') . ': ' . $ev->participantsamount; } /* ------- assemble one VEVENT ----------------------------------- */ diff --git a/lang/de/bookit.php b/lang/de/bookit.php index 24b89109..5bde3ba4 100644 --- a/lang/de/bookit.php +++ b/lang/de/bookit.php @@ -47,6 +47,50 @@ $string['booking:resources_booked'] = 'Gebuchte Ressourcen'; $string['booking:resources_header'] = 'Ressourcen'; $string['booking:resources_info'] = 'Wählen Sie die Ressourcen aus, die Sie für diese Buchung benötigen.'; +$string['bookingstatus_action_accept'] = 'Akzeptieren'; +$string['bookingstatus_action_cancel'] = 'Stornieren'; +$string['bookingstatus_action_inprogress'] = 'In Bearbeitung setzen'; +$string['bookingstatus_action_reject'] = 'Ablehnen'; +$string['bookingstatus_body_accepted'] = 'Nachrichtentext für Akzeptiert'; +$string['bookingstatus_body_canceled'] = 'Nachrichtentext für Storniert'; +$string['bookingstatus_body_desc'] = 'Leer lassen, um die Standardnachricht zu verwenden. Die dokumentierten Platzhalter können verwendet werden.'; +$string['bookingstatus_body_inprogress'] = 'Nachrichtentext für In Bearbeitung'; +$string['bookingstatus_body_new'] = 'Nachrichtentext für Neu'; +$string['bookingstatus_body_rejected'] = 'Nachrichtentext für Abgelehnt'; +$string['bookingstatus_notification_body_default'] = 'Die Buchungsanfrage "###EVENTNAME###" wurde auf "###BOOKINGSTATUS###" gesetzt.' + . "\n\n" + . 'Raum: ###ROOM###' + . "\n" + . 'Beginn: ###STARTTIME###' + . "\n" + . 'Ende: ###ENDTIME###' + . "\n" + . 'Buchende Person: ###BOOKINGPERSON###' + . "\n" + . 'Verantwortliche Person: ###PERSONINCHARGE###' + . "\n" + . 'Weitere Prüfende: ###OTHEREXAMINERS###' + . "\n\n" + . 'Buchung öffnen: ###EVENTURL###'; +$string['bookingstatus_notification_subject_default'] = 'Buchungsstatus aktualisiert: {$a->eventname} ({$a->bookingstatus})'; +$string['bookingstatus_notifications_desc'] = 'Konfiguration der Empfänger und Nachrichtenvorlagen für Änderungen am Buchungsstatus. Unterstützte Platzhalter: ###EVENTNAME###, ###BOOKINGSTATUS###, ###OLDBOOKINGSTATUS###, ###EVENTURL###, ###ROOM###, ###STARTTIME###, ###ENDTIME###, ###BOOKINGPERSON###, ###PERSONINCHARGE###, ###OTHEREXAMINERS###.'; +$string['bookingstatus_notifications_heading'] = 'Benachrichtigungen zum Buchungsstatus'; +$string['bookingstatus_notify_bookingperson'] = 'Buchende Person benachrichtigen'; +$string['bookingstatus_notify_bookingperson_desc'] = 'Sendet Statusänderungen an die Person, die die Buchungsanfrage erstellt hat.'; +$string['bookingstatus_notify_otherexaminers'] = 'Weitere Prüfende benachrichtigen'; +$string['bookingstatus_notify_otherexaminers_desc'] = 'Sendet Statusänderungen an alle weiteren Prüfenden der Buchungsanfrage.'; +$string['bookingstatus_notify_personincharge'] = 'Verantwortliche Person benachrichtigen'; +$string['bookingstatus_notify_personincharge_desc'] = 'Sendet Statusänderungen an die verantwortliche Person.'; +$string['bookingstatus_service_addresses'] = 'Universitäre Service-Adressen'; +$string['bookingstatus_service_addresses_desc'] = 'Optionale Liste von E-Mail-Adressen, die Statusänderungen erhalten sollen (getrennt durch Komma, Semikolon oder Leerzeichen).'; +$string['bookingstatus_service_recipient_firstname'] = 'Universität'; +$string['bookingstatus_service_recipient_lastname'] = 'Service'; +$string['bookingstatus_subject_accepted'] = 'Betreff für Akzeptiert'; +$string['bookingstatus_subject_canceled'] = 'Betreff für Storniert'; +$string['bookingstatus_subject_desc'] = 'Leer lassen, um den Standardbetreff zu verwenden.'; +$string['bookingstatus_subject_inprogress'] = 'Betreff für In Bearbeitung'; +$string['bookingstatus_subject_new'] = 'Betreff für Neu'; +$string['bookingstatus_subject_rejected'] = 'Betreff für Abgelehnt'; $string['bookit:addevent'] = 'Add an event'; $string['bookit:addinstance'] = 'BookIt Instanz hinzufügen'; $string['bookit:editevent'] = 'Edit an event'; @@ -176,6 +220,7 @@ $string['event_bookingstatus_4'] = 'Abgelehnt'; $string['event_bookingstatus_help'] = 'Erklärung der Buchungsstatus-Optionen.'; $string['event_bookingstatus_list'] = 'Neu, In Bearbeitung, Bestätigt, Storniert, Abgelehnt'; +$string['event_cancel_only_notice'] = 'Diese Anfrage kann nicht mehr frei bearbeitet werden. Es steht nur noch die Stornierung zur Verfügung.'; $string['event_checklist:done'] = 'erledigt'; $string['event_checklist:go_to_resources'] = 'Veranstaltungsressourcen'; $string['event_checklist:progress'] = 'Checklisten-Fortschritt'; @@ -189,7 +234,7 @@ $string['event_details'] = 'Veranstaltungsdetails'; $string['event_duration'] = 'Dauer des Termins (in Minuten)'; $string['event_duration_help'] = 'Tragen Sie die Dauer des Events ein.'; -$string['event_error_mintime'] = 'You cannot enter events in the past.'; +$string['event_error_mintime'] = 'Sie koennen keine Termine in der Vergangenheit anlegen.'; $string['event_extratime_description'] = 'Zusätzlich wird Zeit vor und nach dem Termin für Vor- und Nachbereitung automatisch hinzugefügt.'; $string['event_extratime_label'] = 'Extra time for the event'; $string['event_internalnotes'] = 'Interne Hinweise'; @@ -223,6 +268,7 @@ $string['event_students_help'] = 'Tragen Sie die erwartete Anzahl der Teilnehmenden ein.'; $string['event_supportperson'] = 'Support persons'; $string['event_supportperson_help'] = 'Support persons assigned to this event.'; +$string['event_supportperson_internalnotes_notice'] = 'Support vor Ort darf nur das Feld "Interne Notizen" bearbeiten.'; $string['event_timecompensation'] = 'Time compensation'; $string['event_timecompensation_help'] = 'Check if you have participants entitled to time compensation.'; $string['event_usermodified'] = 'Created by user'; @@ -233,6 +279,19 @@ $string['export_success'] = 'Export erfolgreich abgeschlossen'; $string['exportedon'] = 'Exportiert am: {$a}'; $string['exportevents'] = 'Ereignisse exportieren'; +$string['exportevents_from'] = 'Von'; +$string['exportevents_ics_bookingperson'] = 'Buchende Person'; +$string['exportevents_ics_duration'] = 'Dauer'; +$string['exportevents_ics_faculty'] = 'Fakultät'; +$string['exportevents_ics_otherexaminers'] = 'Weitere Prüfende'; +$string['exportevents_ics_participants'] = 'Teilnehmende'; +$string['exportevents_ics_personincharge'] = 'Verantwortliche Person'; +$string['exportevents_ics_requirements'] = 'Anforderungen'; +$string['exportevents_ics_semester'] = 'Semester'; +$string['exportevents_reset_range'] = 'Zeitraum zurücksetzen'; +$string['exportevents_selected'] = 'Ausgewählt'; +$string['exportevents_selectedcount'] = 'Ausgewählt: {$a}'; +$string['exportevents_to'] = 'Bis'; $string['exportfailed'] = 'Export fehlgeschlagen: {$a}'; $string['filters'] = 'Filter: '; $string['filters:label'] = 'Filter:'; @@ -307,7 +366,27 @@ $string['overlapping_non_confirmed'] = 'Überschneidung von nicht bestätigten Terminen erlauben'; $string['overview'] = 'Meine gebuchten Ereignisse'; $string['overview_action_requires_confirmed_booking'] = 'Checkliste und Ressourcen sind erst nach Bestätigung der Buchungsanfrage verfügbar.'; +$string['overview_all_events'] = 'Alle Buchungen'; +$string['overview_apply_filters'] = 'Filter anwenden'; +$string['overview_count'] = '{$a} Ereignisse'; +$string['overview_filter_enddate'] = 'Enddatum'; +$string['overview_filter_startdate'] = 'Startdatum'; $string['overview_help'] = 'Zeigt jedes Ereignis an, für das Sie als Prüfer aufgeführt sind.'; +$string['overview_my_events'] = 'Meine gebuchten Ereignisse'; +$string['overview_no_results'] = 'Zu den gewählten Filtern wurden keine Buchungen gefunden.'; +$string['overview_open_request_count'] = '{$a} offene Anfragen'; +$string['overview_open_requests'] = 'Offene Anfragen'; +$string['overview_open_requests_empty'] = 'Aktuell liegen keine offenen Buchungsanfragen vor.'; +$string['overview_open_requests_help'] = 'Zeigt alle Buchungsanfragen mit Status Neu oder In Bearbeitung.'; +$string['overview_reporting_help'] = 'Zeigt alle Buchungen im gewählten Zeitraum und Semester.'; +$string['overview_reset_filters'] = 'Filter zurücksetzen'; +$string['overview_role_bookingperson'] = 'Buchende Person'; +$string['overview_role_otherexaminer'] = 'Weitere prüfende Person'; +$string['overview_role_personincharge'] = 'Verantwortliche Person'; +$string['overview_role_supportperson'] = 'Support-Person'; +$string['overview_status_group_closed'] = 'Abgeschlossen / nicht bestätigt'; +$string['overview_status_group_confirmed'] = 'Bestätigte Buchung'; +$string['overview_status_group_open'] = 'Offene Anfrage'; $string['overwrite_extratimeafter'] = 'Globale extratimeafter-Einstellung überschreiben?'; $string['overwrite_extratimebefore'] = 'Globale extratimebefore-Einstellung überschreiben?'; $string['pdf_format'] = 'PDF (Portable Document Format)'; diff --git a/lang/en/bookit.php b/lang/en/bookit.php index 199f2b11..161f15ee 100644 --- a/lang/en/bookit.php +++ b/lang/en/bookit.php @@ -48,6 +48,50 @@ $string['booking:resources_booked'] = 'Booked Resources'; $string['booking:resources_header'] = 'Resources'; $string['booking:resources_info'] = 'Select the resources you need for this booking.'; +$string['bookingstatus_action_accept'] = 'Accept'; +$string['bookingstatus_action_cancel'] = 'Cancel'; +$string['bookingstatus_action_inprogress'] = 'Set in progress'; +$string['bookingstatus_action_reject'] = 'Reject'; +$string['bookingstatus_body_accepted'] = 'Message body for Accepted'; +$string['bookingstatus_body_canceled'] = 'Message body for Canceled'; +$string['bookingstatus_body_desc'] = 'Leave empty to use the default message. You can use placeholders documented above.'; +$string['bookingstatus_body_inprogress'] = 'Message body for In Progress'; +$string['bookingstatus_body_new'] = 'Message body for New'; +$string['bookingstatus_body_rejected'] = 'Message body for Rejected'; +$string['bookingstatus_notification_body_default'] = 'The booking request "###EVENTNAME###" has been updated to "###BOOKINGSTATUS###".' + . "\n\n" + . 'Room: ###ROOM###' + . "\n" + . 'Start: ###STARTTIME###' + . "\n" + . 'End: ###ENDTIME###' + . "\n" + . 'Booking person: ###BOOKINGPERSON###' + . "\n" + . 'Person in charge: ###PERSONINCHARGE###' + . "\n" + . 'Other examiners: ###OTHEREXAMINERS###' + . "\n\n" + . 'Open booking: ###EVENTURL###'; +$string['bookingstatus_notification_subject_default'] = 'Booking status updated: {$a->eventname} ({$a->bookingstatus})'; +$string['bookingstatus_notifications_desc'] = 'Configure recipients and message templates for booking-status changes. Supported placeholders: ###EVENTNAME###, ###BOOKINGSTATUS###, ###OLDBOOKINGSTATUS###, ###EVENTURL###, ###ROOM###, ###STARTTIME###, ###ENDTIME###, ###BOOKINGPERSON###, ###PERSONINCHARGE###, ###OTHEREXAMINERS###.'; +$string['bookingstatus_notifications_heading'] = 'Booking status notifications'; +$string['bookingstatus_notify_bookingperson'] = 'Notify booking person'; +$string['bookingstatus_notify_bookingperson_desc'] = 'Send booking-status messages to the user who created the booking request.'; +$string['bookingstatus_notify_otherexaminers'] = 'Notify other examiners'; +$string['bookingstatus_notify_otherexaminers_desc'] = 'Send booking-status messages to all additional examiners assigned to the booking request.'; +$string['bookingstatus_notify_personincharge'] = 'Notify person in charge'; +$string['bookingstatus_notify_personincharge_desc'] = 'Send booking-status messages to the responsible examiner.'; +$string['bookingstatus_service_addresses'] = 'University service addresses'; +$string['bookingstatus_service_addresses_desc'] = 'Optional list of email addresses that should receive booking-status notifications (comma, semicolon, or whitespace separated).'; +$string['bookingstatus_service_recipient_firstname'] = 'University'; +$string['bookingstatus_service_recipient_lastname'] = 'Service'; +$string['bookingstatus_subject_accepted'] = 'Message subject for Accepted'; +$string['bookingstatus_subject_canceled'] = 'Message subject for Canceled'; +$string['bookingstatus_subject_desc'] = 'Leave empty to use the default subject.'; +$string['bookingstatus_subject_inprogress'] = 'Message subject for In Progress'; +$string['bookingstatus_subject_new'] = 'Message subject for New'; +$string['bookingstatus_subject_rejected'] = 'Message subject for Rejected'; $string['bookit:addevent'] = 'Add an event'; $string['bookit:addinstance'] = 'Add BookIt instance'; $string['bookit:editevent'] = 'Edit an event'; @@ -181,6 +225,7 @@ $string['event_bookingstatus_4'] = 'Rejected'; $string['event_bookingstatus_help'] = 'Explanation of the booking status options.'; $string['event_bookingstatus_list'] = 'New, In progress, Accepted, Canceled, Rejected'; +$string['event_cancel_only_notice'] = 'This request can no longer be edited freely. You may only cancel it.'; $string['event_checklist:done'] = 'done'; $string['event_checklist:go_to_resources'] = 'Event Resources'; $string['event_checklist:progress'] = 'Checklist Progress'; @@ -228,6 +273,7 @@ $string['event_students_help'] = 'Enter amount of participants as a number.'; $string['event_supportperson'] = 'Support persons'; $string['event_supportperson_help'] = 'Support persons assigned to this event.'; +$string['event_supportperson_internalnotes_notice'] = 'Support on site may edit only the internal notes field.'; $string['event_timecompensation'] = 'Time compensation'; $string['event_timecompensation_help'] = 'Check if you have participants entitled to time compensation.'; $string['event_usermodified'] = 'Created by user'; @@ -238,6 +284,19 @@ $string['export_success'] = 'Export completed successfully'; $string['exportedon'] = 'Exported on: {$a}'; $string['exportevents'] = 'Export events'; +$string['exportevents_from'] = 'From'; +$string['exportevents_ics_bookingperson'] = 'Booking person'; +$string['exportevents_ics_duration'] = 'Duration'; +$string['exportevents_ics_faculty'] = 'Faculty'; +$string['exportevents_ics_otherexaminers'] = 'Other examiners'; +$string['exportevents_ics_participants'] = 'Participants'; +$string['exportevents_ics_personincharge'] = 'Person in charge'; +$string['exportevents_ics_requirements'] = 'Requirements'; +$string['exportevents_ics_semester'] = 'Semester term'; +$string['exportevents_reset_range'] = 'Reset range'; +$string['exportevents_selected'] = 'Selected'; +$string['exportevents_selectedcount'] = 'Selected: {$a}'; +$string['exportevents_to'] = 'To'; $string['exportfailed'] = 'Export failed: {$a}'; $string['filtercategories'] = 'Filtercategories'; $string['filters'] = 'Filters: '; @@ -314,7 +373,27 @@ $string['overlapping_non_confirmed'] = 'Allow overlapping with non-confirmed events'; $string['overview'] = 'My booked events'; $string['overview_action_requires_confirmed_booking'] = 'Checklist and resources are available only after the booking has been confirmed.'; +$string['overview_all_events'] = 'All bookings'; +$string['overview_apply_filters'] = 'Apply filters'; +$string['overview_count'] = '{$a} events'; +$string['overview_filter_enddate'] = 'End date'; +$string['overview_filter_startdate'] = 'Start date'; $string['overview_help'] = 'Shows every event for which you are listed as examiner.'; +$string['overview_my_events'] = 'My booked events'; +$string['overview_no_results'] = 'No bookings match the selected filters.'; +$string['overview_open_request_count'] = '{$a} open requests'; +$string['overview_open_requests'] = 'Open requests'; +$string['overview_open_requests_empty'] = 'There are currently no open booking requests.'; +$string['overview_open_requests_help'] = 'Shows all booking requests with status New or In Progress.'; +$string['overview_reporting_help'] = 'Shows all bookings in the selected period and semester.'; +$string['overview_reset_filters'] = 'Reset filters'; +$string['overview_role_bookingperson'] = 'Booking person'; +$string['overview_role_otherexaminer'] = 'Other examiner'; +$string['overview_role_personincharge'] = 'Person in charge'; +$string['overview_role_supportperson'] = 'Support person'; +$string['overview_status_group_closed'] = 'Closed / not confirmed'; +$string['overview_status_group_confirmed'] = 'Confirmed booking'; +$string['overview_status_group_open'] = 'Open request'; $string['overwrite_extratimeafter'] = 'Overwrite global extratimeafter setting?'; $string['overwrite_extratimebefore'] = 'Overwrite global extratimebefore setting?'; $string['pdf_format'] = 'PDF (Portable Document Format)'; diff --git a/overview.php b/overview.php index f4974f51..acffd274 100644 --- a/overview.php +++ b/overview.php @@ -24,6 +24,11 @@ require(__DIR__ . '/../../config.php'); +use mod_bookit\local\manager\event_access_manager; +use mod_bookit\local\manager\event_checklist_state_manager; +use mod_bookit\local\manager\event_manager; +use mod_bookit\local\manager\event_resource_manager; + /* ======================================================================= 0. Setup, capability checks ======================================================================= */ @@ -35,11 +40,48 @@ require_login($course, false, $cm); require_capability('mod/bookit:viewownoverview', $context); +global $USER, $DB; +$tab = optional_param('tab', 'myevents', PARAM_ALPHA); +$canmanage = has_capability('mod/bookit:managebasics', $context); +$canmanageopenrequests = event_access_manager::can_manage_open_requests($context); +$showreportfilters = $canmanageopenrequests; +$currenttab = ($tab === 'openrequests' && $canmanageopenrequests) ? 'openrequests' : 'myevents'; +$tableid = $currenttab === 'openrequests' ? 'open-requests-table' : 'overview-table'; + +[$defaultreportstart, $defaultreportend] = event_manager::get_reporting_default_range(); +$defaultreportstartvalue = date('Y-m-d', $defaultreportstart); +$defaultreportendvalue = date('Y-m-d', $defaultreportend); +$reportstartvalue = optional_param('reportstart', $defaultreportstartvalue, PARAM_TEXT); +$reportendvalue = optional_param('reportend', $defaultreportendvalue, PARAM_TEXT); +$selectedsemesterids = optional_param_array('semesterids', [], PARAM_INT); +$hasexplicitreportfilters = $showreportfilters && ( + array_key_exists('reportstart', $_GET) + || array_key_exists('reportend', $_GET) + || array_key_exists('semesterids', $_GET) +); +if ($showreportfilters && empty($selectedsemesterids)) { + $selectedsemesterids = [event_manager::get_current_semester()]; +} +$appliedsemesterids = $hasexplicitreportfilters ? $selectedsemesterids : []; + +$parsetimestamp = static function (string $value, int $fallback, bool $endofday = false): int { + $date = \DateTime::createFromFormat('Y-m-d H:i:s', $value . ($endofday ? ' 23:59:59' : ' 00:00:00')); + if (!$date) { + return $fallback; + } + return $date->getTimestamp(); +}; + +$reportstarttimestamp = $parsetimestamp($reportstartvalue, $defaultreportstart); +$reportendtimestamp = $parsetimestamp($reportendvalue, $defaultreportend, true); +if ($reportendtimestamp < $reportstarttimestamp) { + [$reportstarttimestamp, $reportendtimestamp] = [$reportendtimestamp, $reportstarttimestamp]; + [$reportstartvalue, $reportendvalue] = [$reportendvalue, $reportstartvalue]; +} + /* ======================================================================= 1. Front-end requirements ======================================================================= */ -$tableid = 'overview-table'; - $PAGE->requires->jquery(); /* ----- live search ---------------------------------------------------- */ @@ -100,7 +142,7 @@ 2. Page headings ======================================================================= */ $PAGE->set_url('/mod/bookit/overview.php', ['id' => $cm->id]); -$PAGE->set_title(get_string('overview', 'bookit')); +$PAGE->set_title(get_string('overview', 'mod_bookit')); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); @@ -108,13 +150,27 @@ /* ======================================================================= 3. Fetch examiner’s events ======================================================================= */ -use mod_bookit\local\manager\event_manager; -use mod_bookit\local\manager\event_access_manager; -use mod_bookit\local\manager\event_checklist_state_manager; -use mod_bookit\local\manager\event_resource_manager; - -global $USER, $DB; -$events = event_manager::get_events_for_examiner($USER->id); +$events = $showreportfilters + ? event_manager::get_events_for_reporting( + $context, + (int)$USER->id, + $reportstarttimestamp, + $reportendtimestamp, + $appliedsemesterids + ) + : event_manager::get_events_for_examiner($USER->id); +$openrequests = $canmanageopenrequests ? event_manager::get_open_requests() : []; +$openrequestcount = $canmanageopenrequests ? event_manager::count_open_requests() : 0; +$semesteroptions = []; +if ($showreportfilters) { + foreach (event_manager::get_semester_filter_options() as $value => $label) { + $semesteroptions[] = [ + 'value' => (string)$value, + 'label' => $label, + 'selected' => in_array((int)$value, $selectedsemesterids, true), + ]; + } +} // Fetch master checklist ID directly (no entity = no JS side effects). $masterrecord = $DB->get_record('bookit_checklist_master', ['isdefault' => 1], 'id', IGNORE_MULTIPLE); @@ -128,12 +184,41 @@ 4+5. Render via Mustache template (no HTML in PHP) ======================================================================= */ -// Prepare template context. -$canmanage = has_capability('mod/bookit:managebasics', $context); $templatecontext = [ - 'tableid' => (string)$tableid, - 'canmanage' => $canmanage, - 'events' => [], + 'cmid' => $cm->id, + 'tableid' => (string)$tableid, + 'canmanage' => $canmanage, + 'showopenrequestsnav' => $canmanageopenrequests, + 'showmyeventssection' => $currenttab === 'myevents', + 'showopenrequestssection' => $currenttab === 'openrequests', + 'myeventsactive' => $currenttab === 'myevents', + 'openrequestsactive' => $currenttab === 'openrequests', + 'myeventsurl' => (new moodle_url('/mod/bookit/overview.php', ['id' => $cm->id, 'tab' => 'myevents']))->out(false), + 'openrequestsurl' => (new moodle_url('/mod/bookit/overview.php', ['id' => $cm->id, 'tab' => 'openrequests']))->out(false), + 'myeventstitle' => $showreportfilters + ? get_string('overview_all_events', 'mod_bookit') + : get_string('overview_my_events', 'mod_bookit'), + 'openrequeststitle' => get_string('overview_open_requests', 'mod_bookit'), + 'openrequestshelp' => get_string('overview_open_requests_help', 'mod_bookit'), + 'openrequestsempty' => get_string('overview_open_requests_empty', 'mod_bookit'), + 'openrequestcount' => $openrequestcount, + 'openrequestcounttext' => get_string('overview_open_request_count', 'mod_bookit', $openrequestcount), + 'showreportfilters' => $showreportfilters, + 'reportinghelp' => get_string('overview_reporting_help', 'mod_bookit'), + 'reportstartvalue' => $reportstartvalue, + 'reportendvalue' => $reportendvalue, + 'reportstartlabel' => get_string('overview_filter_startdate', 'mod_bookit'), + 'reportendlabel' => get_string('overview_filter_enddate', 'mod_bookit'), + 'reportapplylabel' => get_string('overview_apply_filters', 'mod_bookit'), + 'reportresetlabel' => get_string('overview_reset_filters', 'mod_bookit'), + 'reportreseturl' => (new moodle_url('/mod/bookit/overview.php', ['id' => $cm->id, 'tab' => 'myevents']))->out(false), + 'semesteroptions' => $semesteroptions, + 'hasevents' => false, + 'eventcounttext' => get_string('overview_count', 'mod_bookit', 0), + 'noeventsmessage' => get_string('overview_no_results', 'mod_bookit'), + 'hasopenrequests' => !empty($openrequests), + 'events' => [], + 'openrequests' => [], ]; // Precompute checklist progress for all events in a single query. @@ -147,34 +232,48 @@ $resourceprogressmap = event_resource_manager::get_resource_progress_for_events($eventids); } -foreach ($events as $ev) { +$prepareeventrow = function ( + stdClass $ev, + bool $isopenrequest = false +) use ( + $USER, + $context, + $cm, + $masterid, + $canmanage, + $statuscolors, + $progressmap, + $resourceprogressmap +): array { $room = $ev->room ?: '-'; $statusbg = $statuscolors[$ev->bookingstatus]['bg'] ?? '#ffffff'; $statusfg = $statuscolors[$ev->bookingstatus]['fg'] ?? '#000000'; $statustxt = get_string('event_bookingstatus_' . (int)($ev->bookingstatus ?? 0), 'mod_bookit'); + $statusclass = event_manager::get_booking_status_class((int)($ev->bookingstatus ?? 0)); + $statusgroupkey = match ((int)($ev->bookingstatus ?? 0)) { + event_access_manager::BOOKINGSTATUS_NEW, + event_access_manager::BOOKINGSTATUS_IN_PROGRESS => 'open', + event_access_manager::BOOKINGSTATUS_ACCEPTED => 'confirmed', + default => 'closed', + }; + $statusgrouptext = get_string('overview_status_group_' . $statusgroupkey, 'mod_bookit'); // My role. $myrole = '-'; $roles = []; - if ($USER->id == $ev->personinchargeid) { - $roles[] = 'Person in charge'; - } - - if ($USER->id == $ev->usermodified) { - $roles[] = 'Booking person'; - } - - $otherids = array_filter(explode(',', $ev->otherexaminers ?? '')); - if (in_array($USER->id, $otherids)) { - $roles[] = 'Other examiner'; - } - - $supportids = array_filter(explode(',', $ev->supportpersons ?? '')); - if (in_array($USER->id, $supportids)) { - $roles[] = 'Support person'; + foreach (event_access_manager::get_user_roles_for_event($ev, (int)$USER->id) as $role) { + if ($role === 'personincharge') { + $roles[] = get_string('overview_role_personincharge', 'mod_bookit'); + } else if ($role === 'bookingperson') { + $roles[] = get_string('overview_role_bookingperson', 'mod_bookit'); + } else if ($role === 'otherexaminer') { + $roles[] = get_string('overview_role_otherexaminer', 'mod_bookit'); + } else if ($role === 'supportperson') { + $roles[] = get_string('overview_role_supportperson', 'mod_bookit'); + } } $myrole = $roles ? implode(', ', $roles) : '-'; @@ -189,19 +288,57 @@ $pic = $u ? fullname($u) : '-'; } - $templatecontext['events'][] = [ + $actions = []; + if ($isopenrequest) { + foreach (event_manager::get_booking_status_transition_options((int)($ev->bookingstatus ?? 0)) as $option) { + $btnclass = 'btn-outline-secondary'; + if ((int)$option['value'] === event_access_manager::BOOKINGSTATUS_IN_PROGRESS) { + $btnclass = 'btn-warning'; + } else if ((int)$option['value'] === event_access_manager::BOOKINGSTATUS_ACCEPTED) { + $btnclass = 'btn-success'; + } else if ((int)$option['value'] === event_access_manager::BOOKINGSTATUS_CANCELED) { + $btnclass = 'btn-dark'; + } else if ((int)$option['value'] === event_access_manager::BOOKINGSTATUS_REJECTED) { + $btnclass = 'btn-danger'; + } + + $actions[] = [ + 'value' => (int)$option['value'], + 'eventid' => (int)$ev->id, + 'cmid' => (int)$cm->id, + 'label' => match ((int)$option['value']) { + event_access_manager::BOOKINGSTATUS_IN_PROGRESS => get_string('bookingstatus_action_inprogress', 'mod_bookit'), + event_access_manager::BOOKINGSTATUS_ACCEPTED => get_string('bookingstatus_action_accept', 'mod_bookit'), + event_access_manager::BOOKINGSTATUS_CANCELED => get_string('bookingstatus_action_cancel', 'mod_bookit'), + event_access_manager::BOOKINGSTATUS_REJECTED => get_string('bookingstatus_action_reject', 'mod_bookit'), + default => $option['label'], + }, + 'btnclass' => $btnclass, + ]; + } + } + + $caneventdetails = event_access_manager::can_user_view_event_details($ev, $context, (int)$USER->id); + + return [ 'id' => (string)$ev->id, 'name' => format_string($ev->name), + 'caneventdetails' => $caneventdetails, 'room' => s($room), 'personincharge' => s($pic), 'myrole' => s($myrole), - 'statustext' => s($statustxt), - 'statusstyle' => "background-color:$statusbg;color:$statusfg;", + 'statustext' => s($statustxt), + 'statusstyle' => "background-color:$statusbg;color:$statusfg;", + 'statusclass' => $statusclass, + 'statusgroupkey' => $statusgroupkey, + 'statusgrouptext' => s($statusgrouptext), 'bookingstatus' => (int)($ev->bookingstatus ?? 0), 'canmanage' => $canmanage, 'statusoptions' => $canmanage ? event_manager::get_booking_status_options((int)($ev->bookingstatus ?? 0)) : [], + 'hasactions' => !empty($actions), + 'actions' => $actions, 'datestr' => $datestr, 'starttime' => (int)$ev->starttime, 'cmid' => (int)$cm->id, @@ -222,6 +359,19 @@ 'resourcesprogress' => $resourceprogressmap[(int)$ev->id]['percent'] ?? 0, 'resourcesprogress_available' => ($resourceprogressmap[(int)$ev->id]['total'] ?? 0) > 0, ]; +}; + +foreach ($events as $ev) { + if (event_access_manager::can_user_view_event_in_overview($ev, $context, (int)$USER->id)) { + $templatecontext['events'][] = $prepareeventrow($ev); + } +} + +$templatecontext['hasevents'] = !empty($templatecontext['events']); +$templatecontext['eventcounttext'] = get_string('overview_count', 'mod_bookit', count($templatecontext['events'])); + +foreach ($openrequests as $ev) { + $templatecontext['openrequests'][] = $prepareeventrow($ev, true); } // Render Mustache. diff --git a/settings.php b/settings.php index 9e2064ec..92e8f28e 100644 --- a/settings.php +++ b/settings.php @@ -56,6 +56,67 @@ )); // XXX TODO: write some text as introduction to bookit. + $settings->add(new admin_setting_heading( + 'mod_bookit_bookingstatus_notifications_heading', + get_string('bookingstatus_notifications_heading', 'mod_bookit'), + get_string('bookingstatus_notifications_desc', 'mod_bookit') + )); + + $settings->add(new admin_setting_configtext( + 'mod_bookit/bookingstatus_service_addresses', + get_string('bookingstatus_service_addresses', 'mod_bookit'), + get_string('bookingstatus_service_addresses_desc', 'mod_bookit'), + '', + PARAM_TEXT + )); + + $settings->add(new admin_setting_configcheckbox( + 'mod_bookit/bookingstatus_notify_bookingperson', + get_string('bookingstatus_notify_bookingperson', 'mod_bookit'), + get_string('bookingstatus_notify_bookingperson_desc', 'mod_bookit'), + 1 + )); + + $settings->add(new admin_setting_configcheckbox( + 'mod_bookit/bookingstatus_notify_personincharge', + get_string('bookingstatus_notify_personincharge', 'mod_bookit'), + get_string('bookingstatus_notify_personincharge_desc', 'mod_bookit'), + 1 + )); + + $settings->add(new admin_setting_configcheckbox( + 'mod_bookit/bookingstatus_notify_otherexaminers', + get_string('bookingstatus_notify_otherexaminers', 'mod_bookit'), + get_string('bookingstatus_notify_otherexaminers_desc', 'mod_bookit'), + 1 + )); + + $notificationstatuses = [ + 0 => 'new', + 1 => 'inprogress', + 2 => 'accepted', + 3 => 'canceled', + 4 => 'rejected', + ]; + + foreach ($notificationstatuses as $statusid => $statuskey) { + $settings->add(new admin_setting_configtext( + 'mod_bookit/bookingstatus_subject_' . $statusid, + get_string('bookingstatus_subject_' . $statuskey, 'mod_bookit'), + get_string('bookingstatus_subject_desc', 'mod_bookit'), + '', + PARAM_TEXT + )); + + $settings->add(new admin_setting_configtextarea( + 'mod_bookit/bookingstatus_body_' . $statusid, + get_string('bookingstatus_body_' . $statuskey, 'mod_bookit'), + get_string('bookingstatus_body_desc', 'mod_bookit'), + '', + PARAM_RAW + )); + } + // Install helper. // XXX TODO: remove next line!! set_config('installhelperfinished', 0, 'mod_bookit'); diff --git a/styles.css b/styles.css index d12186a2..d9d90906 100644 --- a/styles.css +++ b/styles.css @@ -227,3 +227,144 @@ white-space: nowrap; vertical-align: middle; } + +.mod_bookit-overview-examiner_overview .nav-tabs .badge { + vertical-align: middle; +} + +.mod-bookit-open-request-actions { + gap: 0.35rem; +} + +.mod-bookit-open-requests-table tr.mod-bookit-open-request-row td { + vertical-align: middle; +} + +.mod-bookit-status-cell { + min-width: 10rem; +} + +.mod-bookit-status-chip { + display: inline-flex; + flex-direction: column; + gap: 0.1rem; + min-width: 8.5rem; + padding: 0.45rem 0.65rem; + border: 1px solid transparent; + border-radius: 0.5rem; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); +} + +.mod-bookit-status-row-open td:first-child { + box-shadow: inset 4px 0 0 #ffe08a; +} + +.mod-bookit-status-row-confirmed td:first-child { + box-shadow: inset 4px 0 0 #198754; +} + +.mod-bookit-status-row-closed td:first-child { + box-shadow: inset 4px 0 0 #842029; +} + +.mod-bookit-status-chip-open, +.ec-event.bookit-bookingstatus-0, +.ec-event.bookit-bookingstatus-1 { + opacity: 0.82; + border-style: dashed !important; +} + +.mod-bookit-status-chip-confirmed, +.ec-event.bookit-bookingstatus-2 { + opacity: 1; + font-weight: 600; +} + +.mod-bookit-status-chip-closed, +.ec-event.bookit-bookingstatus-3, +.ec-event.bookit-bookingstatus-4 { + opacity: 0.72; +} + +.bookit-bookingstatus-0, +.ec-event.bookit-bookingstatus-0 { + background: #e2e3e5 !important; + border-color: #c6c8ca !important; + color: #212529 !important; +} + +.bookit-bookingstatus-1, +.ec-event.bookit-bookingstatus-1 { + background: #fff3cd !important; + border-color: #ffe08a !important; + color: #664d03 !important; +} + +.bookit-bookingstatus-2, +.ec-event.bookit-bookingstatus-2 { + background: #d1e7dd !important; + border-color: #a3cfbb !important; + color: #0f5132 !important; +} + +.bookit-bookingstatus-3, +.ec-event.bookit-bookingstatus-3 { + background: #343a40 !important; + border-color: #343a40 !important; + color: #ffffff !important; +} + +.bookit-bookingstatus-4, +.ec-event.bookit-bookingstatus-4 { + background: #f8d7da !important; + border-color: #f1aeb5 !important; + color: #842029 !important; +} + +.ec-time-grid .ec-event, +.ec-day-grid .ec-event { + min-width: 0; +} + +.ec-time-grid .ec-event .ec-event-title, +.ec-day-grid .ec-event .ec-event-title { + white-space: normal; + overflow-wrap: anywhere; + line-height: 1.2; +} + +.ec-time-grid .ec-event .ec-event-time, +.ec-day-grid .ec-event .ec-event-time { + font-weight: 600; + line-height: 1.1; +} + +.ec-time-grid .ec-event .ec-event-body, +.ec-day-grid .ec-event .ec-event-body { + overflow: hidden; +} + +.bookit-export-toolbar .form-control[type="date"] { + min-width: 11rem; +} + +.bookit-export-item { + cursor: pointer; +} + +.bookit-export-item .form-check-input { + margin-top: 0.25rem; +} + +.bookit-export-item-text { + min-width: 0; + flex: 1 1 auto; +} + +.bookit-export-item-title { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem; + font-weight: 600; +} diff --git a/templates/view/examiner_overview.mustache b/templates/view/examiner_overview.mustache index c5176f6e..f24a0a99 100644 --- a/templates/view/examiner_overview.mustache +++ b/templates/view/examiner_overview.mustache @@ -75,11 +75,66 @@ }}
+
+ +
+
+ {{#showmyeventssection}} +
+

{{myeventstitle}}

+ {{#showreportfilters}} +
{{reportinghelp}}
+ {{/showreportfilters}} +
+ {{#showreportfilters}} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + {{reportresetlabel}} +
+
+
+
{{eventcounttext}}
+ {{/showreportfilters}} + {{#hasevents}} @@ -98,16 +153,19 @@ {{#events}} - + @@ -115,7 +173,7 @@ {{#canmanage}} - {{/canmanage}} {{^canmanage}} - + {{/canmanage}} @@ -179,4 +244,90 @@ {{/events}}
{{id}} + {{#caneventdetails}} {{name}} + {{/caneventdetails}} + {{^caneventdetails}}{{name}}{{/caneventdetails}} {{room}}{{myrole}} + +
{{statusgrouptext}}
{{statustext}} +
+
{{statustext}}
+
{{statusgrouptext}}
+
+
{{datestr}}
+ {{#showreportfilters}} +
{{eventcounttext}}
+ {{/showreportfilters}} + {{/hasevents}} + {{^hasevents}} +
{{noeventsmessage}}
+ {{/hasevents}} + {{/showmyeventssection}} + + {{#showopenrequestssection}} +
+
+

{{openrequeststitle}}

+
{{openrequestshelp}}
+
+
+ {{openrequestcounttext}} +
+
+ + {{#hasopenrequests}} + + + + + + + + + + + + + + + {{#openrequests}} + + + + + + + + + + + {{/openrequests}} + +
IDTitleRoomPerson in chargeMy roleBooking statusDateActions
{{id}} + {{#caneventdetails}} + + {{name}} + + {{/caneventdetails}} + {{^caneventdetails}}{{name}}{{/caneventdetails}} + {{room}}{{personincharge}}{{myrole}} +
+
{{statustext}}
+
{{statusgrouptext}}
+
+
{{datestr}} +
+ {{#actions}} + + {{/actions}} +
+
+ {{/hasopenrequests}} + + {{^hasopenrequests}} +
+ {{openrequestsempty}} +
+ {{/hasopenrequests}} + {{/showopenrequestssection}}
diff --git a/templates/view/export_events_modal.mustache b/templates/view/export_events_modal.mustache index 9f521c42..d32d8cd1 100644 --- a/templates/view/export_events_modal.mustache +++ b/templates/view/export_events_modal.mustache @@ -55,26 +55,39 @@