From 3c42d9cfc6bba0c366864c2cbafa1236233c4d1f Mon Sep 17 00:00:00 2001 From: Laurent David Date: Wed, 28 Jan 2026 07:34:00 +0100 Subject: [PATCH 01/15] Fix issue #790 with calculation of totals in edition --- amd/build/manager.min.js | 2 +- amd/build/manager.min.js.map | 2 +- amd/src/manager.js | 2 +- scss/styles.scss | 27 ++++++++++++++----------- styles.css | 14 +++++++------ templates/table/columnsheader.mustache | 8 ++++---- templates/table/moduleshistory.mustache | 4 ++-- templates/table/rows.mustache | 4 ++-- version.php | 4 ++-- 9 files changed, 36 insertions(+), 31 deletions(-) diff --git a/amd/build/manager.min.js b/amd/build/manager.min.js index 243270b..2443e9e 100644 --- a/amd/build/manager.min.js +++ b/amd/build/manager.min.js @@ -1,3 +1,3 @@ -define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",todelete:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.oldvalue):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); +define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",todelete:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+(parseFloat(cell.oldvalue)||0):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=manager.min.js.map \ No newline at end of file diff --git a/amd/build/manager.min.js.map b/amd/build/manager.min.js.map index 28a8a7a..1cb9170 100644 --- a/amd/build/manager.min.js.map +++ b/amd/build/manager.min.js.map @@ -1 +1 @@ -{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'todelete': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.oldvalue);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'todelete': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/manager.js b/amd/src/manager.js index f669284..34b07a6 100644 --- a/amd/src/manager.js +++ b/amd/src/manager.js @@ -636,7 +636,7 @@ class Manager { const column = columnsData.find(c => c.columnid === cell.columnid); if (column) { if (cell.changed) { - column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.oldvalue); + column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0); } else if (cell.value && cell.value !== null) { column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value); } diff --git a/scss/styles.scss b/scss/styles.scss index 4198dfe..e2d8be1 100644 --- a/scss/styles.scss +++ b/scss/styles.scss @@ -218,18 +218,15 @@ } .sumcontainer { - position: absolute; - left: 0; - right: 0; + display: flex; + flex-direction: column; background-color: #f7f7f7; padding: 0 10px; bottom: 5px; - display: flex; justify-content: space-between; align-items: center; border-radius: 50%; width: 100%; - height: 20px; color: var(--dark); } } @@ -298,13 +295,19 @@ } } - td.static { - padding: 0.4rem; - } - - td.haschanges { - input { - background-color: #f4ffff; + td { + &.static { + padding: 0.4rem; + } + &.has-changes { + input { + background-color: #f4ffff; + } + } + .change { + display: flex; + flex-direction: column; + align-items: flex-start; } } } diff --git a/styles.css b/styles.css index d430818..a309718 100644 --- a/styles.css +++ b/styles.css @@ -194,18 +194,15 @@ min-width: 55.55px; } .customfield-sprogramme .programm-table.toprow .sumcontainer { - position: absolute; - left: 0; - right: 0; + display: flex; + flex-direction: column; background-color: #f7f7f7; padding: 0 10px; bottom: 5px; - display: flex; justify-content: space-between; align-items: center; border-radius: 50%; width: 100%; - height: 20px; color: var(--dark); } .customfield-sprogramme .programm-table th, @@ -266,9 +263,14 @@ .customfield-sprogramme .programm-table td.static { padding: 0.4rem; } -.customfield-sprogramme .programm-table td.haschanges input { +.customfield-sprogramme .programm-table td.has-changes input { background-color: #f4ffff; } +.customfield-sprogramme .programm-table td .change { + display: flex; + flex-direction: column; + align-items: flex-start; +} .customfield-sprogramme.syllabuspage .programm-table { margin-left: 1rem; width: calc(100% - 1rem); diff --git a/templates/table/columnsheader.mustache b/templates/table/columnsheader.mustache index 80fbb59..6659adf 100644 --- a/templates/table/columnsheader.mustache +++ b/templates/table/columnsheader.mustache @@ -54,17 +54,17 @@ {{#columns}} {{{label}}} -
+
{{#changed}}{{sum}}{{/changed}} - {{^changed}}{{#newsum}}{{newsum}}{{/newsum}}{{/changed}}{{#changed}}{{newsum}}{{/changed}} + {{^changed}}{{#newsum}}{{newsum}}{{/newsum}}{{/changed}}{{#changed}}{{newsum}}{{/changed}}
{{/columns}} {{#str}}overaltotals, customfield_sprogramme{{/str}} -
+
{{#columns.0.totalschanged}}{{columns.0.overaltotals}}{{/columns.0.totalschanged}} - {{columns.0.newsumtotals}} + {{columns.0.newsumtotals}}
diff --git a/templates/table/moduleshistory.mustache b/templates/table/moduleshistory.mustache index 01ed01f..b7a4e54 100644 --- a/templates/table/moduleshistory.mustache +++ b/templates/table/moduleshistory.mustache @@ -81,8 +81,8 @@ {{#columns}} {{{label}}} -
- {{#newsum}}{{newsum}}{{/newsum}} +
+ {{#newsum}}{{newsum}}{{/newsum}} {{#sum}}{{sum}}{{/sum}}
diff --git a/templates/table/rows.mustache b/templates/table/rows.mustache index 7287df2..9da5182 100644 --- a/templates/table/rows.mustache +++ b/templates/table/rows.mustache @@ -163,9 +163,9 @@ {{^editor}} {{#changed}} -
+
{{{oldvalue}}}
-
{{{value}}}
+
{{{value}}}
{{/changed}} {{^changed}} diff --git a/version.php b/version.php index 0ab092f..03364c2 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'customfield_sprogramme'; -$plugin->release = '2.2.0'; -$plugin->version = 2026010500; +$plugin->release = '2.2.1'; +$plugin->version = 2026012700; $plugin->requires = 2022112800; $plugin->maturity = MATURITY_BETA; From 0577414712c84f1843268ba1f1c6f3598d0dc11b Mon Sep 17 00:00:00 2001 From: Laurent David Date: Wed, 28 Jan 2026 07:34:23 +0100 Subject: [PATCH 02/15] Fix build script for scss --- .grunt/library.js | 5 ++++- package.json | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.grunt/library.js b/.grunt/library.js index 6981500..57b7019 100644 --- a/.grunt/library.js +++ b/.grunt/library.js @@ -43,7 +43,7 @@ const buildSass = (grunt) => { const rootGruntfile = path.join(moodleRoot, 'Gruntfile.js'); if (grunt.file.exists(rootGruntfile)) { process.chdir(moodleRoot); // Change to moodle root before loading the main Gruntfile. - // But do not change the process.env.PWD + // But do not change the process.env.PWD. require(rootGruntfile)(grunt); } const config = { @@ -73,10 +73,13 @@ const buildSass = (grunt) => { fix: true, cache: false, failOnError: false, + quietDeprecationWarnings: true, + customSyntax: 'postcss-scss', config: { rules: { "indentation": 4, "declaration-block-single-line-max-declarations": 1, + "selector-list-comma-newline-after": "always", } }, }, diff --git a/package.json b/package.json index 29e4d61..bf1e379 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "scss compiler", "main": "index.js", "scripts": { - "compile:sass": "grunt customfield_sprogramme", - "watch:sass": "grunt customfield_sprogramme --watch", + "compile:sass": "grunt sass:customfield_sprogramme && grunt stylelint:customfield_sprogramme", + "watch:sass": "grunt sass:customfield_sprogramme --watch", "livereload": "livereload -d .", "watch:mustache": "nodemon -e mustache -x \"/opt/homebrew/bin/php ../../admin/cli/purge_caches.php\"", "start": "npm-run-all --parallel watch:sass watch:mustache livereload" From 016b94838177fd4f1fdba3cf53e370847115807a Mon Sep 17 00:00:00 2001 From: Laurent David Date: Mon, 2 Feb 2026 20:31:15 +0100 Subject: [PATCH 03/15] Fix default values to 0 for float --- classes/local/persistent/sprogramme.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/classes/local/persistent/sprogramme.php b/classes/local/persistent/sprogramme.php index a3feccb..629aaa6 100644 --- a/classes/local/persistent/sprogramme.php +++ b/classes/local/persistent/sprogramme.php @@ -114,7 +114,7 @@ protected static function define_properties() { 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:type_ae'), ], 'sequence' => [ - 'default' => null, + 'default' => 0, 'null' => NULL_ALLOWED, 'type' => PARAM_INT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:sequence'), @@ -126,55 +126,55 @@ protected static function define_properties() { 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:intitule_seance'), ], 'cm' => [ - 'default' => null, + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:cm'), ], 'td' => [ - 'default' => null, + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:td'), ], 'tp' => [ - 'default' => '', + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:tp'), ], 'tpa' => [ - 'default' => '', + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:tpa'), ], 'tc' => [ - 'default' => '', + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:tc'), ], 'aas' => [ - 'default' => '', + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:aas'), ], 'fmp' => [ - 'default' => '', + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:fmp'), ], 'perso_av' => [ - 'default' => '', + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:perso_av'), ], 'perso_ap' => [ - 'default' => '', + 'default' => 0.0, 'null' => NULL_ALLOWED, 'type' => PARAM_FLOAT, 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:perso_ap'), From c8b1735b1990404db6c5382d8b0ab29e2e4b76e9 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Thu, 5 Feb 2026 08:05:40 +0100 Subject: [PATCH 04/15] Send the right signal for course info change Fixes #789 --- classes/local/programme_manager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/classes/local/programme_manager.php b/classes/local/programme_manager.php index 048faa3..8cfd4da 100644 --- a/classes/local/programme_manager.php +++ b/classes/local/programme_manager.php @@ -710,6 +710,7 @@ private function update_row(sprogramme $programme, array $row) { */ private function invalidate_cache(): void { cache_helper::invalidate_by_event('customfield_sprogramme/changesinprogramme', [$this->datafieldid]); + cache_helper::invalidate_by_event('changesincourse', [$this->courseid]); } /** From a286979ed4e623bf9274627b0f52976ece1f156e Mon Sep 17 00:00:00 2001 From: Laurent David Date: Thu, 5 Feb 2026 14:18:51 +0100 Subject: [PATCH 05/15] Fix #776 Difficulty to read discipline --- scss/styles.scss | 2 -- styles.css | 2 -- 2 files changed, 4 deletions(-) diff --git a/scss/styles.scss b/scss/styles.scss index e2d8be1..fec1f4d 100644 --- a/scss/styles.scss +++ b/scss/styles.scss @@ -100,7 +100,6 @@ display: flex; align-items: center; padding: 0.15rem 0.5rem; - width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -112,7 +111,6 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 120px; margin-right: 0.5rem; } diff --git a/styles.css b/styles.css index a309718..05ce495 100644 --- a/styles.css +++ b/styles.css @@ -90,7 +90,6 @@ display: flex; align-items: center; padding: 0.15rem 0.5rem; - width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -103,7 +102,6 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 120px; margin-right: 0.5rem; } .customfield-sprogramme .disciplines .badge .percentage, From 3b250d723688b88a8c94522c28f82fdbc7244c94 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Thu, 5 Feb 2026 16:53:14 +0100 Subject: [PATCH 06/15] Fix #774 row deletion was not showing --- amd/build/manager.min.js | 2 +- amd/build/manager.min.js.map | 2 +- amd/src/manager.js | 3 +-- classes/local/programme_manager.php | 2 +- scss/styles.scss | 30 +++++++++++++++----------- styles.css | 11 +++++++++- tests/local/programme_manager_test.php | 3 +++ 7 files changed, 35 insertions(+), 18 deletions(-) diff --git a/amd/build/manager.min.js b/amd/build/manager.min.js index 2443e9e..06c6764 100644 --- a/amd/build/manager.min.js +++ b/amd/build/manager.min.js @@ -1,3 +1,3 @@ -define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",todelete:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+(parseFloat(cell.oldvalue)||0):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); +define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+(parseFloat(cell.oldvalue)||0):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=manager.min.js.map \ No newline at end of file diff --git a/amd/build/manager.min.js.map b/amd/build/manager.min.js.map index 1cb9170..be03ca3 100644 --- a/amd/build/manager.min.js.map +++ b/amd/build/manager.min.js.map @@ -1 +1 @@ -{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'todelete': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Directly remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/manager.js b/amd/src/manager.js index 34b07a6..fcabbe0 100644 --- a/amd/src/manager.js +++ b/amd/src/manager.js @@ -237,7 +237,6 @@ class Manager { 'id': 'id', 'sortorder': 'sortorder', 'deleted': 'false', - 'todelete': 'false', 'cells': { 'type': 'type', 'column': 'column', @@ -542,7 +541,7 @@ class Manager { } }); } else { - // Remove the row from the module. + // Directly remove the row from the module. modulefound.rows.splice(rowIndex, 1); } State.setValue('modules', modules); diff --git a/classes/local/programme_manager.php b/classes/local/programme_manager.php index 8cfd4da..0087269 100644 --- a/classes/local/programme_manager.php +++ b/classes/local/programme_manager.php @@ -500,7 +500,7 @@ public function has_protected_data_changes(array $data): bool { foreach ($module['rows'] as $row) { $rowdeleted = $row['deleted'] ?? false; if ($rowdeleted) { - continue; // Skip deleted rows. + return true; // If a row is deleted, there are data changes for protected columns. } foreach ($row['cells'] as $cell) { $column = array_filter($columns, function ($col) use ($cell) { diff --git a/scss/styles.scss b/scss/styles.scss index fec1f4d..9ca153d 100644 --- a/scss/styles.scss +++ b/scss/styles.scss @@ -163,18 +163,24 @@ background-color: #f8f8f8; } - .deletepending input, - .deletepending textarea, - .deletepending select, - .deletepending .actions button { - text-decoration: line-through; - background-color: #f8f8f8; - } - - .deletepending .actions button, - .deletepending .disciplines button, - .deletepending .competencies button { - display: none; + .deletepending { + input, + textarea, + select, + .actions button, + .static + { + text-decoration: line-through; + background-color: #f8f8f8; + .newvalue { + text-decoration: none; + } + } + .actions button, + .disciplines button, + .competencies button { + display: none; + } } tr.haserror [data-group="unique"] { diff --git a/styles.css b/styles.css index 05ce495..e1d3cd2 100644 --- a/styles.css +++ b/styles.css @@ -151,10 +151,19 @@ .customfield-sprogramme .programm-table .deletepending input, .customfield-sprogramme .programm-table .deletepending textarea, .customfield-sprogramme .programm-table .deletepending select, -.customfield-sprogramme .programm-table .deletepending .actions button { +.customfield-sprogramme .programm-table .deletepending .actions button, +.customfield-sprogramme .programm-table .deletepending .static { text-decoration: line-through; background-color: #f8f8f8; } +.customfield-sprogramme .programm-table .deletepending input .newvalue, +.customfield-sprogramme .programm-table .deletepending textarea .newvalue, +.customfield-sprogramme .programm-table .deletepending select .newvalue, +.customfield-sprogramme .programm-table .deletepending .actions button .newvalue, +.customfield-sprogramme .programm-table .deletepending .static .newvalue { + text-decoration: none; + background-color: initial; +} .customfield-sprogramme .programm-table .deletepending .actions button, .customfield-sprogramme .programm-table .deletepending .disciplines button, .customfield-sprogramme .programm-table .deletepending .competencies button { diff --git a/tests/local/programme_manager_test.php b/tests/local/programme_manager_test.php index 79ca860..0c89e0b 100644 --- a/tests/local/programme_manager_test.php +++ b/tests/local/programme_manager_test.php @@ -519,6 +519,9 @@ public function test_has_protected_data_changes(): void { // Change a protected field (cm). $data[0]['rows'][0]['cells'][2]['value'] = 20.0; $this->assertTrue($pm->has_protected_data_changes($data)); + // Remove an entire row, which should be considered a protected change. + $data[0]['rows'][0]['deleted'] = true; + $this->assertTrue($pm->has_protected_data_changes($data)); } /** From 054db0bde93df481de4b5b9229cf0be87b1d9b21 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Fri, 6 Feb 2026 19:53:26 +0100 Subject: [PATCH 07/15] Fix #768 Validate approval request email --- classes/event/rfc_accepted.php | 70 +++++++ classes/local/api/notifications.php | 106 ++++++++++- classes/local/observers/rfc_observer.php | 16 +- classes/local/rfc_manager.php | 12 ++ db/events.php | 4 + db/upgrade.php | 12 ++ lang/en/customfield_sprogramme.php | 46 +++-- lang/fr/customfield_sprogramme.php | 28 ++- settings.php | 18 ++ tests/local/api/notifications_test.php | 97 +++++++++- tests/local/observers/rfc_observer_test.php | 199 ++++++++++++++++++++ version.php | 4 +- 12 files changed, 575 insertions(+), 37 deletions(-) create mode 100644 classes/event/rfc_accepted.php create mode 100644 tests/local/observers/rfc_observer_test.php diff --git a/classes/event/rfc_accepted.php b/classes/event/rfc_accepted.php new file mode 100644 index 0000000..6b8e50f --- /dev/null +++ b/classes/event/rfc_accepted.php @@ -0,0 +1,70 @@ +. + +namespace customfield_sprogramme\event; + +use customfield_sprogramme\local\persistent\sprogramme_rfc; + +/** + * An rfc has been accepted. + * + * @package customfield_sprogramme + * @copyright 2023 - CALL Learning - Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class rfc_accepted extends \core\event\base { + /** + * Returns the event name. + * @return string + */ + public static function get_name() { + return get_string('event_rfc_accepted', 'customfield_sprogramme'); + } + + /** + * Returns the object id mapping. + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => sprogramme_rfc::TABLE, 'restore' => 'rfc']; + } + + /** + * Returns the event description. + * @return string + */ + public function get_description() { + return "The user with id {$this->userid} has accepted an rfc with id {$this->objectid}."; + } + + /** + * Returns the event URL. + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/customfield/field/sprogramme/rfc.php', ['id' => $this->objectid]); + } + + /** + * Initializes other event data. + * @return void + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = sprogramme_rfc::TABLE; + } +} diff --git a/classes/local/api/notifications.php b/classes/local/api/notifications.php index 6073f7d..849bbb3 100644 --- a/classes/local/api/notifications.php +++ b/classes/local/api/notifications.php @@ -33,16 +33,16 @@ class notifications { * Set the notification for the given planning. * * @param string $type The type of notification to send. - * @param int $userid The Notification unique ID. + * @param int $userid The Notification user ID. * @param int $datafieldid The Data field ID. * @param array $context The email template context (planning, students, etc.). */ public static function add_notification(string $type, int $userid, int $datafieldid, array $context = []) { - $context = self::add_global_context($context, $datafieldid); + $context = self::add_global_context($context, $datafieldid, $userid); // Get the default language for the emails from the Course settings. $subject = self::local_get_string('email:' . $type . ':subject', (object) $context); $body = self::get_email_body($type, $context); - $recipients = self::get_recipients(); + $recipients = self::get_recipients($type, $datafieldid, $context); foreach ($recipients as $recipient) { try { @@ -70,14 +70,24 @@ public static function add_notification(string $type, int $userid, int $datafiel * * @param array $context * @param int $datafieldid + * @param int $userid * @return array */ - private static function add_global_context(array $context, int $datafieldid): array { + private static function add_global_context(array $context, int $datafieldid, int $userid): array { $courseid = utils::get_instanceid_from_datafieldid($datafieldid); $course = get_course($courseid); $programmelink = new moodle_url('/local/envasyllabus/syllabuspage.php', ['id' => $courseid]); $context['programmelink'] = $programmelink->out(); $context['coursename'] = $course->shortname . " - " . $course->fullname; + $context['department'] = self::get_department_for_course($courseid); + $context['responsibles'] = implode(', ', array_map(function ($user) { + return fullname($user); + }, self::get_responsible_for_course($courseid))); + if (isset($context['usercreated'])) { + $user = core_user::get_user($context['usercreated']); + $context['requester'] = fullname($user); + } + $context['userid'] = $userid; return $context; } @@ -118,17 +128,45 @@ private static function get_email_body(string $notification, array $context): st /** * Get the recipients of notifications. * + * @param string $type The type of notification to send. + * @param int $datafieldid + * @param array $context * @return array */ - private static function get_recipients(): array { - $recipients = get_config('customfield_sprogramme', 'approvalemail'); - if (empty(trim($recipients))) { + private static function get_recipients(string $type, int $datafieldid, array $context): array { + $courseid = utils::get_instanceid_from_datafieldid($datafieldid); + $approveremails = get_config('customfield_sprogramme', 'approvalemail'); + if (empty(trim($approveremails))) { return [core_user::get_support_user()->email]; } // Separate the recipients by comma. - $recipients = explode(',', $recipients); - $recipients = array_map('trim', $recipients); - return $recipients; + $approveremails = explode(',', $approveremails); + $approveremails = array_map('trim', $approveremails); + + // Now get the responsibles for courses. + $responsibles = self::get_responsible_for_course($courseid); + $responsibleemails = []; + foreach ($responsibles as $responsible) { + $responsibleemails[] = $responsible->email; + } + $emails = []; + switch ($type) { + case 'rfc_submitted': + // For RFC submitted, we want to send the email to the approvers and the responsibles. + $emails = array_merge($approveremails, $responsibleemails); + break; + case 'rfc_accepted': + // For RFC accepted, we also want to send the email to the user who submitted the RFC. + $emails = array_merge($approveremails, $responsibleemails); + if (isset($context['usercreated'])) { + $user = core_user::get_user($context['usercreated']); + if ($user->email && !in_array($user->email, $emails)) { + $emails[] = $user->email; + } + } + break; + } + return array_unique($emails); } /** @@ -152,4 +190,52 @@ private static function process_placeholders($string, $a): string { } return $string; } + + /** + * Get users matching the responsible role. + * + * Note this is a duplicate of the get_responsible_for_course function in local_envasyllabus + * but we want to avoid a dependency on the local_envasyllabus plugin in the notifications class. + * + * @param int $courseid + * @return array + */ + protected static function get_responsible_for_course(int $courseid): array { + global $DB; + $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); + $teacherroles = $DB->get_fieldset( + 'role', + 'id', + ['shortname' => $responsiblerolename] + ); + if (!empty($teacherroles)) { + $userfieldsapi = \core_user\fields::for_userpic()->including('username', 'deleted'); + $userfields = 'ra.id, u.id, u.username' . $userfieldsapi->get_sql('u')->selects; + return get_role_users($teacherroles, \context_course::instance($courseid), true, $userfields); + } else { + return []; + } + } + + /** + * Get users matching the responsible role. + * + * We assume here that the department is stored in a custom field with + * shortname 'uc_departement' at the course level, but this can be adapted if needed. + * + * @param int $courseid + * @return string + */ + protected static function get_department_for_course(int $courseid): string { + $handler = \core_customfield\handler::get_handler('core_course', 'course'); + $cfdata = $handler->get_instance_data($courseid, true); + $customfieldname = get_config('customfield_sprogramme', 'departmentcustomfieldname') ?: 'uc_departement'; + foreach ($cfdata as $cfdatacontroller) { + $shortname = $cfdatacontroller->get_field()->get('shortname'); + if ($shortname === $customfieldname) { + return $cfdatacontroller->export_value() ?? ''; + } + } + return ''; + } } diff --git a/classes/local/observers/rfc_observer.php b/classes/local/observers/rfc_observer.php index 156b7c8..48d68be 100644 --- a/classes/local/observers/rfc_observer.php +++ b/classes/local/observers/rfc_observer.php @@ -16,6 +16,7 @@ namespace customfield_sprogramme\local\observers; +use customfield_sprogramme\event\rfc_accepted; use customfield_sprogramme\event\rfc_created; use customfield_sprogramme\event\rfc_submitted; use customfield_sprogramme\local\api\notifications; @@ -45,6 +46,19 @@ public static function rfc_submitted(rfc_submitted $event): void { $eventdata = $event->get_data(); $userid = $eventdata['userid']; $datafieldid = $eventdata['other']['datafieldid']; - notifications::add_notification('rfc', $userid, $datafieldid); + notifications::add_notification('rfc_submitted', $userid, $datafieldid); + } + + /** + * An rfc has been created. + * + * @param rfc_accepted $event + */ + public static function rfc_accepted(rfc_accepted $event): void { + $eventdata = $event->get_data(); + $userid = $eventdata['userid']; + $usercreated = $eventdata['other']['usercreated']; + $datafieldid = $eventdata['other']['datafieldid']; + notifications::add_notification('rfc_accepted', $userid, $datafieldid, ['usercreated' => $usercreated]); } } diff --git a/classes/local/rfc_manager.php b/classes/local/rfc_manager.php index 0f566b7..5b83140 100644 --- a/classes/local/rfc_manager.php +++ b/classes/local/rfc_manager.php @@ -98,6 +98,18 @@ public function accept(int $usercreated): bool { $rfc->set('type', sprogramme_rfc::RFC_ACCEPTED); $rfc->set('adminid', $USER->id); $rfc->save(); + $event = \customfield_sprogramme\event\rfc_accepted::create( + [ + 'context' => $this->context, + 'objectid' => $rfc->get('id'), + 'other' => [ + 'datafieldid' => $this->datafieldid, + 'rfcid' => $rfc->get('id'), + 'usercreated' => $usercreated, + ], + ] + ); + $event->trigger(); $programme = new programme_manager($this->datafieldid); $result = $programme->set_data($data); if (!$result) { diff --git a/db/events.php b/db/events.php index d7f7de4..00760fb 100644 --- a/db/events.php +++ b/db/events.php @@ -32,4 +32,8 @@ 'eventname' => '\customfield_sprogramme\event\rfc_submitted', 'callback' => \customfield_sprogramme\local\observers\rfc_observer::class . '::rfc_submitted', ], + [ + 'eventname' => '\customfield_sprogramme\event\rfc_accepted', + 'callback' => \customfield_sprogramme\local\observers\rfc_observer::class . '::rfc_accepted', + ], ]; diff --git a/db/upgrade.php b/db/upgrade.php index 734f99c..2eb941e 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -119,6 +119,18 @@ function xmldb_customfield_sprogramme_upgrade($oldversion) { // Sprogramme savepoint reached. upgrade_plugin_savepoint(true, 2025101700, 'customfield', 'sprogramme'); } + if ($oldversion < 2026020600) { + global $DB; + // We now have two types of notifications. + $DB->set_field( + 'customfield_sprogramme_notification', + 'notification', + 'rfc_submitted', + ['notification' => 'rfc'] + ); + // Sprogramme savepoint reached. + upgrade_plugin_savepoint(true, 2026020600, 'customfield', 'sprogramme'); + } return true; } diff --git a/lang/en/customfield_sprogramme.php b/lang/en/customfield_sprogramme.php index 1f1bda7..7e890d3 100644 --- a/lang/en/customfield_sprogramme.php +++ b/lang/en/customfield_sprogramme.php @@ -52,6 +52,8 @@ $string['csvdelimiter'] = 'CSV delimiter'; $string['csvfile'] = 'CSV file'; $string['dd_rse_help'] = 'Sustainable Development / Social and Environmental Responsibility: This checkbox indicates whether the session fully or partially addresses concepts related to the SD / SER domain.'; +$string['departmentcustomfieldname'] = 'Department custom field'; +$string['departmentcustomfieldname_desc'] = 'The department custom field used for notifications.'; $string['discipline:name'] = 'Name'; $string['discipline:parent'] = 'Parent'; $string['discipline:sortorder'] = 'Sort order'; @@ -63,7 +65,23 @@ $string['disciplines_help'] = 'This field indicates the AEEEV disciplines (1 to 3 maximum) that are concerned by the session/exercise, and their respective percentages within the session (for example, 10% for "2. Immunology", 60% for "2. Parasitology", and 30% for "4.FPA Preventive medicine". The sum must equal 100%.'; $string['edit'] = 'Edit'; $string['editprogramme'] = 'Edit'; -$string['email:rfc'] = <<<'EOF' +$string['email:rfc_accepted'] = <<<'EOF' +

Hello,

+

The programme change request concerning the following course unit:

+

{$a->coursename}

+
    +
  • UC Responsible(s): [{$a->responsibles}]
  • +
  • Requester: {$a->requester}
  • +
  • Department: {$a->department}
  • +
+

has been validated by the relevant department head and the DEVE.

+

This message is addressed to the three department heads, the quality manager, the director of training, and the Moodle administrator, to ensure shared information about changes to the educational framework.

+

To view the history of validated changes for this UC, please follow the link below and click on the "History" button of the programme:

+{$a->programmelink}

+

Best regards

+EOF; +$string['email:rfc_accepted:subject'] = '[Syllabus] Programme change validated for UC: {$a->coursename}'; +$string['email:rfc_submitted'] = <<<'EOF'

Hello,

A change request has been submitted for the programme of the following course: {$a->coursename}.

@@ -73,7 +91,7 @@ {$a->programmelink}

Best regards

EOF; -$string['email:rfc:subject'] = '[Syllabus] Request for programme change for: {$a->coursename}'; +$string['email:rfc_submitted:subject'] = '[Syllabus] Request for programme change for: {$a->coursename}'; $string['emailsenabled'] = 'Activate email notifications'; $string['emailsenabled_desc'] = 'If enabled, email notifications will be sent when a change request is submitted.'; $string['encoding'] = 'Encoding'; @@ -89,7 +107,7 @@ $string['invaliddata'] = 'Invalid data: {$a}'; $string['invalidpagetype'] = 'Invalid page type'; $string['invalidvalue'] = 'Invalid value for column {$a->column}: {$a->value}'; -$string['maxdisciplines'] = 'You can not any more, max allowed reached'; +$string['maxdisciplines'] = 'You cannot add more'; $string['maxpercentage'] = 'Max allowed {$a} The sum of the percentages must be 100'; $string['module:name'] = 'Module Name'; $string['module:sortorder'] = 'Module'; @@ -105,26 +123,26 @@ $string['programme:aas'] = 'AAS'; $string['programme:cct_ept'] = 'CCT EPT'; $string['programme:cm'] = 'CM'; -$string['programme:consignes'] = 'Instructions to prepare for the session'; +$string['programme:consignes'] = 'Work instructions to prepare for the session'; $string['programme:datafieldid'] = 'Data field id'; $string['programme:dd_rse'] = 'DD / RSE'; $string['programme:enabled'] = '{$a} enabled'; -$string['programme:enabledbydefault'] = 'Programme Enabled By default'; +$string['programme:enabledbydefault'] = 'Programme enabled by default'; $string['programme:fmp'] = 'FMP'; -$string['programme:intitule_seance'] = 'Session title or exercise'; -$string['programme:perso_ap'] = 'Perso ap'; -$string['programme:perso_av'] = 'Perso av'; +$string['programme:intitule_seance'] = 'Session/exercise title'; +$string['programme:perso_ap'] = 'Personal work after'; +$string['programme:perso_av'] = 'Personal work before'; $string['programme:sequence'] = 'Sequence'; $string['programme:sortorder'] = 'Sort order'; -$string['programme:supports'] = 'Essential teaching materials'; +$string['programme:supports'] = 'Essential teaching supports'; $string['programme:tc'] = 'TC'; $string['programme:td'] = 'TD'; $string['programme:timecreated'] = 'Time created'; $string['programme:timemodified'] = 'Time modified'; $string['programme:tp'] = 'TP'; -$string['programme:tpa'] = 'TPA'; -$string['programme:type_ae'] = 'Type AE'; -$string['programme:uc'] = 'UC'; +$string['programme:tpa'] = 'TPa'; +$string['programme:type_ae'] = 'AE type'; +$string['programme:uc'] = 'Course unit'; $string['programme:usermodified'] = 'Modified by'; $string['reject'] = 'Reject'; $string['removerfc'] = 'Reset all changes'; @@ -132,8 +150,10 @@ $string['report:disciplines'] = 'Disciplines Report'; $string['report:programme'] = 'Programme'; $string['report:rfcs'] = 'Change requests'; -$string['report:rfctotals'] = 'Cahange requests totals'; +$string['report:rfctotals'] = 'Change requests totals'; $string['resetrfc'] = 'Hide suggested changes'; +$string['responsiblerolename'] = 'Responsible role (shortname)'; +$string['responsiblerolename_desc'] = 'Short name of the responsible role. Users with this role will be notified of programme change requests and can certify them.'; $string['rfc:accepted'] = 'Accepted'; $string['rfc:actions'] = 'Actions'; $string['rfc:changerequestby'] = 'Change request by {$a}'; diff --git a/lang/fr/customfield_sprogramme.php b/lang/fr/customfield_sprogramme.php index 40c230b..a440218 100644 --- a/lang/fr/customfield_sprogramme.php +++ b/lang/fr/customfield_sprogramme.php @@ -52,6 +52,8 @@ $string['csvdelimiter'] = 'Délimiteur CSV'; $string['csvfile'] = 'Fichier CSV'; $string['dd_rse_help'] = 'Développement Durable / Responsabilité Sociétale et Environnementale : Cette case indique si la séance traite intégralement ou en partie de notions en lien avec le domaine DD / RSE.'; +$string['departmentcustomfieldname'] = 'Department custom field'; +$string['departmentcustomfieldname_desc'] = 'The department custom field used for notifications.'; $string['discipline:name'] = 'Nom'; $string['discipline:parent'] = 'Parent'; $string['discipline:sortorder'] = 'Ordre de tri'; @@ -63,8 +65,27 @@ $string['disciplines_help'] = 'Cette case indique les disciplines AEEEV (de 1 à 3 maximum) qui sont concernées par la séance / l’exercice, et leurs % respectifs au sein de la séance (par exemple, 10% pour « 2. Immunology », 60% pour « 2. Parasitology », et 30% pour « 4.FPA Preventive medicine ». La somme doit faire 100%.'; $string['edit'] = 'Modifier'; $string['editprogramme'] = 'Modifier'; -$string['email:rfc'] = <<<'EOF' +$string['email:rfc_accepted'] = <<<'EOF' +

Bonjour,

+

La demande de modification de programme concernant l’unité d’enseignement suivante :

+

{$a->coursename}

+
    +
  • Responsable(s) de l’UC : [{$a->responsibles}] +
  • Demandeur : {$a->requester}
  • +
  • Département de rattachement : {$a->department}
  • +
+

+ +

a été validée par le chef de département concerné et la DEVE.

+

Ce message est adressé aux trois chefs de département, au responsable qualité, au directeur des formations ainsi qu’à l’administrateur Moodle, afin d’assurer une information partagée sur les évolutions de la maquette pédagogique.

+

Pour consulter l’historique des modifications validées pour cette UC, veuillez suivre le lien ci-dessous et cliquer sur le bouton "Historique" du programme :

+{$a->programmelink}

+

Bien cordialement

+EOF; + +$string['email:rfc_accepted:subject'] = '[Syllabus] Modification de programme validée pour l\'UC :{$a->coursename}'; +$string['email:rfc_submitted'] = <<<'EOF'

Bonjour,

Une demande de modification de programme a été soumise pour l'UC suivante :{$a->coursename}.

@@ -75,7 +96,8 @@ {$a->programmelink}

Bien cordialement

EOF; -$string['email:rfc:subject'] = '[Syllabus] Demande de modification de programme pour l\'UC :{$a->coursename}'; +$string['email:rfc_submitted:subject'] = '[Syllabus] Demande de modification de programme pour l\'UC :{$a->coursename}'; + $string['emailsenabled'] = 'Activer les notifications par e-mail'; $string['emailsenabled_desc'] = 'Si activé, les notifications par e-mail seront envoyées lors de la soumission de demandes de modification de programme.'; $string['encoding'] = 'Encodage'; @@ -136,6 +158,8 @@ $string['report:rfcs'] = 'Demande de changement'; $string['report:rfctotals'] = 'Totaux des demandes de changement'; $string['resetrfc'] = 'Masquer les modifications proposées'; +$string['responsiblerolename'] = 'Rôle responsable (shortname)'; +$string['responsiblerolename_desc'] = 'Nom court du rôle responsable. Les utilisateurs avec ce rôle seront notifiés des demandes de modification de programme et pourront les certifier.'; $string['rfc:accepted'] = 'Acceptée'; $string['rfc:actions'] = 'Actions'; $string['rfc:changerequestby'] = 'Demande de modification par {$a}'; diff --git a/settings.php b/settings.php index 607d2c9..f739b9c 100644 --- a/settings.php +++ b/settings.php @@ -53,5 +53,23 @@ ['class' => 'btn btn-primary mb-3'] ) )); + + $departementcustomfieldname = new admin_setting_configtext( + 'customfield_sprogramme/departmentcustomfieldname', + get_string('departmentcustomfieldname', 'customfield_sprogramme'), + get_string('departmentcustomfieldname_desc', 'customfield_sprogramme'), + 'uc_departement', + PARAM_ALPHANUMEXT + ); + $settings->add($departementcustomfieldname); + + $responsiblerolename = new admin_setting_configtext( + 'customfield_sprogramme/responsiblerolename', + get_string('responsiblerolename', 'customfield_sprogramme'), + get_string('responsiblerolename_desc', 'customfield_sprogramme'), + 'responsablecourse', + PARAM_ALPHANUM + ); + $settings->add($responsiblerolename); } } diff --git a/tests/local/api/notifications_test.php b/tests/local/api/notifications_test.php index 9869a02..4185eef 100644 --- a/tests/local/api/notifications_test.php +++ b/tests/local/api/notifications_test.php @@ -46,6 +46,13 @@ final class notifications_test extends \advanced_testcase { */ protected data_controller $cfdata; + /** + * Course data + * + * @var \stdClass $course ; + */ + protected \stdClass $course; + /** * Setup test environment */ @@ -68,6 +75,7 @@ public function setUp(): void { ); $course = $this->getDataGenerator()->create_course(); $this->cfdata = $cfgenerator->add_instance_data($cfield, $course->id, 1); + $this->course = $course; set_config('emailsenabled', true, 'customfield_sprogramme'); } @@ -84,11 +92,11 @@ public function test_add_notification(): void { snapshot: json_encode($this->sampleprogrammedata[0]), usercreated: $user1->id, ); - notifications::add_notification('rfc', $user1->id, $this->cfdata->get('id')); + notifications::add_notification('rfc_submitted', $user1->id, $this->cfdata->get('id')); $this->assertCount(1, notification::get_records([])); $notification = notification::get_records([])[0]; - $this->assertEquals('rfc', $notification->get('notification')); + $this->assertEquals('rfc_submitted', $notification->get('notification')); $this->assertEquals($this->cfdata->get('id'), $notification->get('datafieldid')); $this->assertEquals('admin@example.com', $notification->get('recipient')); // Default admin email. $this->assertEquals(notification::STATUS_PENDING, $notification->get('status')); @@ -116,18 +124,18 @@ public function test_add_notification_approvalemail(): void { snapshot: json_encode($this->sampleprogrammedata[0]), usercreated: $user1->id, ); - notifications::add_notification('rfc', $user1->id, $this->cfdata->get('id')); + notifications::add_notification('rfc_submitted', $user1->id, $this->cfdata->get('id')); $notifications = notification::get_records(); $this->assertCount(2, $notifications); $notification = $notifications[0]; - $this->assertEquals('rfc', $notification->get('notification')); + $this->assertEquals('rfc_submitted', $notification->get('notification')); $this->assertEquals($this->cfdata->get('id'), $notification->get('datafieldid')); $this->assertEquals('recipient@example.com', $notification->get('recipient')); $this->assertEquals(notification::STATUS_PENDING, $notification->get('status')); $notification = $notifications[1]; - $this->assertEquals('rfc', $notification->get('notification')); + $this->assertEquals('rfc_submitted', $notification->get('notification')); $this->assertEquals($this->cfdata->get('id'), $notification->get('datafieldid')); $this->assertEquals('recipient2@example.com', $notification->get('recipient')); $this->assertEquals(notification::STATUS_PENDING, $notification->get('status')); @@ -153,8 +161,8 @@ public function test_send_notifications(): void { snapshot: json_encode($this->sampleprogrammedata[0]), usercreated: $user2->id, ); - notifications::add_notification('rfc', $user1->id, $this->cfdata->get('id')); - notifications::add_notification('rfc', $user2->id, $this->cfdata->get('id')); + notifications::add_notification('rfc_submitted', $user1->id, $this->cfdata->get('id')); + notifications::add_notification('rfc_submitted', $user2->id, $this->cfdata->get('id')); $this->assertCount(2, notification::get_records(['status' => notification::STATUS_PENDING])); foreach (notification::get_records([]) as $notification) { $notification->send(); @@ -199,8 +207,8 @@ public function test_send_notifications_disabled(): void { snapshot: json_encode($this->sampleprogrammedata[0]), usercreated: $user2->id, ); - notifications::add_notification('rfc', $user1->id, $this->cfdata->get('id')); - notifications::add_notification('rfc', $user2->id, $this->cfdata->get('id')); + notifications::add_notification('rfc_submitted', $user1->id, $this->cfdata->get('id')); + notifications::add_notification('rfc_submitted', $user2->id, $this->cfdata->get('id')); $this->assertCount(2, notification::get_records(['status' => notification::STATUS_PENDING])); foreach (notification::get_records([]) as $notification) { $notification->send(); @@ -210,4 +218,75 @@ public function test_send_notifications_disabled(): void { $this->assertCount(0, notification::get_records(['status' => notification::STATUS_SEND])); $this->assertCount(2, notification::get_records(['status' => notification::STATUS_PENDING])); } + + /** + * Test adding global context + */ + public function test_add_global_context(): void { + $generator = $this->getDataGenerator(); + + $cfgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); + // If the local_envasyllabus plugin is not installed, the department field will not be presents, so we change the default + // value to a new field. + $cfielddept = $cfgenerator->create_field( + [ + 'categoryid' => + $this->cfdata->get_field()->get_category()->get('id'), + 'shortname' => 'newdept', + 'type' => 'text', + ] + ); + $cfgenerator->add_instance_data($cfielddept, $this->course->id, 'DSPB'); + set_config('departmentcustomfieldname', 'newdept', 'customfield_sprogramme'); + + $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); + $generator->create_role( + [ + 'shortname' => $responsiblerolename, + 'name' => 'Responsible', + 'archetype' => 'editingteacher', + ] + ); + $generator->create_and_enrol($this->course, $responsiblerolename, [ + 'username' => 'responsible1', + 'email' => 'responsible1@example.com', + 'firstname' => 'Responsible', + 'lastname' => 'One', + ]); + $generator->create_and_enrol($this->course, $responsiblerolename, [ + 'username' => 'responsible2', + 'email' => 'responsible2@example.com', + 'firstname' => 'Responsible', + 'lastname' => 'Two', + ]); + + $method = new \ReflectionMethod(notifications::class, 'add_global_context'); + $method->setAccessible(true); + $user = $generator->create_user([ + 'username' => 'user1', + 'email' => 'user1@example.com', + 'firstname' => 'User', + 'lastname' => '1', + ]); + $context = $method->invoke( + null, + ['usercreated' => $user->id,], + $this->cfdata->get('id'), + $user->id + ); + $this->assertArrayHasKey('programmelink', $context); + $this->assertArrayHasKey('coursename', $context); + $this->assertArrayHasKey('department', $context); + $this->assertArrayHasKey('responsibles', $context); + $this->assertArrayHasKey('requester', $context); + $this->assertStringContainsString( + '/local/envasyllabus/syllabuspage.php?id=' . $this->cfdata->get('instanceid'), + $context['programmelink'] + ); + $this->assertEquals('tc_1 - Test course 1', $context['coursename']); + $this->assertEquals('DSPB', $context['department']); + $this->assertStringContainsString('Responsible One', $context['responsibles']); + $this->assertStringContainsString('Responsible Two', $context['responsibles']); + $this->assertEquals('User 1', $context['requester']); + } } diff --git a/tests/local/observers/rfc_observer_test.php b/tests/local/observers/rfc_observer_test.php new file mode 100644 index 0000000..f66adf8 --- /dev/null +++ b/tests/local/observers/rfc_observer_test.php @@ -0,0 +1,199 @@ +. + +namespace customfield_sprogramme\local\observers; + +use core_customfield\data_controller; +use customfield_sprogramme\local\persistent\sprogramme_rfc; +use customfield_sprogramme\local\rfc_manager; +use customfield_sprogramme\task\notifications; +use customfield_sprogramme\test\testcase_helper_trait; + +/** + * + * + * @package customfield_sprogramme + * @copyright 2025 - CALL Learning - Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \customfield_sprogramme\local\observers\rfc_observer + */ +final class rfc_observer_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Custom field data + * + * @var data_controller $cfdata ; + */ + protected data_controller $cfdata; + + /** + * Course data + * + * @var \stdClass $course ; + */ + protected \stdClass $course; + + /** + * Setup test environment + */ + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $cfgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); + $cfcat = $cfgenerator->create_category(); + + $cfield = $cfgenerator->create_field( + ['categoryid' => $cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'sprogramme'] + ); + $this->course = $this->getDataGenerator()->create_course(); + $this->cfdata = $cfgenerator->add_instance_data($cfield, $this->course->id, 1); + $this->getDataGenerator()->create_and_enrol($this->course, 'editingteacher', [ + 'username' => 'teacher1', + 'email' => 'teacher1@example.com', + 'firstname' => 'Teacher', + 'lastname' => 'One', + ]); + set_config('emailsenabled', true, 'customfield_sprogramme'); + set_config('approvalemail', 'admin@example.com,otheruser@example.com', 'customfield_sprogramme'); + } + + /** + * Test that an email is sent when an RFC is submitted + */ + public function test_rfc_submitted_email_sent(): void { + global $DB; + $this->resetAfterTest(); + + $emailsink = $this->redirectEmails(); + $rfcmanager = new rfc_manager($this->cfdata->get('id')); + $teacher = $DB->get_record('user', ['username' => 'teacher1']); + $pgenerator = $this->getDataGenerator()->get_plugin_generator('customfield_sprogramme'); + $pgenerator->create_rfc( + $this->cfdata->get('id'), + usercreated: $teacher->id, + ); + $rfcmanager->submit($teacher->id); + // Execute the cron. + ob_start(); + \core\cron::setup_user(); + $cron = new notifications(); + $cron->execute(); + ob_end_clean(); + + $emails = $emailsink->get_messages(); + $this->assertCount(2, $emails); + $emailsto = array_map(fn($email) => $email->to, $emails); + $this->assertContains('admin@example.com', $emailsto); + $this->assertContains('otheruser@example.com', $emailsto); + $email = reset($emails); + $this->assertEquals('[Syllabus] Request for programme change for: tc_1 - Test course 1', $email->subject); + } + + /** + * Test that an email is sent when an RFC is accepted + */ + public function test_rfc_accepted_email_sent(): void { + global $DB; + $this->resetAfterTest(); + + $rfcmanager = new rfc_manager($this->cfdata->get('id')); + $teacher = $DB->get_record('user', ['username' => 'teacher1']); + $pgenerator = $this->getDataGenerator()->get_plugin_generator('customfield_sprogramme'); + $pgenerator->create_rfc( + $this->cfdata->get('id'), + type: sprogramme_rfc::RFC_SUBMITTED, + snapshot: json_encode([ + [ + 'moduleid' => -1, + 'modulesortorder' => 0, + 'modulename' => 'Test Module 1', + 'deleted' => false, + 'rows' => [], + ], + ]), + usercreated: $teacher->id, + ); + + // Prepare the data for the context. + $generator = $this->getDataGenerator(); + $cfgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); + // If the local_envasyllabus plugin is not installed, the department field will not be presents, so we change the default + // value to a new field. + $cfielddept = $cfgenerator->create_field( + [ + 'categoryid' => + $this->cfdata->get_field()->get_category()->get('id'), + 'shortname' => 'newdept', + 'type' => 'text', + ] + ); + $cfgenerator->add_instance_data($cfielddept, $this->course->id, 'DSPB'); + set_config('departmentcustomfieldname', 'newdept', 'customfield_sprogramme'); + + $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); + $generator->create_role( + [ + 'shortname' => $responsiblerolename, + 'name' => 'Responsible', + 'archetype' => 'editingteacher', + ] + ); + $generator->create_and_enrol($this->course, $responsiblerolename, [ + 'username' => 'responsible1', + 'email' => 'responsible1@example.com', + 'firstname' => 'Responsible', + 'lastname' => 'One', + ]); + $generator->create_and_enrol($this->course, $responsiblerolename, [ + 'username' => 'responsible2', + 'email' => 'responsible2@example.com', + 'firstname' => 'Responsible', + 'lastname' => 'Two', + ]); + + $emailsink = $this->redirectEmails(); + // Accept the RFC. + $this->setAdminUser(); + $rfcmanager->accept( + $teacher->id, + ); + // Execute the cron. + ob_start(); + \core\cron::setup_user(); + $cron = new notifications(); + $cron->execute(); + ob_end_clean(); + + $emails = $emailsink->get_messages(); + // No email should be sent on approval. + $this->assertCount(5, $emails); + $emailsto = array_map(fn($email) => $email->to, $emails); + $this->assertContains('admin@example.com', $emailsto); + $this->assertContains('otheruser@example.com', $emailsto); + $this->assertContains('teacher1@example.com', $emailsto); + $this->assertContains('responsible1@example.com', $emailsto); + $this->assertContains('responsible2@example.com', $emailsto); + $email = reset($emails); + $this->assertEquals( + '[Syllabus] Programme change validated for UC: tc_1 - Test course 1', + $email->subject + ); + $this->assertStringContainsString('UC Responsible(s): [Responsible One, Responsible Two]', $email->body); + $this->assertStringContainsString('Requester: Teacher One', $email->body); + $this->assertStringContainsString('Department: DSPB', $email->body); + } +} diff --git a/version.php b/version.php index 03364c2..911ac92 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'customfield_sprogramme'; -$plugin->release = '2.2.1'; -$plugin->version = 2026012700; +$plugin->release = '2.3.0'; +$plugin->version = 2026020600; $plugin->requires = 2022112800; $plugin->maturity = MATURITY_BETA; From b9ce5aa52899ed96df7b31a8fabf9f367acdaf28 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Sun, 8 Feb 2026 10:24:58 +0100 Subject: [PATCH 08/15] Fix 802 remove approval on RFC pages --- classes/external/get_programme_history.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/classes/external/get_programme_history.php b/classes/external/get_programme_history.php index df53480..bbc18a2 100644 --- a/classes/external/get_programme_history.php +++ b/classes/external/get_programme_history.php @@ -67,20 +67,17 @@ public static function execute($rfcid, $datafieldid): array { self::validate_context(utils::get_context_from_datafieldid($datafieldid)); // Get the programme history. $programmemanager = new programme_manager($datafieldid); - $rfcmanager = new rfc_manager($datafieldid); $history = $programmemanager->get_history($rfcid); $modules = $history['modules']; if (empty($modules)) { throw new \invalid_parameter_exception('No programme history found for this course.'); } - $rfc = $rfcmanager->get_data(); $columns = $programmemanager->get_column_structure(); $columnstotals = $programmemanager->get_column_totals($modules, $columns); return [ 'modules' => $modules, 'columns' => $columnstotals, - 'rfc' => $rfc, ]; } @@ -165,19 +162,6 @@ public static function execute_returns(): external_single_structure { 'group' => new external_value(PARAM_TEXT, 'Group', VALUE_OPTIONAL), ]) ), - 'rfc' => new external_single_structure([ - 'timemodified' => new external_value(PARAM_INT, 'Time modified', VALUE_OPTIONAL), - 'issubmitted' => new external_value(PARAM_BOOL, 'Is submitted', VALUE_OPTIONAL), - 'canaccept' => new external_value(PARAM_BOOL, 'Can accept', VALUE_OPTIONAL), - 'canreject' => new external_value(PARAM_BOOL, 'Can reject', VALUE_OPTIONAL), - 'canremove' => new external_value(PARAM_BOOL, 'Can remove', VALUE_OPTIONAL), - 'cansubmit' => new external_value(PARAM_BOOL, 'Can submit', VALUE_OPTIONAL), - 'cancancel' => new external_value(PARAM_BOOL, 'Can cancel', VALUE_OPTIONAL), - 'userinfo' => new external_single_structure([ - 'id' => new external_value(PARAM_INT, 'UserId', VALUE_REQUIRED), - 'fullname' => new external_value(PARAM_TEXT, 'New value', VALUE_OPTIONAL), - ], 'User who created the RFC', VALUE_OPTIONAL), - ], 'RFC data', VALUE_OPTIONAL), ]); } } From 4419fbe3acb0fe5ec3613b03142f5b1b89684bd6 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Sun, 8 Feb 2026 21:21:15 +0100 Subject: [PATCH 09/15] Fix #761 acceptation by responsible (visa) --- amd/build/local/components/table.min.js | 2 +- amd/build/local/components/table.min.js.map | 2 +- amd/build/local/repository.min.js | 2 +- amd/build/local/repository.min.js.map | 2 +- amd/build/manager.min.js | 2 +- amd/build/manager.min.js.map | 2 +- amd/src/local/components/table.js | 1 + amd/src/local/repository.js | 34 +++++ amd/src/manager.js | 37 +++++ classes/external/accept_visa.php | 81 ++++++++++ classes/external/get_data.php | 28 ++++ classes/external/reject_visa.php | 81 ++++++++++ classes/local/api/notifications.php | 30 +--- classes/local/persistent/sprogramme_visa.php | 87 +++++++++++ classes/local/visa_manager.php | 147 +++++++++++++++++++ classes/utils.php | 51 +++++++ db/install.xml | 23 ++- db/services.php | 16 ++ db/upgrade.php | 28 ++++ lang/en/customfield_sprogramme.php | 4 + lang/fr/customfield_sprogramme.php | 4 + templates/formfield.mustache | 2 + templates/table/visainfo.mustache | 57 +++++++ version.php | 2 +- 24 files changed, 689 insertions(+), 36 deletions(-) create mode 100644 classes/external/accept_visa.php create mode 100644 classes/external/reject_visa.php create mode 100644 classes/local/persistent/sprogramme_visa.php create mode 100644 classes/local/visa_manager.php create mode 100644 templates/table/visainfo.mustache diff --git a/amd/build/local/components/table.min.js b/amd/build/local/components/table.min.js index b48eaa3..a3757d2 100644 --- a/amd/build/local/components/table.min.js +++ b/amd/build/local/components/table.min.js @@ -5,6 +5,6 @@ define("customfield_sprogramme/local/components/table",["exports","customfield_s * @module customfield_sprogramme/local/components/table * @copyright 2024 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_templates=_interopRequireDefault(_templates);const stateTemplate=function(type){let templatename=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",root=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"app";const app=document.querySelector('[data-region="'.concat(root,'"]'));if(!app)return;const region=app.querySelector('[data-region="'.concat(type,'"]'));if(!region)return;""==templatename&&(templatename=type);const template="customfield_sprogramme/table/".concat(templatename),tableColumns=async context=>{if(void 0===context[type])return;context[type]=_state.default.getValue(type),context=JSON.parse(JSON.stringify(context));const{html:html,js:js}=await _templates.default.renderForPromise(template,context);_templates.default.replaceNodeContents(region,html,js)};_state.default.subscribe(type,tableColumns)};var _default=()=>{stateTemplate("columns","columnsheader"),stateTemplate("modules"),stateTemplate("modulesstatic","","static"),stateTemplate("rfc"),stateTemplate("editbuttons","","modalheader")};return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_templates=_interopRequireDefault(_templates);const stateTemplate=function(type){let templatename=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",root=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"app";const app=document.querySelector('[data-region="'.concat(root,'"]'));if(!app)return;const region=app.querySelector('[data-region="'.concat(type,'"]'));if(!region)return;""==templatename&&(templatename=type);const template="customfield_sprogramme/table/".concat(templatename),tableColumns=async context=>{if(void 0===context[type])return;context[type]=_state.default.getValue(type),context=JSON.parse(JSON.stringify(context));const{html:html,js:js}=await _templates.default.renderForPromise(template,context);_templates.default.replaceNodeContents(region,html,js)};_state.default.subscribe(type,tableColumns)};var _default=()=>{stateTemplate("columns","columnsheader"),stateTemplate("modules"),stateTemplate("modulesstatic","","static"),stateTemplate("rfc"),stateTemplate("visainfo"),stateTemplate("editbuttons","","modalheader")};return _exports.default=_default,_exports.default})); //# sourceMappingURL=table.min.js.map \ No newline at end of file diff --git a/amd/build/local/components/table.min.js.map b/amd/build/local/components/table.min.js.map index 55cab07..40e29b1 100644 --- a/amd/build/local/components/table.min.js.map +++ b/amd/build/local/components/table.min.js.map @@ -1 +1 @@ -{"version":3,"file":"table.min.js","sources":["../../../src/local/components/table.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 * TODO describe module table\n *\n * @module customfield_sprogramme/local/components/table\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Templates from 'core/templates';\n\n/**\n * Define the user navigation.\n * @param {String} type The type.\n * @param {String} templatename The template name.\n * @param {String} root region root.\n */\nconst stateTemplate = (type, templatename = '', root = 'app') => {\n const app = document.querySelector(`[data-region=\"${root}\"]`);\n if (!app) {\n return;\n }\n const region = app.querySelector(`[data-region=\"${type}\"]`);\n if (!region) {\n return;\n }\n if (templatename == '') {\n templatename = type;\n }\n const template = `customfield_sprogramme/table/${templatename}`;\n const tableColumns = async(context) => {\n if (context[type] === undefined) {\n return;\n }\n context[type] = State.getValue(type);\n // Clone the context to avoid issues with the same context in multiple templates.\n context = JSON.parse(JSON.stringify(context));\n const {html, js} = await Templates.renderForPromise(template, context);\n Templates.replaceNodeContents(region, html, js);\n };\n State.subscribe(type, tableColumns);\n};\n\nconst componentInit = () => {\n stateTemplate('columns', 'columnsheader');\n stateTemplate('modules');\n stateTemplate('modulesstatic', '', 'static');\n stateTemplate('rfc');\n stateTemplate('editbuttons', '', 'modalheader');\n};\n\nexport default componentInit;\n"],"names":["stateTemplate","type","templatename","root","app","document","querySelector","region","template","tableColumns","async","undefined","context","State","getValue","JSON","parse","stringify","html","js","Templates","renderForPromise","replaceNodeContents","subscribe"],"mappings":";;;;;;;+KAgCMA,cAAgB,SAACC,UAAMC,oEAAe,GAAIC,4DAAO,YAC7CC,IAAMC,SAASC,sCAA+BH,gBAC/CC,iBAGCG,OAASH,IAAIE,sCAA+BL,gBAC7CM,cAGe,IAAhBL,eACAA,aAAeD,YAEbO,gDAA2CN,cAC3CO,aAAeC,MAAAA,kBACKC,IAAlBC,QAAQX,aAGZW,QAAQX,MAAQY,eAAMC,SAASb,MAE/BW,QAAUG,KAAKC,MAAMD,KAAKE,UAAUL,gBAC9BM,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiBb,SAAUI,4BACpDU,oBAAoBf,OAAQW,KAAMC,oBAE1CI,UAAUtB,KAAMQ,4BAGJ,KAClBT,cAAc,UAAW,iBACzBA,cAAc,WACdA,cAAc,gBAAiB,GAAI,UACnCA,cAAc,OACdA,cAAc,cAAe,GAAI"} \ No newline at end of file +{"version":3,"file":"table.min.js","sources":["../../../src/local/components/table.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 * TODO describe module table\n *\n * @module customfield_sprogramme/local/components/table\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Templates from 'core/templates';\n\n/**\n * Define the user navigation.\n * @param {String} type The type.\n * @param {String} templatename The template name.\n * @param {String} root region root.\n */\nconst stateTemplate = (type, templatename = '', root = 'app') => {\n const app = document.querySelector(`[data-region=\"${root}\"]`);\n if (!app) {\n return;\n }\n const region = app.querySelector(`[data-region=\"${type}\"]`);\n if (!region) {\n return;\n }\n if (templatename == '') {\n templatename = type;\n }\n const template = `customfield_sprogramme/table/${templatename}`;\n const tableColumns = async(context) => {\n if (context[type] === undefined) {\n return;\n }\n context[type] = State.getValue(type);\n // Clone the context to avoid issues with the same context in multiple templates.\n context = JSON.parse(JSON.stringify(context));\n const {html, js} = await Templates.renderForPromise(template, context);\n Templates.replaceNodeContents(region, html, js);\n };\n State.subscribe(type, tableColumns);\n};\n\nconst componentInit = () => {\n stateTemplate('columns', 'columnsheader');\n stateTemplate('modules');\n stateTemplate('modulesstatic', '', 'static');\n stateTemplate('rfc');\n stateTemplate('visainfo');\n stateTemplate('editbuttons', '', 'modalheader');\n};\n\nexport default componentInit;\n"],"names":["stateTemplate","type","templatename","root","app","document","querySelector","region","template","tableColumns","async","undefined","context","State","getValue","JSON","parse","stringify","html","js","Templates","renderForPromise","replaceNodeContents","subscribe"],"mappings":";;;;;;;+KAgCMA,cAAgB,SAACC,UAAMC,oEAAe,GAAIC,4DAAO,YAC7CC,IAAMC,SAASC,sCAA+BH,gBAC/CC,iBAGCG,OAASH,IAAIE,sCAA+BL,gBAC7CM,cAGe,IAAhBL,eACAA,aAAeD,YAEbO,gDAA2CN,cAC3CO,aAAeC,MAAAA,kBACKC,IAAlBC,QAAQX,aAGZW,QAAQX,MAAQY,eAAMC,SAASb,MAE/BW,QAAUG,KAAKC,MAAMD,KAAKE,UAAUL,gBAC9BM,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiBb,SAAUI,4BACpDU,oBAAoBf,OAAQW,KAAMC,oBAE1CI,UAAUtB,KAAMQ,4BAGJ,KAClBT,cAAc,UAAW,iBACzBA,cAAc,WACdA,cAAc,gBAAiB,GAAI,UACnCA,cAAc,OACdA,cAAc,YACdA,cAAc,cAAe,GAAI"} \ No newline at end of file diff --git a/amd/build/local/repository.min.js b/amd/build/local/repository.min.js index 0c97aac..80d662e 100644 --- a/amd/build/local/repository.min.js +++ b/amd/build/local/repository.min.js @@ -5,6 +5,6 @@ define("customfield_sprogramme/local/repository",["exports","core/ajax","core/no * @module customfield_sprogramme/local/repository * @copyright 2024 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);var _default=new class{getColumns(args){const request={methodname:"customfield_sprogramme_get_columns",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getData(args){const request={methodname:"customfield_sprogramme_get_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}setData(args){const request={methodname:"customfield_sprogramme_set_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}csvData(args){const request={methodname:"customfield_sprogramme_csv_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}acceptRfc(args){const request={methodname:"customfield_sprogramme_accept_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}rejectRfc(args){const request={methodname:"customfield_sprogramme_reject_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}submitRfc(args){const request={methodname:"customfield_sprogramme_submit_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}cancelRfc(args){const request={methodname:"customfield_sprogramme_cancel_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}removeRfc(args){const request={methodname:"customfield_sprogramme_remove_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getProgrammeHistory(args){const request={methodname:"customfield_sprogramme_get_programme_history",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getTags(){return _ajax.default.call([{methodname:"customfield_sprogramme_get_tags",args:{}}])[0].fail(_notification.default.exception)}};return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);var _default=new class{getColumns(args){const request={methodname:"customfield_sprogramme_get_columns",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getData(args){const request={methodname:"customfield_sprogramme_get_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}setData(args){const request={methodname:"customfield_sprogramme_set_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}csvData(args){const request={methodname:"customfield_sprogramme_csv_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}acceptRfc(args){const request={methodname:"customfield_sprogramme_accept_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}rejectRfc(args){const request={methodname:"customfield_sprogramme_reject_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}submitRfc(args){const request={methodname:"customfield_sprogramme_submit_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}cancelRfc(args){const request={methodname:"customfield_sprogramme_cancel_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}removeRfc(args){const request={methodname:"customfield_sprogramme_remove_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getProgrammeHistory(args){const request={methodname:"customfield_sprogramme_get_programme_history",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getTags(){return _ajax.default.call([{methodname:"customfield_sprogramme_get_tags",args:{}}])[0].fail(_notification.default.exception)}async acceptVisa(args){const request={methodname:"customfield_sprogramme_reject_visa",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}async rejectVisa(args){const request={methodname:"customfield_sprogramme_accept_visa",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/amd/build/local/repository.min.js.map b/amd/build/local/repository.min.js.map index cc3b4ec..44775aa 100644 --- a/amd/build/local/repository.min.js.map +++ b/amd/build/local/repository.min.js.map @@ -1 +1 @@ -{"version":3,"file":"repository.min.js","sources":["../../src/local/repository.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 * Gateway to the webservices.\n *\n * @module customfield_sprogramme/local/repository\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n\n/**\n * Competvet repository class.\n */\nclass Repository {\n\n /**\n * Get JSON data\n * @param {Object} args The data to get.\n * @return {Promise} The promise.\n */\n getColumns(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_columns',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getData(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Set the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n setData(args) {\n const request = {\n methodname: 'customfield_sprogramme_set_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the CSV data for download.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n csvData(args) {\n const request = {\n methodname: 'customfield_sprogramme_csv_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Accept the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n acceptRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_accept_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Reject the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n rejectRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_reject_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Submit the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n submitRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_submit_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Cancel the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n cancelRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_cancel_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Remove the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n removeRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_remove_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the programme history.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getProgrammeHistory(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_programme_history',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the tags.\n * @return {Promise} The promise.\n */\n getTags() {\n const request = {\n methodname: 'customfield_sprogramme_get_tags',\n args: {}\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n}\n\nconst RepositoryInstance = new Repository();\n\nexport default RepositoryInstance;\n"],"names":["getColumns","args","request","methodname","Ajax","call","fail","Notification","exception","getData","setData","csvData","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","getProgrammeHistory","getTags"],"mappings":";;;;;;;0LAwN2B,UAnLvBA,WAAWC,YACDC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BC,QAAQR,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BE,QAAQT,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BG,QAAQV,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BI,UAAUX,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BK,UAAUZ,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BM,UAAUb,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BO,UAAUd,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BQ,UAAUf,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BS,oBAAoBhB,YACVC,QAAU,CACZC,WAAY,+CACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BU,iBAMkBd,cAAKC,KAAK,CALR,CACZF,WAAY,kCACZF,KAAM,MAGyB,GAC9BK,KAAKC,sBAAaC"} \ No newline at end of file +{"version":3,"file":"repository.min.js","sources":["../../src/local/repository.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 * Gateway to the webservices.\n *\n * @module customfield_sprogramme/local/repository\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n\n/**\n * Competvet repository class.\n */\nclass Repository {\n\n /**\n * Get JSON data\n * @param {Object} args The data to get.\n * @return {Promise} The promise.\n */\n getColumns(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_columns',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getData(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Set the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n setData(args) {\n const request = {\n methodname: 'customfield_sprogramme_set_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the CSV data for download.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n csvData(args) {\n const request = {\n methodname: 'customfield_sprogramme_csv_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Accept the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n acceptRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_accept_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Reject the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n rejectRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_reject_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Submit the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n submitRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_submit_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Cancel the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n cancelRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_cancel_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Remove the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n removeRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_remove_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the programme history.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getProgrammeHistory(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_programme_history',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the tags.\n * @return {Promise} The promise.\n */\n getTags() {\n const request = {\n methodname: 'customfield_sprogramme_get_tags',\n args: {}\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Accept the rfc (visa).\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n async acceptVisa(args) {\n const request = {\n methodname: 'customfield_sprogramme_reject_visa',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Reject the rfc (visa).\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n async rejectVisa(args) {\n const request = {\n methodname: 'customfield_sprogramme_accept_visa',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n}\n\nconst RepositoryInstance = new Repository();\n\nexport default RepositoryInstance;\n"],"names":["getColumns","args","request","methodname","Ajax","call","fail","Notification","exception","getData","setData","csvData","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","getProgrammeHistory","getTags"],"mappings":";;;;;;;0LA0P2B,UArNvBA,WAAWC,YACDC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BC,QAAQR,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BE,QAAQT,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BG,QAAQV,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BI,UAAUX,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BK,UAAUZ,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BM,UAAUb,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BO,UAAUd,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BQ,UAAUf,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BS,oBAAoBhB,YACVC,QAAU,CACZC,WAAY,+CACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BU,iBAMkBd,cAAKC,KAAK,CALR,CACZF,WAAY,kCACZF,KAAM,MAGyB,GAC9BK,KAAKC,sBAAaC,4BAUVP,YACPC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,4BAUVP,YACPC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC"} \ No newline at end of file diff --git a/amd/build/manager.min.js b/amd/build/manager.min.js index 06c6764..5f5360a 100644 --- a/amd/build/manager.min.js +++ b/amd/build/manager.min.js @@ -1,3 +1,3 @@ -define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+(parseFloat(cell.oldvalue)||0):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); +define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),response.visainfo&&_state.default.setValue("visainfo",response.visainfo),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,acceptvisa:this.acceptVisa,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,rejectvisa:this.rejectVisa,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+(parseFloat(cell.oldvalue)||0):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async rejectVisa(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectVisa"),rfcId=btn.dataset.rfcId;await _repository.default.rejectVisa({rfcid:rfcId,comment:""})&&await this.getTableData(),pending.resolve()}async acceptVisa(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectVisa"),rfcId=btn.dataset.rfcId;await _repository.default.acceptVisa({rfcid:rfcId,comment:""})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=manager.min.js.map \ No newline at end of file diff --git a/amd/build/manager.min.js.map b/amd/build/manager.min.js.map index be03ca3..b8372d5 100644 --- a/amd/build/manager.min.js.map +++ b/amd/build/manager.min.js.map @@ -1 +1 @@ -{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Directly remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n if (response.visainfo) {\n State.setValue('visainfo', response.visainfo);\n }\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'acceptvisa': this.acceptVisa,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'rejectvisa': this.rejectVisa,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Directly remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reject the Visa.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectVisa(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectVisa');\n const rfcId = btn.dataset.rfcId;\n\n const response = await Repository.rejectVisa({rfcid: rfcId, comment: ''});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Accept the Visa.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptVisa(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectVisa');\n const rfcId = btn.dataset.rfcId;\n\n const response = await Repository.acceptVisa({rfcid: rfcId, comment: ''});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","visainfo","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","acceptVisa","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","rejectVisa","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcId","rfcid","comment","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/local/components/table.js b/amd/src/local/components/table.js index 42bd856..8d70276 100644 --- a/amd/src/local/components/table.js +++ b/amd/src/local/components/table.js @@ -61,6 +61,7 @@ const componentInit = () => { stateTemplate('modules'); stateTemplate('modulesstatic', '', 'static'); stateTemplate('rfc'); + stateTemplate('visainfo'); stateTemplate('editbuttons', '', 'modalheader'); }; diff --git a/amd/src/local/repository.js b/amd/src/local/repository.js index 289a34d..b6c19eb 100644 --- a/amd/src/local/repository.js +++ b/amd/src/local/repository.js @@ -212,6 +212,40 @@ class Repository { return promise; } + + /** + * Accept the rfc (visa). + * @param {Object} args The arguments. + * @return {Promise} The promise. + */ + async acceptVisa(args) { + const request = { + methodname: 'customfield_sprogramme_accept_visa', + args: args + }; + + let promise = Ajax.call([request])[0] + .fail(Notification.exception); + + return promise; + } + + /** + * Reject the rfc (visa). + * @param {Object} args The arguments. + * @return {Promise} The promise. + */ + async rejectVisa(args) { + const request = { + methodname: 'customfield_sprogramme_reject_visa', + args: args + }; + + let promise = Ajax.call([request])[0] + .fail(Notification.exception); + + return promise; + } } const RepositoryInstance = new Repository(); diff --git a/amd/src/manager.js b/amd/src/manager.js index fcabbe0..c6f9a5f 100644 --- a/amd/src/manager.js +++ b/amd/src/manager.js @@ -179,6 +179,9 @@ class Manager { State.setValue('columns', [...columns]); State.setValue('modules', modules); State.setValue('rfc', response.rfc ?? []); + if (response.visainfo) { + State.setValue('visainfo', response.visainfo); + } State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit}); this.sumtotals(); } else { @@ -443,6 +446,7 @@ class Manager { actions(action, element) { const actionMap = { 'addrow': this.addRow, + 'acceptvisa': this.acceptVisa, 'deleterow': this.deleteRow, 'addmodule': this.addModule, 'deletemodule': this.deleteModule, @@ -451,6 +455,7 @@ class Manager { 'closechanges': this.closeChanges, 'acceptrfc': this.acceptRfc, 'rejectrfc': this.rejectRfc, + 'rejectvisa': this.rejectVisa, 'submitrfc': this.submitRfc, 'cancelrfc': this.cancelRfc, 'removerfc': this.removeRfc, @@ -870,6 +875,38 @@ class Manager { pending.resolve(); } + /** + * Reject the Visa. + * @param {object} btn The button that was clicked. + * @return {void} + */ + async rejectVisa(btn) { + const pending = new Pending('customfield_sprogramme/manager:rejectVisa'); + const rfcId = btn.dataset.rfcId; + + const response = await Repository.rejectVisa({rfcid: rfcId, comment: ''}); + if (response) { + await this.getTableData(); + } + pending.resolve(); + } + + /** + * Accept the Visa. + * @param {object} btn The button that was clicked. + * @return {void} + */ + async acceptVisa(btn) { + const pending = new Pending('customfield_sprogramme/manager:rejectVisa'); + const rfcId = btn.dataset.rfcId; + + const response = await Repository.acceptVisa({rfcid: rfcId, comment: ''}); + if (response) { + await this.getTableData(); + } + pending.resolve(); + } + /** * Submit the RFC for approval. * @param {object} btn The button that was clicked. diff --git a/classes/external/accept_visa.php b/classes/external/accept_visa.php new file mode 100644 index 0000000..8313791 --- /dev/null +++ b/classes/external/accept_visa.php @@ -0,0 +1,81 @@ +. + +namespace customfield_sprogramme\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\restricted_context_exception; +use customfield_sprogramme\local\visa_manager; +use customfield_sprogramme\utils; + +/** + * Class accept_visa + * + * @package customfield_sprogramme + * @copyright 2025 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class accept_visa extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'rfcid' => new external_value(PARAM_INT, 'rfcid', VALUE_REQUIRED, ''), + 'comment' => new external_value(PARAM_TEXT, 'Comment', VALUE_DEFAULT, ''), + ]); + } + + /** + * Accept a RFC + * + * @param int $rfcid + * @param string $comment + * @return bool + */ + public static function execute(int $rfcid, string $comment): bool { + global $USER; + $params = self::validate_parameters( + self::execute_parameters(), + [ + 'rfcid' => $rfcid, + 'comment' => $comment, + ] + ); + $rfc = \customfield_sprogramme\local\persistent\sprogramme_rfc::get_record(['id' => $params['rfcid']]); + $datafieldid = $rfc->get('datafieldid'); + $context = utils::get_context_from_datafieldid($datafieldid); + self::validate_context($context); + $visa = new visa_manager($rfc->get('id')); + if (!$visa->can_visa($USER->id)) { + throw new \moodle_exception('visaacceptancenotallowed', 'customfield_sprogramme'); + } + return $visa->accept_visa($USER->id, $comment); + } + + /** + * Returns description of method result value + * + * @return external_value + */ + public static function execute_returns(): external_value { + return new external_value(PARAM_BOOL, 'Accepted'); + } +} diff --git a/classes/external/get_data.php b/classes/external/get_data.php index 4eb62e0..d986ca1 100644 --- a/classes/external/get_data.php +++ b/classes/external/get_data.php @@ -23,6 +23,7 @@ use core_external\external_value; use customfield_sprogramme\local\programme_manager; use customfield_sprogramme\local\rfc_manager; +use customfield_sprogramme\local\visa_manager; use customfield_sprogramme\utils; /** @@ -53,6 +54,7 @@ public static function execute_parameters(): external_function_parameters { * @return array $data - The data in JSON format */ public static function execute(int $datafieldid, bool $showrfc = false): array { + global $USER; $params = self::validate_parameters( self::execute_parameters(), ['datafieldid' => $datafieldid, 'showrfc' => $showrfc] @@ -81,8 +83,15 @@ public static function execute(int $datafieldid, bool $showrfc = false): array { $rfcdata = $rfc->get('snapshot'); $data['modules'] = json_decode($rfcdata, true); $data['rfc'] = $rfcmanager->get_data(); + $visamanager = new visa_manager($rfc->get('id')); + $data['visainfo'] = [ + 'canvisa' => $visamanager->can_visa($USER->id), + 'rfcid' => $rfc->get('id'), + 'visas' => $visamanager->get_visa_data(), + ]; } } + $data['columns'] = $programmemanger->get_column_totals($modules, $columns); return $data; } @@ -181,6 +190,25 @@ public static function execute_returns(): external_single_structure { 'fullname' => new external_value(PARAM_TEXT, 'New value', VALUE_OPTIONAL), ], 'User who created the RFC', VALUE_OPTIONAL), ], 'RFC data', VALUE_OPTIONAL), + 'visainfo' => new external_single_structure([ + 'canvisa' => new external_value(PARAM_BOOL, 'Can apply a visa', VALUE_OPTIONAL), + 'rfcid' => new external_value(PARAM_INT, 'RFC id', VALUE_OPTIONAL), + 'visas' => new external_multiple_structure( + new external_single_structure([ + 'comment' => new external_value(PARAM_BOOL, 'Comment', VALUE_OPTIONAL), + 'status' => new external_value(PARAM_BOOL, 'Status', VALUE_OPTIONAL), + 'statustext' => new external_value(PARAM_TEXT, 'Status text', VALUE_OPTIONAL), + 'visauser' => new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'UserId', VALUE_REQUIRED), + 'fullname' => new external_value(PARAM_TEXT, 'New value', VALUE_OPTIONAL), + ], 'User who created the RFC', VALUE_OPTIONAL), + 'timemodified' => new external_value(PARAM_INT, 'Time modified', VALUE_OPTIONAL), + ], 'Single visa', VALUE_OPTIONAL), + 'Visas data', + VALUE_OPTIONAL + ), + ], 'Visa info', VALUE_OPTIONAL), + 'canedit' => new external_value(PARAM_BOOL, 'Can edit', VALUE_REQUIRED), ]); } diff --git a/classes/external/reject_visa.php b/classes/external/reject_visa.php new file mode 100644 index 0000000..1595ce7 --- /dev/null +++ b/classes/external/reject_visa.php @@ -0,0 +1,81 @@ +. + +namespace customfield_sprogramme\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\restricted_context_exception; +use customfield_sprogramme\local\visa_manager; +use customfield_sprogramme\utils; + +/** + * Class reject_visa + * + * @package customfield_sprogramme + * @copyright 2025 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reject_visa extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'rfcid' => new external_value(PARAM_INT, 'rfcid', VALUE_REQUIRED, ''), + 'comment' => new external_value(PARAM_TEXT, 'Comment', VALUE_DEFAULT, ''), + ]); + } + + /** + * Accept a RFC + * + * @param int $rfcid + * @param string $comment + * @return bool + */ + public static function execute(int $rfcid, string $comment): bool { + global $USER; + $params = self::validate_parameters( + self::execute_parameters(), + [ + 'rfcid' => $rfcid, + 'comment' => $comment, + ] + ); + $rfc = \customfield_sprogramme\local\persistent\sprogramme_rfc::get_record(['id' => $params['rfcid']]); + $datafieldid = $rfc->get('datafieldid'); + $context = utils::get_context_from_datafieldid($datafieldid); + self::validate_context($context); + $visa = new visa_manager($rfc->get('id')); + if (!$visa->can_visa($USER->id)) { + throw new \moodle_exception('visaeditionnotallowed', 'customfield_sprogramme'); + } + return $visa->reject_visa($USER->id, $comment); + } + + /** + * Returns description of method result value + * + * @return external_value + */ + public static function execute_returns(): external_value { + return new external_value(PARAM_BOOL, 'Rejected'); + } +} diff --git a/classes/local/api/notifications.php b/classes/local/api/notifications.php index 849bbb3..1847496 100644 --- a/classes/local/api/notifications.php +++ b/classes/local/api/notifications.php @@ -82,7 +82,7 @@ private static function add_global_context(array $context, int $datafieldid, int $context['department'] = self::get_department_for_course($courseid); $context['responsibles'] = implode(', ', array_map(function ($user) { return fullname($user); - }, self::get_responsible_for_course($courseid))); + }, utils::get_responsible_for_course($courseid))); if (isset($context['usercreated'])) { $user = core_user::get_user($context['usercreated']); $context['requester'] = fullname($user); @@ -144,7 +144,7 @@ private static function get_recipients(string $type, int $datafieldid, array $co $approveremails = array_map('trim', $approveremails); // Now get the responsibles for courses. - $responsibles = self::get_responsible_for_course($courseid); + $responsibles = utils::get_responsible_for_course($courseid); $responsibleemails = []; foreach ($responsibles as $responsible) { $responsibleemails[] = $responsible->email; @@ -191,32 +191,6 @@ private static function process_placeholders($string, $a): string { return $string; } - /** - * Get users matching the responsible role. - * - * Note this is a duplicate of the get_responsible_for_course function in local_envasyllabus - * but we want to avoid a dependency on the local_envasyllabus plugin in the notifications class. - * - * @param int $courseid - * @return array - */ - protected static function get_responsible_for_course(int $courseid): array { - global $DB; - $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); - $teacherroles = $DB->get_fieldset( - 'role', - 'id', - ['shortname' => $responsiblerolename] - ); - if (!empty($teacherroles)) { - $userfieldsapi = \core_user\fields::for_userpic()->including('username', 'deleted'); - $userfields = 'ra.id, u.id, u.username' . $userfieldsapi->get_sql('u')->selects; - return get_role_users($teacherroles, \context_course::instance($courseid), true, $userfields); - } else { - return []; - } - } - /** * Get users matching the responsible role. * diff --git a/classes/local/persistent/sprogramme_visa.php b/classes/local/persistent/sprogramme_visa.php new file mode 100644 index 0000000..08ef432 --- /dev/null +++ b/classes/local/persistent/sprogramme_visa.php @@ -0,0 +1,87 @@ +. + +namespace customfield_sprogramme\local\persistent; + +use core\persistent; +use customfield_sprogramme\utils; +use lang_string; + +/** + * Class sprogramme_visa + * + * @package customfield_sprogramme + * @copyright 2025 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sprogramme_visa extends persistent { + /** + * Current table + */ + public const TABLE = 'customfield_sprogramme_rfc_visa'; + + /** + * Visa status constants PENDING = 2, APPROVED = 1, REJECTED = 0 + */ + public const STATUS_PENDING = 2; + /** + * Visa status constants PENDING = 2, APPROVED = 1, REJECTED = 0 + */ + public const STATUS_APPROVED = 1; + /** + * Visa status constants PENDING = 2, APPROVED = 1, REJECTED = 0 + */ + public const STATUS_REJECTED = 0; + + + #[\Override] + protected static function define_properties() { + return [ + 'rfcid' => [ + 'type' => PARAM_INT, + 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme_comp:rfcid'), + 'default' => 0, + ], + 'visauser' => [ + 'type' => PARAM_INT, + 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme:visauser'), + ], + 'status' => [ + 'type' => PARAM_INT, + 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme_comp:status'), + 'default' => 2, // Default to 'pending'. + ], + 'comment' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_ALLOWED, + 'message' => new lang_string('invaliddata', 'customfield_sprogramme', 'sprogramme_comp:comment'), + ], + ]; + } + + /** + * Get the status as a human-readable string. + * + * @return string + */ + public function get_status_string(): string { + return match ($this->get('status')) { + self::STATUS_APPROVED => get_string('approved', 'customfield_sprogramme'), + self::STATUS_REJECTED => get_string('rejected', 'customfield_sprogramme'), + default => get_string('pending', 'customfield_sprogramme'), + }; + } +} diff --git a/classes/local/visa_manager.php b/classes/local/visa_manager.php new file mode 100644 index 0000000..4ff8d37 --- /dev/null +++ b/classes/local/visa_manager.php @@ -0,0 +1,147 @@ +. + +namespace customfield_sprogramme\local; + +use context_system; +use customfield_sprogramme\local\persistent\sprogramme_rfc; +use customfield_sprogramme\local\persistent\sprogramme_visa; +use customfield_sprogramme\utils; + +/** + * Class programme + * + * @package customfield_sprogramme + * @copyright 2024 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class visa_manager { + /** + * The context identifier for the RFC. + * + * @var context The context of the RFC. + */ + private \context $context; + + /** + * Constructor + * + * @param int $rfcid + */ + public function __construct( + /** @var int $rfcid */ + private int $rfcid, + ) { + if (!sprogramme_rfc::record_exists($rfcid) ) { + throw new \moodle_exception('invalidrfcid', 'customfield_sprogramme'); + } + $datafieldid = sprogramme_rfc::get_record(['id' => $rfcid])->get('datafieldid'); + $this->context = utils::get_context_from_datafieldid($datafieldid) ?? context_system::instance(); + } + + /** + * Get all visas for the current RFC. + * + * @return sprogramme_visa[] + */ + public function get_visas(): array { + return sprogramme_visa::get_records(['rfcid' => $this->rfcid]); + } + + /** + * Get all visas for the current RFC formatted for output. + * + * @return array + */ + public function get_visa_data(): array { + $visas = $this->get_visas(); + $data = []; + foreach ($visas as $visa) { + $visauser = \core_user::get_user($visa->get('visauser')); + + $data[] = [ + 'id' => $visa->get('id'), + 'rfcid' => $visa->get('rfcid'), + 'visauser' => [ + 'id' => $visauser->id, + 'fullname' => fullname($visauser), + ], + 'status' => $visa->get('status'), + 'statustext' => $visa->get_status_string(), + 'timemodified' => $visa->get('timemodified'), + ]; + } + return $data; + } + + /** + * Check if a user can visa the current RFC. + * + * @param int $userid + * @return bool + */ + public function can_visa($userid): bool { + return utils::is_responsible($userid, $this->context); + } + + /** + * Accept a visa for the current RFC. + * + * @param int $userid + * @param string $comment + * @return bool + */ + public function accept_visa(int $userid, string $comment) { + $visa = $this->get_visa($userid); + $visa->set('status', sprogramme_visa::STATUS_APPROVED); + $visa->set('comment', $comment); + $visa->update(); + return true; + } + /** + * Accept a visa for the current RFC. + * + * @param int $userid + * @param string $comment + * @return bool + */ + public function reject_visa(int $userid, string $comment) { + $visa = $this->get_visa($userid); + $visa->set('status', sprogramme_visa::STATUS_REJECTED); + $visa->set('comment', $comment); + $visa->update(); + return true; + } + + /** + * Get or create a visa for a user and the current RFC. + * + * @param int $userid + * @return sprogramme_visa + */ + private function get_visa(int $userid): sprogramme_visa { + $visa = sprogramme_visa::get_record(['rfcid' => $this->rfcid, 'visauser' => $userid]); + if (!$visa) { + $visa = new sprogramme_visa(0, (object) [ + 'rfcid' => $this->rfcid, + 'visauser' => $userid, + 'comment' => '', + ]); + $visa->create(); + } + return $visa; + } +} \ No newline at end of file diff --git a/classes/utils.php b/classes/utils.php index 080a354..018c78d 100644 --- a/classes/utils.php +++ b/classes/utils.php @@ -16,6 +16,7 @@ namespace customfield_sprogramme; +use core\context; use core\output\user_picture; use core_user; @@ -102,4 +103,54 @@ public static function get_context_from_datafieldid(int $datafieldid): ?\context } return null; } + + /** + * Get users matching the responsible role. + * + * Note this is a duplicate of the get_responsible_for_course function in local_envasyllabus + * but we want to avoid a dependency on the local_envasyllabus plugin in the notifications class. + * + * @param int $courseid + * @return array + */ + public static function get_responsible_for_course(int $courseid): array { + $responsibleroleid = self::get_responsible_role_id(); + if (!empty($responsibleroleid)) { + $userfieldsapi = \core_user\fields::for_userpic()->including('username', 'deleted'); + $userfields = 'ra.id, u.id, u.username' . $userfieldsapi->get_sql('u')->selects; + return get_role_users($responsibleroleid, \context_course::instance($courseid), true, $userfields); + } else { + return []; + } + } + + /** + * Check if a user has the responsible role for a given context. + * + * @param int $userid The ID of the user to check. + * @param context $context The context to check against. + * @return bool True if the user has the responsible role, false otherwise. + */ + public static function is_responsible(int $userid, context $context): bool { + $responsibleroleid = self::get_responsible_role_id(); + if (!empty($responsibleroleid)) { + return user_has_role_assignment($userid, $responsibleroleid, $context->id); + } + return false; + } + + /** + * Get the ID of the responsible role from the plugin settings. + * + * @return int|null The ID of the responsible role, or null if not found. + */ + protected static function get_responsible_role_id(): ?int { + global $DB; + $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); + return $DB->get_field( + 'role', + 'id', + ['shortname' => $responsiblerolename] + ); + } } diff --git a/db/install.xml b/db/install.xml index 26c6b31..8802874 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -103,6 +103,27 @@ + + + + + + + + + + + + + + + + + + + + +
diff --git a/db/services.php b/db/services.php index 3ceb281..abfe4b1 100644 --- a/db/services.php +++ b/db/services.php @@ -114,4 +114,20 @@ 'ajax' => true, 'capabilities' => 'customfield/sprogramme:view', ], + 'customfield_sprogramme_accept_visa' => [ + 'classname' => \customfield_sprogramme\external\accept_visa::class, + 'methodname' => 'execute', + 'description' => 'Accept a visa for RFC', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'customfield/sprogramme:edit', + ], + 'customfield_sprogramme_reject_visa' => [ + 'classname' => \customfield_sprogramme\external\reject_visa::class, + 'methodname' => 'execute', + 'description' => 'Accept a visa for RFC', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'customfield/sprogramme:edit', + ], ]; diff --git a/db/upgrade.php b/db/upgrade.php index 2eb941e..d5a085f 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -131,6 +131,34 @@ function xmldb_customfield_sprogramme_upgrade($oldversion) { // Sprogramme savepoint reached. upgrade_plugin_savepoint(true, 2026020600, 'customfield', 'sprogramme'); } + if ($oldversion < 2026020800) { + // Define table customfield_sprogramme_rfc_visa to be created. + $table = new xmldb_table('customfield_sprogramme_rfc_visa'); + // Adding fields to table customfield_sprogramme_rfc_visa. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('rfcid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('visauser', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('status', XMLDB_TYPE_INTEGER, '4', null, null, null, null); + $table->add_field('comment', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table customfield_sprogramme_rfc_visa. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('visauser_fk', XMLDB_KEY_FOREIGN, ['visauser'], 'user', ['id']); + $table->add_key('rfc_fk', XMLDB_KEY_FOREIGN, ['rfcid'], 'customfield_sprogramme_rfc', ['id']); + $table->add_key('usermodified_fk', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']); + // Adding indexes to table customfield_sprogramme_rfc_visa. + $table->add_index('rfcuser_ix', XMLDB_INDEX_UNIQUE, ['rfcid', 'visauser']); + + // Conditionally launch create table for customfield_sprogramme_rfc_visa. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + upgrade_plugin_savepoint(true, 2026020800, 'customfield', 'sprogramme'); + } return true; } diff --git a/lang/en/customfield_sprogramme.php b/lang/en/customfield_sprogramme.php index 7e890d3..dd23fbd 100644 --- a/lang/en/customfield_sprogramme.php +++ b/lang/en/customfield_sprogramme.php @@ -27,6 +27,8 @@ $string['aas_help'] = 'Supervised Self-Learning: Teaching including sequences of individual autonomous learning where students use available teaching materials (and can obtain, upon request, occasional help from teachers) and self-evaluate (e-learning for example).'; $string['accept'] = 'Accept'; +$string['acceptvisa'] = 'Validate the modification'; +$string['accepted'] = 'Accepted'; $string['addmodule'] = 'Add module'; $string['addrow'] = 'Add row'; $string['alreadyset'] = 'Already set for this row.'; @@ -145,6 +147,8 @@ $string['programme:uc'] = 'Course unit'; $string['programme:usermodified'] = 'Modified by'; $string['reject'] = 'Reject'; +$string['rejected'] = 'Rejected'; +$string['rejectvisa'] = 'Do not validate the modification'; $string['removerfc'] = 'Reset all changes'; $string['report:competencies'] = 'Competencies Report'; $string['report:disciplines'] = 'Disciplines Report'; diff --git a/lang/fr/customfield_sprogramme.php b/lang/fr/customfield_sprogramme.php index a440218..4e76ecf 100644 --- a/lang/fr/customfield_sprogramme.php +++ b/lang/fr/customfield_sprogramme.php @@ -27,6 +27,8 @@ $string['aas_help'] = 'Auto-Apprentissage Supervisé : Enseignement comprenant des séquences d’apprentissage individuel en autonomie où les élèves utilisent un matériel pédagogique disponible (et peuvent obtenir, à leur demande, une aide ponctuelle des enseignants) et s\'auto-évaluent (e-learning par exemple).'; $string['accept'] = 'Accepter'; +$string['acceptvisa'] = 'Valider la modification'; +$string['accepted'] = 'Accepté'; $string['addmodule'] = 'Ajouter un module'; $string['addrow'] = 'Ajouter une ligne'; $string['alreadyset'] = 'Déjà définie pour cette ligne.'; @@ -151,6 +153,8 @@ $string['programme:uc'] = 'UC'; $string['programme:usermodified'] = 'Modifié par'; $string['reject'] = 'Rejeter'; +$string['rejected'] = 'Rejeté'; +$string['rejectvisa'] = 'Ne pas valider la modification'; $string['removerfc'] = 'Réinitialiser toutes les modifications'; $string['report:competencies'] = 'Rapport des compétences'; $string['report:disciplines'] = 'Rapport des disciplines'; diff --git a/templates/formfield.mustache b/templates/formfield.mustache index 5f6d497..abb738c 100644 --- a/templates/formfield.mustache +++ b/templates/formfield.mustache @@ -38,6 +38,8 @@
+
+
diff --git a/templates/table/visainfo.mustache b/templates/table/visainfo.mustache new file mode 100644 index 0000000..e469d4f --- /dev/null +++ b/templates/table/visainfo.mustache @@ -0,0 +1,57 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template customfield_sprogramme/table/visainfo + + A visa represents a change request for a specific row in the table. It allows responsible users to accept, reject and comment on the change. This will be displayed + to the user who made the change and to other responsible users until the change is accepted or rejected. + + Example context (json): + { + "canvisa": true, + "visas": [ + { + "comment": "Please provide more details about the change.", + "status": "accepted", + "timemodified": 1685678901, + "visauser": { + "fullname": "John Doe", + "id": 1 + } + } + ] + } +}} +
+ {{#visainfo}} + {{#visas}} +
+ {{#str}}visa:changerequestby, customfield_sprogramme, {{visauser.fullname}}{{/str}} +

{{#str}} + submitdate, customfield_sprogramme{{/str}} {{#userdate}} {{timemodified}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}

+
+ {{/visas}} + {{#canvisa}} + + + {{/canvisa}} + {{/visainfo}} +
diff --git a/version.php b/version.php index 911ac92..8092bd2 100644 --- a/version.php +++ b/version.php @@ -26,6 +26,6 @@ $plugin->component = 'customfield_sprogramme'; $plugin->release = '2.3.0'; -$plugin->version = 2026020600; +$plugin->version = 2026020801; $plugin->requires = 2022112800; $plugin->maturity = MATURITY_BETA; From 6c3688287fd8594d16c5be0c7e0960bcec44b358 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Mon, 9 Feb 2026 20:03:25 +0100 Subject: [PATCH 10/15] Fix #768: Add head of department role --- classes/local/api/notifications.php | 4 +- classes/local/visa_manager.php | 2 +- classes/utils.php | 58 ++++++++++++++++++--- lang/en/customfield_sprogramme.php | 2 + lang/fr/customfield_sprogramme.php | 2 + settings.php | 9 ++++ tests/local/api/notifications_test.php | 18 +++++++ tests/local/observers/rfc_observer_test.php | 19 ++++++- 8 files changed, 102 insertions(+), 12 deletions(-) diff --git a/classes/local/api/notifications.php b/classes/local/api/notifications.php index 1847496..c8e6bae 100644 --- a/classes/local/api/notifications.php +++ b/classes/local/api/notifications.php @@ -82,7 +82,7 @@ private static function add_global_context(array $context, int $datafieldid, int $context['department'] = self::get_department_for_course($courseid); $context['responsibles'] = implode(', ', array_map(function ($user) { return fullname($user); - }, utils::get_responsible_for_course($courseid))); + }, utils::get_responsible_visa_reviewer_for_course($courseid))); if (isset($context['usercreated'])) { $user = core_user::get_user($context['usercreated']); $context['requester'] = fullname($user); @@ -144,7 +144,7 @@ private static function get_recipients(string $type, int $datafieldid, array $co $approveremails = array_map('trim', $approveremails); // Now get the responsibles for courses. - $responsibles = utils::get_responsible_for_course($courseid); + $responsibles = utils::get_responsible_visa_reviewer_for_course($courseid); $responsibleemails = []; foreach ($responsibles as $responsible) { $responsibleemails[] = $responsible->email; diff --git a/classes/local/visa_manager.php b/classes/local/visa_manager.php index 4ff8d37..c07eb4a 100644 --- a/classes/local/visa_manager.php +++ b/classes/local/visa_manager.php @@ -94,7 +94,7 @@ public function get_visa_data(): array { * @return bool */ public function can_visa($userid): bool { - return utils::is_responsible($userid, $this->context); + return utils::is_responsible_visa_reviewer($userid, $this->context); } /** diff --git a/classes/utils.php b/classes/utils.php index 018c78d..99380c6 100644 --- a/classes/utils.php +++ b/classes/utils.php @@ -105,7 +105,7 @@ public static function get_context_from_datafieldid(int $datafieldid): ?\context } /** - * Get users matching the responsible role. + * Get users matching the head of department role. * * Note this is a duplicate of the get_responsible_for_course function in local_envasyllabus * but we want to avoid a dependency on the local_envasyllabus plugin in the notifications class. @@ -113,7 +113,7 @@ public static function get_context_from_datafieldid(int $datafieldid): ?\context * @param int $courseid * @return array */ - public static function get_responsible_for_course(int $courseid): array { + public static function get_head_of_department_for_course(int $courseid): array { $responsibleroleid = self::get_responsible_role_id(); if (!empty($responsibleroleid)) { $userfieldsapi = \core_user\fields::for_userpic()->including('username', 'deleted'); @@ -125,18 +125,45 @@ public static function get_responsible_for_course(int $courseid): array { } /** - * Check if a user has the responsible role for a given context. + * Get users matching the role of the person who can add an approval/visa to the changes made on the syllabus + * + * Note this is not exactly simular to the get_responsible_for_course function in local_envasyllabus as + * we add a new set of role like head of department role that can also be responsible for approving the changes on the syllabus. + * + * @param int $courseid + * @return array + */ + public static function get_responsible_visa_reviewer_for_course(int $courseid): array { + $responsibleroleid = self::get_responsible_role_id(); + $headofdepartmentroleid = self::get_hod_role_id(); + + if (!empty($responsibleroleid) && !empty($headofdepartmentroleid)) { + $userfieldsapi = \core_user\fields::for_userpic()->including('username', 'deleted'); + $userfields = 'ra.id, u.id, u.username' . $userfieldsapi->get_sql('u')->selects; + return get_role_users( + [$responsibleroleid, $headofdepartmentroleid], + \context_course::instance($courseid), + true, + $userfields + ); + } else { + return []; + } + } + + /** + * Check if a user can review/approve the changes made on the syllabus based on the responsible + * role or head of department role assignment in the given context. * * @param int $userid The ID of the user to check. * @param context $context The context to check against. * @return bool True if the user has the responsible role, false otherwise. */ - public static function is_responsible(int $userid, context $context): bool { + public static function is_responsible_visa_reviewer(int $userid, context $context): bool { $responsibleroleid = self::get_responsible_role_id(); - if (!empty($responsibleroleid)) { - return user_has_role_assignment($userid, $responsibleroleid, $context->id); - } - return false; + $headofdepartmentroleid = self::get_hod_role_id(); + return user_has_role_assignment($userid, $responsibleroleid, $context->id) || + user_has_role_assignment($userid, $headofdepartmentroleid, $context->id); } /** @@ -153,4 +180,19 @@ protected static function get_responsible_role_id(): ?int { ['shortname' => $responsiblerolename] ); } + + /** + * Get the ID of the head of department role from the plugin settings. + * + * @return int|null The ID of the responsible role, or null if not found. + */ + protected static function get_hod_role_id(): ?int { + global $DB; + $headofdepartmentrolename = get_config('customfield_sprogramme', 'departmentheadrolename'); + return $DB->get_field( + 'role', + 'id', + ['shortname' => $headofdepartmentrolename] + ); + } } diff --git a/lang/en/customfield_sprogramme.php b/lang/en/customfield_sprogramme.php index dd23fbd..a682b70 100644 --- a/lang/en/customfield_sprogramme.php +++ b/lang/en/customfield_sprogramme.php @@ -56,6 +56,8 @@ $string['dd_rse_help'] = 'Sustainable Development / Social and Environmental Responsibility: This checkbox indicates whether the session fully or partially addresses concepts related to the SD / SER domain.'; $string['departmentcustomfieldname'] = 'Department custom field'; $string['departmentcustomfieldname_desc'] = 'The department custom field used for notifications.'; +$string['departmentheadrolename'] = 'Department head role (shortname)'; +$string['departmentheadrolename_desc'] = 'Department head role (shortname) - Users with this role will be notified of programme change requests and can visa them.'; $string['discipline:name'] = 'Name'; $string['discipline:parent'] = 'Parent'; $string['discipline:sortorder'] = 'Sort order'; diff --git a/lang/fr/customfield_sprogramme.php b/lang/fr/customfield_sprogramme.php index 4e76ecf..789b08c 100644 --- a/lang/fr/customfield_sprogramme.php +++ b/lang/fr/customfield_sprogramme.php @@ -56,6 +56,8 @@ $string['dd_rse_help'] = 'Développement Durable / Responsabilité Sociétale et Environnementale : Cette case indique si la séance traite intégralement ou en partie de notions en lien avec le domaine DD / RSE.'; $string['departmentcustomfieldname'] = 'Department custom field'; $string['departmentcustomfieldname_desc'] = 'The department custom field used for notifications.'; +$string['departmentheadrolename'] = 'Chef de département (nom court)'; +$string['departmentheadrolename_desc'] = 'Chef de département (nom court) - Les utilisateurs avec ce rôle seront notifiés des demandes de modification de programme et pourront les certifier (visa).'; $string['discipline:name'] = 'Nom'; $string['discipline:parent'] = 'Parent'; $string['discipline:sortorder'] = 'Ordre de tri'; diff --git a/settings.php b/settings.php index f739b9c..aaa9e04 100644 --- a/settings.php +++ b/settings.php @@ -71,5 +71,14 @@ PARAM_ALPHANUM ); $settings->add($responsiblerolename); + + $responsiblerolename = new admin_setting_configtext( + 'customfield_sprogramme/departmentheadrolename', + get_string('departmentheadrolename', 'customfield_sprogramme'), + get_string('departmentheadrolename_desc', 'customfield_sprogramme'), + 'qualite', + PARAM_ALPHANUM + ); + $settings->add($responsiblerolename); } } diff --git a/tests/local/api/notifications_test.php b/tests/local/api/notifications_test.php index 4185eef..bc123f2 100644 --- a/tests/local/api/notifications_test.php +++ b/tests/local/api/notifications_test.php @@ -239,6 +239,7 @@ public function test_add_global_context(): void { $cfgenerator->add_instance_data($cfielddept, $this->course->id, 'DSPB'); set_config('departmentcustomfieldname', 'newdept', 'customfield_sprogramme'); + // Responsible and department head roles are required for the context, so we create them and enrol some users with these roles. $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); $generator->create_role( [ @@ -260,6 +261,22 @@ public function test_add_global_context(): void { 'lastname' => 'Two', ]); + $hoduser = $generator->create_user([ + 'username' => 'headofdepartment1', + 'email' => 'headofdepartment@example.com', + 'firstname' => 'Head of', + 'lastname' => 'Department', + ]); + $departmentheadrolename = get_config('customfield_sprogramme', 'departmentheadrolename'); + $hodroleid = $generator->create_role( + [ + 'shortname' => $departmentheadrolename, + 'name' => 'Responsible', + 'archetype' => 'teacher', + ] + ); + $generator->role_assign($hodroleid, $hoduser->id, \context_coursecat::instance($this->course->category)); + $method = new \ReflectionMethod(notifications::class, 'add_global_context'); $method->setAccessible(true); $user = $generator->create_user([ @@ -287,6 +304,7 @@ public function test_add_global_context(): void { $this->assertEquals('DSPB', $context['department']); $this->assertStringContainsString('Responsible One', $context['responsibles']); $this->assertStringContainsString('Responsible Two', $context['responsibles']); + $this->assertStringContainsString('Head of Department', $context['responsibles']); $this->assertEquals('User 1', $context['requester']); } } diff --git a/tests/local/observers/rfc_observer_test.php b/tests/local/observers/rfc_observer_test.php index f66adf8..86399f6 100644 --- a/tests/local/observers/rfc_observer_test.php +++ b/tests/local/observers/rfc_observer_test.php @@ -165,6 +165,22 @@ public function test_rfc_accepted_email_sent(): void { 'lastname' => 'Two', ]); + $hoduser = $generator->create_user([ + 'username' => 'headofdepartment1', + 'email' => 'headofdepartment@example.com', + 'firstname' => 'Head of', + 'lastname' => 'Department', + ]); + $departmentheadrolename = get_config('customfield_sprogramme', 'departmentheadrolename'); + $hodroleid = $generator->create_role( + [ + 'shortname' => $departmentheadrolename, + 'name' => 'Responsible', + 'archetype' => 'teacher', + ] + ); + $generator->role_assign($hodroleid, $hoduser->id, \context_coursecat::instance($this->course->category)); + $emailsink = $this->redirectEmails(); // Accept the RFC. $this->setAdminUser(); @@ -180,13 +196,14 @@ public function test_rfc_accepted_email_sent(): void { $emails = $emailsink->get_messages(); // No email should be sent on approval. - $this->assertCount(5, $emails); + $this->assertCount(6, $emails); $emailsto = array_map(fn($email) => $email->to, $emails); $this->assertContains('admin@example.com', $emailsto); $this->assertContains('otheruser@example.com', $emailsto); $this->assertContains('teacher1@example.com', $emailsto); $this->assertContains('responsible1@example.com', $emailsto); $this->assertContains('responsible2@example.com', $emailsto); + $this->assertContains('headofdepartment@example.com', $emailsto); $email = reset($emails); $this->assertEquals( '[Syllabus] Programme change validated for UC: tc_1 - Test course 1', From 0d8aab1ac6e899a17ef0c74206d987434add9a98 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Mon, 9 Feb 2026 21:57:49 +0100 Subject: [PATCH 11/15] Fix #702 --- .../local/entities/programme.php | 5 +++ .../local/entities/rfc_totals.php | 37 +++++++++++++++++++ .../local/systemreports/competencies.php | 2 +- .../local/systemreports/disciplines.php | 2 +- lang/en/customfield_sprogramme.php | 6 ++- lang/fr/customfield_sprogramme.php | 6 ++- 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/classes/reportbuilder/local/entities/programme.php b/classes/reportbuilder/local/entities/programme.php index 1fa3231..82282b9 100644 --- a/classes/reportbuilder/local/entities/programme.php +++ b/classes/reportbuilder/local/entities/programme.php @@ -441,4 +441,9 @@ protected function get_all_filters(): array { return $filters; } + + #[\Override] + protected function can_view(): bool { + return has_capability('moodle/reportbuilder:view', \context_system::instance()); + } } diff --git a/classes/reportbuilder/local/entities/rfc_totals.php b/classes/reportbuilder/local/entities/rfc_totals.php index 2843f61..1051a65 100644 --- a/classes/reportbuilder/local/entities/rfc_totals.php +++ b/classes/reportbuilder/local/entities/rfc_totals.php @@ -23,6 +23,7 @@ use core_reportbuilder\local\filters\user; use core_reportbuilder\local\helpers\format as rbformat; use core_reportbuilder\local\report\{column, filter}; +use customfield_sprogramme\local\persistent\sprogramme_rfc; use customfield_sprogramme\local\programme_manager; use customfield_sprogramme\reportbuilder\local\helpers\format; use lang_string; @@ -175,6 +176,26 @@ protected function get_all_columns(): array { ->add_fields("{$rfcalias}.fmp") ->set_is_sortable(true); + $columns[] = (new column( + 'perso_ap', + new lang_string('programme:perso_ap', 'customfield_sprogramme'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_FLOAT) + ->add_fields("{$rfcalias}.perso_ap") + ->set_is_sortable(true); + + $columns[] = (new column( + 'perso_av', + new lang_string('programme:perso_av', 'customfield_sprogramme'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_FLOAT) + ->add_fields("{$rfcalias}.perso_av") + ->set_is_sortable(true); + $columns[] = (new column( 'timecreated', new lang_string('programme:timecreated', 'customfield_sprogramme'), @@ -196,6 +217,21 @@ protected function get_all_columns(): array { ->add_fields("{$rfcalias}.timemodified") ->set_is_sortable(true) ->set_callback([rbformat::class, 'userdate']); + $columns[] = (new column( + 'type', + new lang_string('rfc:type', 'customfield_sprogramme'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$rfcalias}.type") + ->set_is_sortable(true) + ->set_callback( + fn($value) => get_string( + 'rfc:' . sprogramme_rfc::CHANGE_TYPES[$value] ?? 'unknown', + 'customfield_sprogramme' + ) + ); return $columns; } @@ -341,6 +377,7 @@ private function init_temp_table() { $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('type', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); $dbman->create_temp_table($table); diff --git a/classes/reportbuilder/local/systemreports/competencies.php b/classes/reportbuilder/local/systemreports/competencies.php index ecffaa4..13e69c8 100644 --- a/classes/reportbuilder/local/systemreports/competencies.php +++ b/classes/reportbuilder/local/systemreports/competencies.php @@ -78,6 +78,6 @@ protected function add_filters(): void { #[\Override] protected function can_view(): bool { - return has_capability('moodle/site:config', \context_system::instance()); + return has_capability('moodle/reportbuilder:view', \context_system::instance()); } } diff --git a/classes/reportbuilder/local/systemreports/disciplines.php b/classes/reportbuilder/local/systemreports/disciplines.php index 4d5d422..5896d82 100644 --- a/classes/reportbuilder/local/systemreports/disciplines.php +++ b/classes/reportbuilder/local/systemreports/disciplines.php @@ -78,6 +78,6 @@ protected function add_filters(): void { #[\Override] protected function can_view(): bool { - return has_capability('moodle/site:viewreports', \context_system::instance()); + return has_capability('moodle/reportbuilder:view', \context_system::instance()); } } diff --git a/lang/en/customfield_sprogramme.php b/lang/en/customfield_sprogramme.php index a682b70..e2717cc 100644 --- a/lang/en/customfield_sprogramme.php +++ b/lang/en/customfield_sprogramme.php @@ -162,6 +162,7 @@ $string['responsiblerolename_desc'] = 'Short name of the responsible role. Users with this role will be notified of programme change requests and can certify them.'; $string['rfc:accepted'] = 'Accepted'; $string['rfc:actions'] = 'Actions'; +$string['rfc:cancelled'] = 'Cancelled'; $string['rfc:changerequestby'] = 'Change request by {$a}'; $string['rfc:course'] = 'Course'; $string['rfc:help'] = 'Help'; @@ -184,8 +185,11 @@ $string['rfc:selectstatus'] = 'Select status'; $string['rfc:status'] = 'Status'; $string['rfc:submitted'] = 'Submitted'; +$string['rfc:type'] = 'Modification type'; $string['rfc:timecreated'] = 'Time created'; -$string['rfc:user'] = 'User'; +$string['rfc:unknown'] = 'Unknown status'; +$string['rfc:user'] = 'Submitted by'; +$string['rfc:validator'] = 'Validator'; $string['rfc:view'] = 'View'; $string['rfcs'] = 'Requests {$a}'; $string['row'] = 'Row {$a}'; diff --git a/lang/fr/customfield_sprogramme.php b/lang/fr/customfield_sprogramme.php index 789b08c..6d14771 100644 --- a/lang/fr/customfield_sprogramme.php +++ b/lang/fr/customfield_sprogramme.php @@ -168,6 +168,7 @@ $string['responsiblerolename_desc'] = 'Nom court du rôle responsable. Les utilisateurs avec ce rôle seront notifiés des demandes de modification de programme et pourront les certifier.'; $string['rfc:accepted'] = 'Acceptée'; $string['rfc:actions'] = 'Actions'; +$string['rfc:cancelled'] = 'Annulée'; $string['rfc:changerequestby'] = 'Demande de modification par {$a}'; $string['rfc:course'] = 'Cours'; $string['rfc:help'] = 'Aide'; @@ -191,8 +192,11 @@ $string['rfc:status'] = 'Statut'; $string['rfc:submitted'] = 'Soumise'; $string['rfc:timecreated'] = 'Date de création'; -$string['rfc:user'] = 'Utilisateur'; +$string['rfc:type'] = 'Type de modification'; +$string['rfc:unknown'] = 'Statut inconnu'; +$string['rfc:user'] = 'Soumission par'; $string['rfc:view'] = 'Voir'; +$string['rfc:validator'] = 'Validateur'; $string['rfcs'] = 'Demandes {$a}'; $string['row'] = 'Ligne {$a}'; $string['saving'] = 'Enregistrement...'; From b5620ea8e9548800d3ea0eb5ba9dc5c3766ecde5 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Thu, 12 Feb 2026 22:33:39 +0100 Subject: [PATCH 12/15] Add visa interface for #761 --- amd/build/local/repository.min.js | 2 +- amd/build/local/repository.min.js.map | 2 +- amd/build/manager.min.js | 2 +- amd/build/manager.min.js.map | 2 +- amd/build/visa_form.min.js | 10 + amd/build/visa_form.min.js.map | 1 + amd/src/manager.js | 36 +--- amd/src/visa_form.js | 82 ++++++++ classes/event/rfc_visa_updated.php | 70 +++++++ classes/external/get_data.php | 11 +- classes/local/api/notifications.php | 4 + classes/local/form/programme_upload_form.php | 4 +- classes/local/form/visa_validate_form.php | 142 +++++++++++++ classes/local/observers/rfc_visa_observer.php | 52 +++++ classes/local/persistent/sprogramme_rfc.php | 10 + classes/local/visa_manager.php | 110 ++++++++-- db/events.php | 4 + lang/en/customfield_sprogramme.php | 32 ++- lang/fr/customfield_sprogramme.php | 32 ++- scss/styles.scss | 6 + styles.css | 22 +- templates/formfield.mustache | 2 +- templates/table/visainfo.mustache | 106 ++++++++-- .../observers/rfc_visa_observer_test.php | 135 ++++++++++++ tests/local/visa_manager_test.php | 198 ++++++++++++++++++ 25 files changed, 984 insertions(+), 93 deletions(-) create mode 100644 amd/build/visa_form.min.js create mode 100644 amd/build/visa_form.min.js.map create mode 100644 amd/src/visa_form.js create mode 100644 classes/event/rfc_visa_updated.php create mode 100644 classes/local/form/visa_validate_form.php create mode 100644 classes/local/observers/rfc_visa_observer.php create mode 100644 tests/local/observers/rfc_visa_observer_test.php create mode 100644 tests/local/visa_manager_test.php diff --git a/amd/build/local/repository.min.js b/amd/build/local/repository.min.js index 80d662e..470a992 100644 --- a/amd/build/local/repository.min.js +++ b/amd/build/local/repository.min.js @@ -5,6 +5,6 @@ define("customfield_sprogramme/local/repository",["exports","core/ajax","core/no * @module customfield_sprogramme/local/repository * @copyright 2024 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);var _default=new class{getColumns(args){const request={methodname:"customfield_sprogramme_get_columns",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getData(args){const request={methodname:"customfield_sprogramme_get_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}setData(args){const request={methodname:"customfield_sprogramme_set_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}csvData(args){const request={methodname:"customfield_sprogramme_csv_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}acceptRfc(args){const request={methodname:"customfield_sprogramme_accept_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}rejectRfc(args){const request={methodname:"customfield_sprogramme_reject_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}submitRfc(args){const request={methodname:"customfield_sprogramme_submit_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}cancelRfc(args){const request={methodname:"customfield_sprogramme_cancel_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}removeRfc(args){const request={methodname:"customfield_sprogramme_remove_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getProgrammeHistory(args){const request={methodname:"customfield_sprogramme_get_programme_history",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getTags(){return _ajax.default.call([{methodname:"customfield_sprogramme_get_tags",args:{}}])[0].fail(_notification.default.exception)}async acceptVisa(args){const request={methodname:"customfield_sprogramme_reject_visa",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}async rejectVisa(args){const request={methodname:"customfield_sprogramme_accept_visa",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}};return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);var _default=new class{getColumns(args){const request={methodname:"customfield_sprogramme_get_columns",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getData(args){const request={methodname:"customfield_sprogramme_get_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}setData(args){const request={methodname:"customfield_sprogramme_set_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}csvData(args){const request={methodname:"customfield_sprogramme_csv_data",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}acceptRfc(args){const request={methodname:"customfield_sprogramme_accept_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}rejectRfc(args){const request={methodname:"customfield_sprogramme_reject_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}submitRfc(args){const request={methodname:"customfield_sprogramme_submit_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}cancelRfc(args){const request={methodname:"customfield_sprogramme_cancel_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}removeRfc(args){const request={methodname:"customfield_sprogramme_remove_rfc",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getProgrammeHistory(args){const request={methodname:"customfield_sprogramme_get_programme_history",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}getTags(){return _ajax.default.call([{methodname:"customfield_sprogramme_get_tags",args:{}}])[0].fail(_notification.default.exception)}async acceptVisa(args){const request={methodname:"customfield_sprogramme_accept_visa",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}async rejectVisa(args){const request={methodname:"customfield_sprogramme_reject_visa",args:args};return _ajax.default.call([request])[0].fail(_notification.default.exception)}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/amd/build/local/repository.min.js.map b/amd/build/local/repository.min.js.map index 44775aa..08c1021 100644 --- a/amd/build/local/repository.min.js.map +++ b/amd/build/local/repository.min.js.map @@ -1 +1 @@ -{"version":3,"file":"repository.min.js","sources":["../../src/local/repository.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 * Gateway to the webservices.\n *\n * @module customfield_sprogramme/local/repository\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n\n/**\n * Competvet repository class.\n */\nclass Repository {\n\n /**\n * Get JSON data\n * @param {Object} args The data to get.\n * @return {Promise} The promise.\n */\n getColumns(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_columns',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getData(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Set the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n setData(args) {\n const request = {\n methodname: 'customfield_sprogramme_set_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the CSV data for download.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n csvData(args) {\n const request = {\n methodname: 'customfield_sprogramme_csv_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Accept the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n acceptRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_accept_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Reject the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n rejectRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_reject_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Submit the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n submitRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_submit_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Cancel the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n cancelRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_cancel_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Remove the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n removeRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_remove_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the programme history.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getProgrammeHistory(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_programme_history',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the tags.\n * @return {Promise} The promise.\n */\n getTags() {\n const request = {\n methodname: 'customfield_sprogramme_get_tags',\n args: {}\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Accept the rfc (visa).\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n async acceptVisa(args) {\n const request = {\n methodname: 'customfield_sprogramme_reject_visa',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Reject the rfc (visa).\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n async rejectVisa(args) {\n const request = {\n methodname: 'customfield_sprogramme_accept_visa',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n}\n\nconst RepositoryInstance = new Repository();\n\nexport default RepositoryInstance;\n"],"names":["getColumns","args","request","methodname","Ajax","call","fail","Notification","exception","getData","setData","csvData","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","getProgrammeHistory","getTags"],"mappings":";;;;;;;0LA0P2B,UArNvBA,WAAWC,YACDC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BC,QAAQR,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BE,QAAQT,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BG,QAAQV,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BI,UAAUX,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BK,UAAUZ,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BM,UAAUb,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BO,UAAUd,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BQ,UAAUf,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BS,oBAAoBhB,YACVC,QAAU,CACZC,WAAY,+CACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BU,iBAMkBd,cAAKC,KAAK,CALR,CACZF,WAAY,kCACZF,KAAM,MAGyB,GAC9BK,KAAKC,sBAAaC,4BAUVP,YACPC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,4BAUVP,YACPC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC"} \ No newline at end of file +{"version":3,"file":"repository.min.js","sources":["../../src/local/repository.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 * Gateway to the webservices.\n *\n * @module customfield_sprogramme/local/repository\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n\n/**\n * Competvet repository class.\n */\nclass Repository {\n\n /**\n * Get JSON data\n * @param {Object} args The data to get.\n * @return {Promise} The promise.\n */\n getColumns(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_columns',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getData(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Set the Table data.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n setData(args) {\n const request = {\n methodname: 'customfield_sprogramme_set_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the CSV data for download.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n csvData(args) {\n const request = {\n methodname: 'customfield_sprogramme_csv_data',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Accept the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n acceptRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_accept_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Reject the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n rejectRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_reject_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Submit the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n submitRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_submit_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n /**\n * Cancel the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n cancelRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_cancel_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Remove the changes.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n removeRfc(args) {\n const request = {\n methodname: 'customfield_sprogramme_remove_rfc',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the programme history.\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n getProgrammeHistory(args) {\n const request = {\n methodname: 'customfield_sprogramme_get_programme_history',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Get the tags.\n * @return {Promise} The promise.\n */\n getTags() {\n const request = {\n methodname: 'customfield_sprogramme_get_tags',\n args: {}\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Accept the rfc (visa).\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n async acceptVisa(args) {\n const request = {\n methodname: 'customfield_sprogramme_accept_visa',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n\n /**\n * Reject the rfc (visa).\n * @param {Object} args The arguments.\n * @return {Promise} The promise.\n */\n async rejectVisa(args) {\n const request = {\n methodname: 'customfield_sprogramme_reject_visa',\n args: args\n };\n\n let promise = Ajax.call([request])[0]\n .fail(Notification.exception);\n\n return promise;\n }\n}\n\nconst RepositoryInstance = new Repository();\n\nexport default RepositoryInstance;\n"],"names":["getColumns","args","request","methodname","Ajax","call","fail","Notification","exception","getData","setData","csvData","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","getProgrammeHistory","getTags"],"mappings":";;;;;;;0LA0P2B,UArNvBA,WAAWC,YACDC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BC,QAAQR,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BE,QAAQT,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BG,QAAQV,YACEC,QAAU,CACZC,WAAY,kCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BI,UAAUX,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BK,UAAUZ,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BM,UAAUb,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BO,UAAUd,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BQ,UAAUf,YACAC,QAAU,CACZC,WAAY,oCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAU3BS,oBAAoBhB,YACVC,QAAU,CACZC,WAAY,+CACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,WAS3BU,iBAMkBd,cAAKC,KAAK,CALR,CACZF,WAAY,kCACZF,KAAM,MAGyB,GAC9BK,KAAKC,sBAAaC,4BAUVP,YACPC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC,4BAUVP,YACPC,QAAU,CACZC,WAAY,qCACZF,KAAMA,aAGIG,cAAKC,KAAK,CAACH,UAAU,GAC9BI,KAAKC,sBAAaC"} \ No newline at end of file diff --git a/amd/build/manager.min.js b/amd/build/manager.min.js index 5f5360a..b15acf3 100644 --- a/amd/build/manager.min.js +++ b/amd/build/manager.min.js @@ -1,3 +1,3 @@ -define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),response.visainfo&&_state.default.setValue("visainfo",response.visainfo),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,acceptvisa:this.acceptVisa,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,rejectvisa:this.rejectVisa,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+(parseFloat(cell.oldvalue)||0):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async rejectVisa(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectVisa"),rfcId=btn.dataset.rfcId;await _repository.default.rejectVisa({rfcid:rfcId,comment:""})&&await this.getTableData(),pending.resolve()}async acceptVisa(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectVisa"),rfcId=btn.dataset.rfcId;await _repository.default.acceptVisa({rfcid:rfcId,comment:""})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); +define("customfield_sprogramme/manager",["exports","customfield_sprogramme/local/state","customfield_sprogramme/local/repository","core/notification","core/str","core/utils","./local/components/table","core/pending","./tagmanager","./programme_form","./visa_form"],(function(_exports,_state,_repository,_notification,_str,_utils,_table,_pending,_tagmanager,_programme_form,_visa_form){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_state=_interopRequireDefault(_state),_repository=_interopRequireDefault(_repository),_notification=_interopRequireDefault(_notification),_table=_interopRequireDefault(_table),_pending=_interopRequireDefault(_pending),_programme_form=_interopRequireDefault(_programme_form),_visa_form=_interopRequireDefault(_visa_form);class Manager{constructor(element,datafieldid){_defineProperty(this,"rowNumber",0),_defineProperty(this,"moduleNumber",0),_defineProperty(this,"datafieldid",void 0),_defineProperty(this,"element",void 0),_defineProperty(this,"table","customfield_sprogramme"),_defineProperty(this,"columns",[]),this.element=element,this.datafieldid=parseInt(datafieldid),this.addEventListeners(),this.getTableData()}addEventListeners(){document.addEventListener("click",(e=>{let btn=e.target.closest(".modal-customfield_sprogramme_editor [data-action]");btn&&(e.preventDefault(),this.actions(btn.dataset.action,btn))}));const form=document.querySelector('[data-region="app"]');form.addEventListener("change",(e=>{const input=e.target.closest('[data-input="auto"]');input&&this.change(input);const modulename=e.target.closest('[data-region="modulename"]');modulename&&this.changeModule(modulename)})),document.addEventListener("input",(function(e){if("TEXTAREA"===e.target.tagName){const textarea=e.target;textarea.style.height="auto",textarea.style.height="".concat(textarea.scrollHeight+1,"px"),textarea.dataset.height=textarea.scrollHeight+1}})),form.addEventListener("keydown",(e=>{"ArrowDown"!==e.key&&"ArrowUp"!==e.key||(this.navigate(e),e.preventDefault())})),form.addEventListener("submit",(e=>{e.preventDefault()}));let dragging=null;form.addEventListener("dragstart",(e=>{const handle=e.target.closest('[data-region="dragicon"]');handle?(dragging=handle.closest("tr"),e.dataTransfer.effectAllowed="move"):e.preventDefault()})),form.addEventListener("dragover",(e=>{e.preventDefault();const target=e.target.closest("tr");if(target&&target!==dragging&&"rows"===target.parentNode.dataset.region){const rect=target.getBoundingClientRect();e.clientY-rect.top>rect.height/2?target.parentNode.insertBefore(dragging,target.nextSibling):target.parentNode.insertBefore(dragging,target)}})),form.addEventListener("drop",(e=>{e.preventDefault()})),form.addEventListener("dragend",(e=>{const rowId=dragging.dataset.index,prevRowId=dragging.previousElementSibling?dragging.previousElementSibling.dataset.index:0,moduleId=dragging.closest('[data-region="module"]').dataset.id;this.moveRow(parseInt(moduleId),parseInt(rowId),prevRowId?parseInt(prevRowId):null),dragging=null,e.preventDefault()}))}async getTableData(){try{const response=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:1});if(response.modules.length>0){var _response$rfc;const modules=this.parseModules(response),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",modules),_state.default.setValue("rfc",null!==(_response$rfc=response.rfc)&&void 0!==_response$rfc?_response$rfc:[]),response.visainfo&&_state.default.setValue("visainfo",response.visainfo),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.sumtotals()}else{const response=await _repository.default.getColumns({datafieldid:this.datafieldid}),columns=response.columns;_state.default.setValue("columns",[...columns]),_state.default.setValue("modules",[]),_state.default.setValue("rfc",[]),_state.default.setValue("editbuttons",{datafieldid:this.datafieldid,canedit:response.canedit}),this.addModule()}}catch(error){_notification.default.exception(error)}}parseModules(response){return response.modules.forEach((mod=>{mod.editor=response.canedit,mod.rows.map((row=>(row.cells=row.cells.map((cell=>{const column=response.columns.find((column=>column.column==cell.column));return"select"===(cell=Object.assign({},cell,column)).type&&(cell.options=cell.options.map((option=>{const clonedOption=Object.assign({},option);return clonedOption.name==cell.value&&(clonedOption.selected=!0),clonedOption}))),cell.changed=cell.value!==cell.oldvalue,cell})),row)))})),response.modules}getRowObject(){return{rows:{id:"id",sortorder:"sortorder",deleted:"false",cells:{type:"type",column:"column",value:"value",group:"group",oldvalue:"oldvalue"},disciplines:{id:"id",name:"name",percentage:"percentage"},competencies:{id:"id",name:"name",percentage:"percentage"}}}}checkCellValue(cell){null!==cell.value&&"text"===cell.type&&cell.value.length>cell.length&&(cell.value=cell.value.substring(0,cell.length))}cleanCell(cell,cellKeys){const cleaned={};return this.checkCellValue(cell),cellKeys.forEach((key=>{cleaned[key]=cell[key]})),cleaned}cleanList(items,allowedKeys){return items.map((item=>{const cleaned={};return allowedKeys.forEach((key=>{cleaned[key]=item[key]})),cleaned}))}validateModules(){const modules=_state.default.getValue("modules");let result=!0;return 0===modules.length?(result=!1,result):(modules.forEach((module=>{module.modulename&&""!==module.modulename.trim()||(module.error=!0),module.rows.forEach((row=>{row.deleted||(0===row.cells.length?(row.error=!0,result=!1):this.checkRow(row)||(result=!1))}))})),_state.default.setValue("modules",modules),result)}checkRow(row){const groups={};let result=!0;return row.cells.forEach((cell=>{cell.group&&(groups[cell.group]||(groups[cell.group]=[]),cell.value&&cell.value>0&&groups[cell.group].push(cell))})),Object.keys(groups).forEach((group=>{groups[group].length>1?(groups[group].forEach((cell=>{cell.error=!0})),row.error=!0,result=!1):0===groups[group].length?(row.error=!0,result=!1):(row.error=!1,row.error=!1)})),result}cleanModules(modules){const rowSpec=this.getRowObject().rows;return modules.map((module=>({moduleid:module.moduleid,modulename:module.modulename,modulesortorder:module.modulesortorder,deleted:module.deleted||!1,rows:module.rows.map((row=>({id:row.id,sortorder:row.sortorder,deleted:row.deleted||!1,cells:this.cleanList(row.cells,Object.keys(rowSpec.cells)),disciplines:this.cleanList(row.disciplines,Object.keys(rowSpec.disciplines)),competencies:this.cleanList(row.competencies,Object.keys(rowSpec.competencies))})))})))}async setTableData(){const pending=new _pending.default("customfield_sprogramme/manager:setTableData");(0,_utils.debounce)((async()=>{const saveConfirmButton=document.querySelector('[data-action="saveconfirm"]');if(saveConfirmButton.classList.add("saving"),!this.validateModules())return void pending.resolve();const modules=_state.default.getValue("modules"),cleanedModules=this.cleanModules(modules);if(await _repository.default.setData({datafieldid:this.datafieldid,modules:cleanedModules})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}else _notification.default.exception("No response from the server");pending.resolve(),setTimeout((()=>{saveConfirmButton.classList.remove("saving")}),200)}),600)()}actions(action,element){const actionMap={addrow:this.addRow,deleterow:this.deleteRow,addmodule:this.addModule,deletemodule:this.deleteModule,saveconfirm:this.setTableData,showchanges:this.showchanges,closechanges:this.closeChanges,acceptrfc:this.acceptRfc,rejectrfc:this.rejectRfc,submitrfc:this.submitRfc,cancelrfc:this.cancelRfc,removerfc:this.removeRfc,resetrfc:this.resetRfc,closeform:this.closeForm,hide:this.closeForm,downloadcsv:this.downloadCsv,augmenttable:this.augmentTable};actionMap[action]&&actionMap[action].call(this,element)}async addRow(btn){const modules=_state.default.getValue("modules");let rowid=btn.dataset.id;const moduleid=btn.closest('[data-region="module"]').dataset.id,rows=modules.find((m=>m.moduleid==moduleid)).rows;-1==rowid&&(rowid=rows[rows.length-1].id);const row=await this.createRow();row&&(rows.splice(rows.indexOf(rows.find((r=>r.id==rowid)))+1,0,row),this.resetRowSortorder(),_state.default.setValue("modules",modules))}createRow(){const row={};this.rowNumber=this.rowNumber-1,row.id=this.rowNumber;const columns=_state.default.getValue("columns");return row.cells=columns.map((column=>structuredClone(column))),row.cells.forEach((cell=>{cell.isnewcell=!0,cell.value=null,cell[cell.type]=!0,cell.oldvalue=null,cell.changed=!1})),row.disciplines=[],row.competencies=[],row}async deleteRow(btn){const modules=_state.default.getValue("modules"),rowid=parseInt(btn.closest("[data-row]").dataset.index),moduleid=parseInt(btn.closest('[data-region="module"]').dataset.id),modulefound=modules.find((m=>m.moduleid==moduleid));if(modulefound.rows.length>0){const rowIndex=modulefound.rows.findIndex((r=>r.id==rowid));-1!==rowIndex?(rowid>0?(modulefound.rows[rowIndex].deleted=!0,modulefound.rows[rowIndex].cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))):modulefound.rows.splice(rowIndex,1),_state.default.setValue("modules",modules)):_notification.default.exception("Row not found")}this.sumtotals()}change(input){const row=input.closest("[data-row]"),cell=input.closest("[data-cell]"),group=input.dataset.group,value=input.value,columnid=parseInt(cell.dataset.columnid),index=parseInt(row.dataset.index),modules=_state.default.getValue("modules");modules.forEach((module=>{const rowIndex=module.rows.findIndex((r=>r.id==index));if(-1===rowIndex)return;const cellIndex=module.rows[rowIndex].cells.findIndex((c=>c.columnid==columnid)),cell=module.rows[rowIndex].cells[cellIndex];cell.value=value||null,input.dataset.height&&(cell.height=input.dataset.height),group&&module.rows[rowIndex].cells.forEach((c=>{c.group===group&&c.columnid!==columnid&&null!==c.value&&(c.value=null)})),"select"==cell.type&&cell.options.forEach((option=>{option.selected=option.name===value}))})),this.markchanges(),this.sumtotals(),_state.default.setValue("modules",modules)}markchanges(){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.changed=cell.value!=cell.oldvalue}))}))})),_state.default.setValue("modules",modules)}sumtotals(){const columnsData=_state.default.getValue("columns");columnsData.forEach((column=>{column.sum=0,column.newsum=0,column.hasnewsum=!1,column.changed=!1}));const modules=_state.default.getValue("modules");let overaltotals=0,newsumtotals=0;modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{if("number"===cell.type||"float"===cell.type){const column=columnsData.find((c=>c.columnid===cell.columnid));column&&(cell.changed?column.sum=(parseFloat(column.sum)||0)+(parseFloat(cell.oldvalue)||0):cell.value&&null!==cell.value&&(column.sum=(parseFloat(column.sum)||0)+parseFloat(cell.value)),cell.value&&(column.newsum=(parseFloat(column.newsum)||0)+parseFloat(cell.value),column.hasnewsum=!0),0==column.sum&&column.newsum>0&&(column.hasnewsum=!0,column.sum=" 0")),cell.changed&&(column.changed=!0)}}))}))}));let totalsChanged=!1;columnsData.forEach((column=>{"number"!==column.type&&"float"!==column.type||(totalsChanged=totalsChanged||column.changed,overaltotals+=parseFloat(column.sum)||0,newsumtotals+=parseFloat(column.newsum)||0)})),columnsData[0].overaltotals=overaltotals,columnsData[0].newsumtotals=newsumtotals,columnsData[0].totalschanged=totalsChanged,_state.default.setValue("columns",columnsData)}changeModule(input){const moduleid=input.closest('[data-region="module"]').dataset.id,name=input.value;_state.default.getValue("modules").forEach((module=>{module.moduleid==moduleid&&(module.modulename=name)}))}async deleteModule(btn){const moduleid=btn.closest('[data-region="module"]').dataset.id,modules=_state.default.getValue("modules"),moduleIndex=modules.findIndex((m=>m.moduleid==moduleid));-1!==moduleIndex&&(modules[moduleIndex].deleted=!0,modules[moduleIndex].rows.forEach((row=>{row.deleted=!0,row.cells.forEach((cell=>{null===cell.value||"float"!=cell.type&&"number"!=cell.type||(cell.changed=!0,cell.value=null)}))})),_state.default.setValue("modules",modules))}createModule(){return this.moduleNumber=this.moduleNumber-1,this.moduleNumber}addModule(){const modules=_state.default.getValue("modules"),module={moduleid:this.createModule(),modulename:" ",deleted:!1,editor:!0,rows:[this.createRow()]};modules.push(module),this.resetRowSortorder(),_state.default.setValue("modules",modules)}getRow(rowid){return _state.default.getValue("modules").reduce(((acc,module)=>acc.concat(module.rows)),[]).find((r=>r.id==rowid))}moveRow(moduleId,rowId,prevRowId){const modules=_state.default.getValue("modules"),module=modules.find((m=>m.moduleid===moduleId));if(!module)return;const rows=module.rows,rowIndex=rows.findIndex((r=>r.id===rowId));if(-1===rowIndex)return;const[rowToMove]=rows.splice(rowIndex,1);let insertIndex=0;if(null!==prevRowId){insertIndex=rows.findIndex((r=>r.id===prevRowId))+1}rows.splice(insertIndex,0,rowToMove),rows.forEach(((row,index)=>{row.sortorder=index+1})),_state.default.setValue("modules",modules)}resetRowSortorder(){const modules=_state.default.getValue("modules");modules.forEach(((module,mindex)=>{module.modulesortorder=mindex,module.rows.forEach(((row,index)=>{row.sortorder=index}))})),_state.default.setValue("modules",modules)}async showchanges(btn){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td!==btn&&td.classList.remove("active")})),btn.classList.add("active")}async closeChanges(){document.querySelectorAll('[data-action="showchanges"]').forEach((td=>{td.classList.remove("active")}))}async acceptRfc(btn){const userid=btn.closest("[data-rfc]").dataset.userid;if(await _repository.default.acceptRfc({datafieldid:this.datafieldid,userid:userid})){await this.getTableData();const update=await _repository.default.getData({datafieldid:this.datafieldid,showrfc:0}),modulesStatic=this.parseModules(update);_state.default.setValue("modulesstatic",modulesStatic)}}async rejectRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:rejectRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.rejectRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async submitRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:submitRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.submitRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async cancelRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:cancelRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.cancelRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async removeRfc(btn){const pending=new _pending.default("customfield_sprogramme/manager:removeRFC"),userid=btn.closest("[data-rfc]").dataset.userid;await _repository.default.removeRfc({datafieldid:this.datafieldid,userid:userid})&&await this.getTableData(),pending.resolve()}async resetRfc(btn){document.querySelector('[data-region="app"]').querySelectorAll('[data-action="showchanges"]').forEach((cell=>{const input=cell.querySelector('[data-input="rfc"]');input&&(input.dataset.input="auto",input.value=input.dataset.oldvalue,input.dataset.rfcstate="0",cell.classList.remove("rfc"))})),this.sumtotals(),btn.classList.add("d-none")}async augmentTable(btn){const modules=_state.default.getValue("modules");modules.forEach((module=>{module.rows.forEach((row=>{row.cells.forEach((cell=>{cell.oldvalue!=cell.value?(cell.changes={oldvalue:cell.oldvalue?cell.oldvalue:"0",newvalue:cell.value},cell.changed=!0):(cell.changes=null,cell.changed=!1)}))}))})),_state.default.setValue("modules",modules),btn.classList.add("active")}async closeForm(){const modules=_state.default.getValue("modules"),rfc=_state.default.getValue("rfc"),event=new CustomEvent("closeform",{bubbles:!0,composed:!0});if(rfc&&rfc.issubmitted)return void document.dispatchEvent(event);const confirmationStrings=await(0,_str.getStrings)([{key:"confirm",component:"customfield_sprogramme"},{key:"unsavedchanges",component:"customfield_sprogramme"},{key:"closewithoutsaving",component:"customfield_sprogramme"},{key:"cancel",component:"customfield_sprogramme"}]);modules.some((module=>module.rows.some((row=>row.cells.some((cell=>{var _cell$isnewcell;return cell.changed||null!==(_cell$isnewcell=cell.isnewcell)&&void 0!==_cell$isnewcell&&_cell$isnewcell}))))))?_notification.default.confirm(...confirmationStrings,(()=>{document.dispatchEvent(event)}),(()=>{})):document.dispatchEvent(event)}async downloadCsv(){const csv=await _repository.default.csvData({datafieldid:this.datafieldid}),blob=new Blob([csv.csv],{type:"text/csv"}),url=window.URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=csv.filename,a.click(),window.URL.revokeObjectURL(url)}navigate(e){const currentIndex=e.target.closest("[data-row]").dataset.index,currentColumn=e.target.closest("[data-cell]").dataset.columnid,allRows=document.querySelectorAll("[data-row]");for(let i=0;i0){const previousInput=allRows[i-1].querySelector('[data-columnid="'.concat(currentColumn,'"] input'));previousInput&&previousInput.focus()}}if("ArrowRight"===e.key){const nextColumn=e.target.closest("[data-cell]").nextElementSibling;nextColumn&&nextColumn.focus()}if("ArrowLeft"===e.key){const previousColumn=e.target.closest("[data-cell]").previousElementSibling;previousColumn&&previousColumn.focus()}}}var _default={init:(element,datafieldid)=>{(0,_table.default)();const manager=new Manager(element,datafieldid);return(0,_programme_form.default)(manager),(0,_visa_form.default)(manager),manager}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=manager.min.js.map \ No newline at end of file diff --git a/amd/build/manager.min.js.map b/amd/build/manager.min.js.map index b8372d5..4b48204 100644 --- a/amd/build/manager.min.js.map +++ b/amd/build/manager.min.js.map @@ -1 +1 @@ -{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n if (response.visainfo) {\n State.setValue('visainfo', response.visainfo);\n }\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'acceptvisa': this.acceptVisa,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'rejectvisa': this.rejectVisa,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Directly remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reject the Visa.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectVisa(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectVisa');\n const rfcId = btn.dataset.rfcId;\n\n const response = await Repository.rejectVisa({rfcid: rfcId, comment: ''});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Accept the Visa.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptVisa(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectVisa');\n const rfcId = btn.dataset.rfcId;\n\n const response = await Repository.acceptVisa({rfcid: rfcId, comment: ''});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","visainfo","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","acceptVisa","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","rejectVisa","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcId","rfcid","comment","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"manager.min.js","sources":["../src/manager.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 * TODO describe module manager\n *\n * @module customfield_sprogramme/manager\n * @copyright 2024 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport State from 'customfield_sprogramme/local/state';\nimport Repository from 'customfield_sprogramme/local/repository';\nimport Notification from 'core/notification';\nimport {getStrings} from 'core/str';\nimport {debounce} from 'core/utils';\nimport componentInit from './local/components/table';\nimport Pending from 'core/pending'; // For Behat to make sure that async calls are finished.\nimport './tagmanager';\nimport initProgrammeForm from './programme_form';\nimport initVisaForm from \"./visa_form\";\n\n/**\n * Manager class.\n * @class\n */\nclass Manager {\n\n /**\n * Row number.\n */\n rowNumber = 0;\n\n /**\n * Module number.\n */\n moduleNumber = 0;\n\n /**\n * The datafieldid.\n * @type {Number}\n */\n datafieldid;\n\n /**\n * The element.\n * @type {HTMLElement}\n */\n element;\n\n /**\n * The table name.\n */\n table = 'customfield_sprogramme';\n\n /**\n * The table columns.\n * @type {Array}\n */\n columns = [];\n\n /**\n * Constructor.\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid.\n * @return {void}\n */\n constructor(element, datafieldid) {\n this.element = element;\n this.datafieldid = parseInt(datafieldid);\n this.addEventListeners();\n this.getTableData();\n }\n\n /**\n * Add event listeners.\n * @return {void}\n */\n addEventListeners() {\n document.addEventListener('click', (e) => {\n let btn = e.target.closest('.modal-customfield_sprogramme_editor [data-action]');\n if (btn) {\n e.preventDefault();\n this.actions(btn.dataset.action, btn);\n }\n\n });\n // Listen to all changes in the table.\n const form = document.querySelector('[data-region=\"app\"]');\n form.addEventListener('change', (e) => {\n const input = e.target.closest('[data-input=\"auto\"]');\n if (input) {\n this.change(input);\n }\n const modulename = e.target.closest('[data-region=\"modulename\"]');\n if (modulename) {\n this.changeModule(modulename);\n }\n });\n // Resize the textareas when needed.\n document.addEventListener('input', function(e) {\n if (e.target.tagName === 'TEXTAREA') {\n const textarea = e.target;\n // Resize the textarea to fit the content.\n textarea.style.height = 'auto'; // Reset height to auto to shrink if needed.\n textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content.\n textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute.\n }\n });\n // Listen to the arrow down and up keys to navigate to the next or previous row.\n form.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n this.navigate(e);\n e.preventDefault();\n }\n });\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n });\n\n let dragging = null;\n\n form.addEventListener('dragstart', (e) => {\n const handle = e.target.closest('[data-region=\"dragicon\"]');\n if (!handle) {\n e.preventDefault();\n return;\n }\n dragging = handle.closest('tr');\n e.dataTransfer.effectAllowed = 'move';\n });\n form.addEventListener('dragover', (e) => {\n e.preventDefault();\n const target = e.target.closest('tr');\n if (target && target !== dragging && target.parentNode.dataset.region === 'rows') {\n const rect = target.getBoundingClientRect();\n if (e.clientY - rect.top > rect.height / 2) {\n target.parentNode.insertBefore(dragging, target.nextSibling);\n } else {\n target.parentNode.insertBefore(dragging, target);\n }\n }\n });\n form.addEventListener(\"drop\", (e) => {\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n form.addEventListener('dragend', (e) => {\n const rowId = dragging.dataset.index;\n const prevRowId = dragging.previousElementSibling ? dragging.previousElementSibling.dataset.index : 0;\n const moduleId = dragging.closest('[data-region=\"module\"]').dataset.id;\n this.moveRow(parseInt(moduleId), parseInt(rowId), prevRowId ? parseInt(prevRowId) : null);\n dragging = null;\n e.preventDefault(); // Voorkom standaard drop-actie\n });\n }\n\n /**\n * Get the table data.\n * @return {void}\n */\n async getTableData() {\n try {\n const response = await Repository.getData({datafieldid: this.datafieldid, showrfc: 1});\n if (response.modules.length > 0) {\n const modules = this.parseModules(response);\n const columns = response.columns;\n\n State.setValue('columns', [...columns]);\n State.setValue('modules', modules);\n State.setValue('rfc', response.rfc ?? []);\n if (response.visainfo) {\n State.setValue('visainfo', response.visainfo);\n }\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.sumtotals();\n } else {\n const response = await Repository.getColumns({datafieldid: this.datafieldid});\n const columns = response.columns;\n State.setValue('columns', [...columns]);\n State.setValue('modules', []);\n State.setValue('rfc', []);\n State.setValue('editbuttons', {datafieldid: this.datafieldid, canedit: response.canedit});\n this.addModule();\n }\n } catch (error) {\n Notification.exception(error);\n }\n }\n\n /**\n * Parse the response, add the correct column properties to each cell.\n * @param {Array} response The response.\n * @return {Array} The parsed rows.\n */\n parseModules(response) {\n response.modules.forEach(mod => {\n mod.editor = response.canedit;\n mod.rows.map(row => {\n row.cells = row.cells.map(cell => {\n const column = response.columns.find(column => column.column == cell.column);\n // Clone the column properties to the cell but keep the cell properties.\n cell = Object.assign({}, cell, column);\n if (cell.type === 'select') {\n // Clone the options array to avoid shared references.\n cell.options = cell.options.map(option => {\n const clonedOption = Object.assign({}, option);\n if (clonedOption.name == cell.value) {\n clonedOption.selected = true;\n }\n return clonedOption;\n });\n }\n cell.changed = cell.value !== cell.oldvalue;\n return cell;\n });\n return row;\n });\n });\n return response.modules;\n }\n\n /**\n * Get the row object that can be accepted by the webservice.\n * @return {Array} The keys.\n */\n getRowObject() {\n return {\n 'rows': {\n 'id': 'id',\n 'sortorder': 'sortorder',\n 'deleted': 'false',\n 'cells': {\n 'type': 'type',\n 'column': 'column',\n 'value': 'value',\n 'group': 'group',\n 'oldvalue': 'oldvalue',\n },\n 'disciplines': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n 'competencies': {\n 'id': 'id',\n 'name': 'name',\n 'percentage': 'percentage',\n },\n },\n };\n }\n\n /**\n * Check the cell value. It can not exceed the cell length.\n * @param {object} cell The cell.\n * @return {void}\n */\n checkCellValue(cell) {\n if (cell.value === null) {\n return;\n }\n if (cell.type === 'text' && cell.value.length > cell.length) {\n cell.value = cell.value.substring(0, cell.length);\n }\n }\n\n /**\n * Clean a single cell.\n * @param {object} cell The cell to clean.\n * @param {Array} cellKeys The keys to keep in the cell.\n */\n cleanCell(cell, cellKeys) {\n const cleaned = {};\n this.checkCellValue(cell);\n cellKeys.forEach(key => {\n cleaned[key] = cell[key];\n });\n return cleaned;\n }\n\n /**\n * Clean a list of objects based on allowed keys.\n * @param {Array} items The items to clean.\n * @param {Array} allowedKeys The keys to keep in the items.\n */\n cleanList(items, allowedKeys) {\n return items.map(item => {\n const cleaned = {};\n allowedKeys.forEach(key => {\n cleaned[key] = item[key];\n });\n return cleaned;\n });\n }\n\n /**\n * Validate the modules.\n * @return {boolean} True if the modules are valid.\n */\n validateModules() {\n const modules = State.getValue('modules');\n let result = true;\n if (modules.length === 0) {\n result = false;\n return result;\n }\n modules.forEach(module => {\n if (!module.modulename || module.modulename.trim() === '') {\n module.error = true;\n }\n module.rows.forEach(row => {\n if (row.deleted) {\n return; // Skip deleted rows.\n }\n if (row.cells.length === 0) {\n row.error = true;\n result = false;\n } else {\n if (!this.checkRow(row)) {\n result = false;\n }\n }\n });\n });\n State.setValue('modules', modules);\n return result;\n }\n\n /**\n * Check the row, if there are grouped cells, only one can have a value. and one should have a value.\n * @param {Object} row The row to check.\n * @return {boolean} True if the row is valid.\n */\n checkRow(row) {\n const groups = {};\n let result = true;\n row.cells.forEach(cell => {\n if (cell.group) {\n if (!groups[cell.group]) {\n groups[cell.group] = [];\n }\n if (cell.value && cell.value > 0) {\n groups[cell.group].push(cell);\n }\n }\n });\n Object.keys(groups).forEach(group => {\n if (groups[group].length > 1) {\n // More than one cell in the group has a value.\n groups[group].forEach(cell => {\n cell.error = true;\n });\n row.error = true;\n result = false;\n } else if (groups[group].length === 0) {\n // No cell in the group has a value.\n row.error = true;\n result = false;\n } else {\n row.error = false;\n row.error = false;\n }\n });\n return result;\n }\n\n\n /**\n * Clean the Modules array.\n * @param {Array} modules The modules.\n * @return {Array} The cleaned modules.\n */\n cleanModules(modules) {\n const rowSpec = this.getRowObject().rows;\n\n return modules.map(module => {\n const cleanedModule = {\n moduleid: module.moduleid,\n modulename: module.modulename,\n modulesortorder: module.modulesortorder,\n deleted: module.deleted || false,\n rows: module.rows.map(row => {\n const cleanedRow = {\n id: row.id,\n sortorder: row.sortorder,\n deleted: row.deleted || false,\n cells: this.cleanList(row.cells, Object.keys(rowSpec.cells)),\n disciplines: this.cleanList(row.disciplines, Object.keys(rowSpec.disciplines)),\n competencies: this.cleanList(row.competencies, Object.keys(rowSpec.competencies)),\n };\n return cleanedRow;\n }),\n };\n return cleanedModule;\n });\n }\n\n /**\n * Set the table data.\n * @return {void}\n */\n async setTableData() {\n const pending = new Pending('customfield_sprogramme/manager:setTableData');\n const set = debounce(async() => {\n const saveConfirmButton = document.querySelector('[data-action=\"saveconfirm\"]');\n saveConfirmButton.classList.add('saving');\n if (!this.validateModules()) {\n pending.resolve();\n return;\n }\n const modules = State.getValue('modules');\n const cleanedModules = this.cleanModules(modules);\n const response = await Repository.setData({datafieldid: this.datafieldid, modules: cleanedModules});\n if (!response) {\n Notification.exception('No response from the server');\n } else {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n pending.resolve();\n setTimeout(() => {\n saveConfirmButton.classList.remove('saving');\n }, 200);\n }, 600);\n set();\n }\n\n /**\n * Actions.\n * @param {string} action The button that was clicked.\n * @param {HTMLElement|null} element The element that was clicked.\n */\n actions(action, element) {\n const actionMap = {\n 'addrow': this.addRow,\n 'deleterow': this.deleteRow,\n 'addmodule': this.addModule,\n 'deletemodule': this.deleteModule,\n 'saveconfirm': this.setTableData,\n 'showchanges': this.showchanges,\n 'closechanges': this.closeChanges,\n 'acceptrfc': this.acceptRfc,\n 'rejectrfc': this.rejectRfc,\n 'submitrfc': this.submitRfc,\n 'cancelrfc': this.cancelRfc,\n 'removerfc': this.removeRfc,\n 'resetrfc': this.resetRfc,\n 'closeform': this.closeForm,\n 'hide': this.closeForm,\n 'downloadcsv': this.downloadCsv,\n 'augmenttable': this.augmentTable,\n };\n if (actionMap[action]) {\n actionMap[action].call(this, element);\n }\n }\n\n /**\n * Inject a new row after this row.\n * @param {object} btn The button that was clicked.\n */\n async addRow(btn) {\n const modules = State.getValue('modules');\n\n let rowid = btn.dataset.id;\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const module = modules.find(m => m.moduleid == moduleid);\n const rows = module.rows;\n // When called from the link under the table, the rowid is not set.\n if (rowid == -1) {\n rowid = rows[rows.length - 1].id;\n }\n\n const row = await this.createRow();\n if (!row) {\n return;\n }\n // Inject the row after the clicked row.\n rows.splice(rows.indexOf(rows.find(r => r.id == rowid)) + 1, 0, row);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Create a new row.\n *\n * @return {Object} The row object.\n */\n createRow() {\n const row = {};\n this.rowNumber = this.rowNumber - 1;\n row.id = this.rowNumber;\n const columns = State.getValue('columns');\n // The copy the columns to the row and call them cells.\n row.cells = columns.map(column => structuredClone(column));\n // Set the correct types for the cells.\n row.cells.forEach(cell => {\n cell.isnewcell = true;\n cell.value = null;\n cell[cell.type] = true;\n cell.oldvalue = null;\n cell.changed = false;\n });\n row.disciplines = [];\n row.competencies = [];\n return row;\n }\n\n /**\n * Delete a row.\n * @param {Object} btn The button that was clicked.\n * @return {Promise} The promise.\n */\n async deleteRow(btn) {\n const modules = State.getValue('modules');\n const rowid = parseInt(btn.closest('[data-row]').dataset.index);\n const moduleid = parseInt(btn.closest('[data-region=\"module\"]').dataset.id);\n const modulefound = modules.find(m => m.moduleid == moduleid);\n if (modulefound.rows.length > 0) {\n // Find the row in the module.\n const rowIndex = modulefound.rows.findIndex(r => r.id == rowid);\n if (rowIndex !== -1) {\n // Add the deleted attribute to the row.\n if (rowid > 0) {\n modulefound.rows[rowIndex].deleted = true;\n // Set the changed attribute to each cell in the row.\n modulefound.rows[rowIndex].cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n } else {\n // Directly remove the row from the module.\n modulefound.rows.splice(rowIndex, 1);\n }\n State.setValue('modules', modules);\n } else {\n Notification.exception('Row not found');\n }\n }\n this.sumtotals();\n }\n\n /**\n * Change.\n * @param {object} input The input that was changed.\n */\n change(input) {\n const row = input.closest('[data-row]');\n const cell = input.closest('[data-cell]');\n const group = input.dataset.group;\n const value = input.value;\n const columnid = parseInt(cell.dataset.columnid);\n const index = parseInt(row.dataset.index);\n const modules = State.getValue('modules');\n modules.forEach(module => {\n // Find the correct cell in the row.\n const rowIndex = module.rows.findIndex(r => r.id == index);\n if (rowIndex === -1) {\n return;\n }\n const cellIndex = module.rows[rowIndex].cells.findIndex(c => c.columnid == columnid);\n const cell = module.rows[rowIndex].cells[cellIndex];\n cell.value = value ? value : null;\n if (input.dataset.height) {\n cell.height = input.dataset.height;\n }\n // Find the other cells with the same group and null the value.\n if (group) {\n module.rows[rowIndex].cells.forEach(c => {\n if (c.group === group && c.columnid !== columnid && c.value !== null) {\n c.value = null;\n }\n });\n }\n if (cell.type == 'select') {\n // Find the option that matches the value and set it as selected.\n cell.options.forEach(option => {\n option.selected = (option.name === value);\n });\n }\n });\n this.markchanges();\n this.sumtotals();\n State.setValue('modules', modules);\n }\n\n /**\n * Markchanges.\n * Mark the cells that have changed.\n */\n markchanges() {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n cell.changed = cell.value != cell.oldvalue;\n });\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Sumtotals.\n * Sum all columns.\n */\n sumtotals() {\n const columnsData = State.getValue('columns');\n // Reset the sum for all columns.\n columnsData.forEach(column => {\n column.sum = 0;\n column.newsum = 0;\n column.hasnewsum = false;\n column.changed = false;\n });\n const modules = State.getValue('modules');\n let overaltotals = 0;\n let newsumtotals = 0;\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.type === 'number' || cell.type === 'float') {\n const column = columnsData.find(c => c.columnid === cell.columnid);\n if (column) {\n if (cell.changed) {\n column.sum = (parseFloat(column.sum) || 0) + (parseFloat(cell.oldvalue) || 0);\n } else if (cell.value && cell.value !== null) {\n column.sum = (parseFloat(column.sum) || 0) + parseFloat(cell.value);\n }\n if (cell.value) {\n column.newsum = (parseFloat(column.newsum) || 0) + parseFloat(cell.value);\n column.hasnewsum = true;\n }\n if (column.sum == 0 && column.newsum > 0) {\n column.hasnewsum = true;\n column.sum = \" 0\"; // If the sum is 0 and the new sum is greater than 0, set the sum to 0.\n }\n }\n if (cell.changed) {\n column.changed = true;\n }\n }\n });\n });\n });\n let totalsChanged = false;\n columnsData.forEach(column => {\n if (column.type === 'number' || column.type === 'float') {\n totalsChanged = totalsChanged || column.changed;\n overaltotals += parseFloat(column.sum) || 0;\n newsumtotals += parseFloat(column.newsum) || 0;\n }\n });\n columnsData[0].overaltotals = overaltotals;\n columnsData[0].newsumtotals = newsumtotals;\n columnsData[0].totalschanged = totalsChanged;\n State.setValue('columns', columnsData);\n }\n\n /**\n * Change the module name.\n * @param {object} input The input that was changed.\n * @return {void}\n */\n changeModule(input) {\n const module = input.closest('[data-region=\"module\"]');\n const moduleid = module.dataset.id;\n const name = input.value;\n const modules = State.getValue('modules');\n modules.forEach(module => {\n if (module.moduleid == moduleid) {\n module.modulename = name;\n }\n });\n }\n\n /**\n * Delete a module.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async deleteModule(btn) {\n const moduleid = btn.closest('[data-region=\"module\"]').dataset.id;\n const modules = State.getValue('modules');\n const moduleIndex = modules.findIndex(m => m.moduleid == moduleid);\n if (moduleIndex !== -1) {\n // Add the deleted attribute to the module.\n modules[moduleIndex].deleted = true;\n modules[moduleIndex].rows.forEach(row => {\n row.deleted = true; // Mark all rows as deleted.\n row.cells.forEach(cell => {\n if (cell.value !== null && (cell.type == 'float' || cell.type == 'number')) {\n cell.changed = true;\n cell.value = null; // Clear the value.\n }\n });\n });\n State.setValue('modules', modules);\n }\n }\n\n /**\n * Create a new module.\n * @return {Integer} The module id.\n */\n createModule() {\n this.moduleNumber = this.moduleNumber - 1;\n return this.moduleNumber;\n }\n\n /**\n * Add a new module.\n * @return {void}\n */\n addModule() {\n const modules = State.getValue('modules');\n const moduleid = this.createModule();\n const row = this.createRow();\n const module = {\n moduleid: moduleid,\n modulename: ' ',\n deleted: false,\n editor: true,\n rows: [row],\n };\n modules.push(module);\n this.resetRowSortorder();\n State.setValue('modules', modules);\n }\n\n /**\n * Get the row from the state.\n * @param {int} rowid The rowid.\n */\n getRow(rowid) {\n const modules = State.getValue('modules');\n // Combine all rows in one array.\n const rows = modules.reduce((acc, module) => {\n return acc.concat(module.rows);\n }, []);\n const row = rows.find(r => r.id == rowid);\n return row;\n }\n\n /**\n * Move a row within a module to a new position, based on previd.\n * @param {Number} moduleId The module to update.\n * @param {Number} rowId The row to move.\n * @param {Number|null} prevRowId The row after which the moved row should appear. Null means move to top.\n */\n moveRow(moduleId, rowId, prevRowId) {\n const modules = State.getValue('modules');\n const module = modules.find(m => m.moduleid === moduleId);\n if (!module) {\n return;\n }\n\n const rows = module.rows;\n const rowIndex = rows.findIndex(r => r.id === rowId);\n if (rowIndex === -1) {\n return;\n }\n\n // Remove the row from its current position\n const [rowToMove] = rows.splice(rowIndex, 1);\n\n // Find index to insert after\n let insertIndex = 0;\n if (prevRowId !== null) {\n const prevIndex = rows.findIndex(r => r.id === prevRowId);\n insertIndex = prevIndex + 1;\n }\n\n // Insert the row\n rows.splice(insertIndex, 0, rowToMove);\n\n // Reset sortorders\n rows.forEach((row, index) => {\n row.sortorder = index + 1;\n });\n\n // Update the state\n State.setValue('modules', modules);\n }\n\n /**\n * Reset the row sortorder values.\n * @return {void}\n */\n resetRowSortorder() {\n const modules = State.getValue('modules');\n modules.forEach((module, mindex) => {\n module.modulesortorder = mindex;\n module.rows.forEach((row, index) => {\n row.sortorder = index;\n });\n });\n State.setValue('modules', modules);\n }\n\n /**\n * Show the changes.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async showchanges(btn) {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n if (td !== btn) {\n td.classList.remove('active');\n }\n });\n // Add the active class to the clicked button.\n btn.classList.add('active');\n }\n\n /**\n * Close the changes.\n * @return {void}\n */\n async closeChanges() {\n // Remove the active class from all buttons.\n const tds = document.querySelectorAll('[data-action=\"showchanges\"]');\n tds.forEach(td => {\n td.classList.remove('active');\n });\n }\n\n /**\n * Accept the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async acceptRfc(btn) {\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.acceptRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n const update = await Repository.getData({datafieldid: this.datafieldid, showrfc: 0});\n const modulesStatic = this.parseModules(update);\n State.setValue('modulesstatic', modulesStatic);\n }\n }\n\n /**\n * Reject the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async rejectRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:rejectRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.rejectRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Submit the RFC for approval.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async submitRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:submitRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.submitRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Cancel the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async cancelRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:cancelRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.cancelRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Remove the RFC.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async removeRfc(btn) {\n const pending = new Pending('customfield_sprogramme/manager:removeRFC');\n const userid = btn.closest('[data-rfc]').dataset.userid;\n const response = await Repository.removeRfc({datafieldid: this.datafieldid, userid: userid});\n if (response) {\n await this.getTableData();\n }\n pending.resolve();\n }\n\n /**\n * Reset the table to the original state.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async resetRfc(btn) {\n const form = document.querySelector('[data-region=\"app\"]');\n const changeCells = form.querySelectorAll('[data-action=\"showchanges\"]');\n changeCells.forEach(cell => {\n const input = cell.querySelector('[data-input=\"rfc\"]');\n if (input) {\n input.dataset.input = 'auto';\n input.value = input.dataset.oldvalue;\n input.dataset.rfcstate = '0';\n cell.classList.remove('rfc');\n }\n });\n this.sumtotals();\n btn.classList.add('d-none');\n }\n\n /**\n * Augment the table with additional data.\n * @param {object} btn The button that was clicked.\n * @return {void}\n */\n async augmentTable(btn) {\n const modules = State.getValue('modules');\n modules.forEach(module => {\n module.rows.forEach(row => {\n row.cells.forEach(cell => {\n if (cell.oldvalue != cell.value) {\n cell.changes = {\n oldvalue: cell.oldvalue ? cell.oldvalue : '0',\n newvalue: cell.value,\n };\n cell.changed = true;\n } else {\n cell.changes = null;\n cell.changed = false;\n }\n });\n });\n });\n State.setValue('modules', modules);\n // Add the augment class to the button.\n btn.classList.add('active');\n }\n\n /**\n * Send a closeform custom event.\n * @return {void}\n */\n async closeForm() {\n // Check if there are unsaved changes.\n const modules = State.getValue('modules');\n const rfc = State.getValue('rfc');\n const event = new CustomEvent('closeform', {\n bubbles: true,\n composed: true,\n });\n if (rfc && rfc.issubmitted) {\n document.dispatchEvent(event);\n return;\n }\n\n const confirmationStrings = await getStrings([\n {\n key: 'confirm',\n component: 'customfield_sprogramme',\n },\n {\n key: 'unsavedchanges',\n component: 'customfield_sprogramme',\n },\n {\n key: 'closewithoutsaving',\n component: 'customfield_sprogramme',\n },\n {\n key: 'cancel',\n component: 'customfield_sprogramme',\n },\n ]);\n\n const hasChanges = modules.some(module => module.rows.some(\n row => row.cells.some(cell => cell.changed || (cell.isnewcell ?? false))\n ));\n if (hasChanges) {\n Notification.confirm(\n ...confirmationStrings,\n () => {\n document.dispatchEvent(event);\n },\n () => {\n // Do nothing, the user cancelled the action.\n },\n );\n return;\n } else {\n document.dispatchEvent(event);\n }\n }\n\n /**\n * Download the table as a CSV file.\n * @return {void}\n */\n async downloadCsv() {\n const csv = await Repository.csvData({datafieldid: this.datafieldid});\n const blob = new Blob([csv.csv], {type: 'text/csv'});\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = csv.filename;\n a.click();\n window.URL.revokeObjectURL(url);\n }\n\n /**\n * Navigate to the next or previous row and left or right column.\n * @param {Event} e The event.\n * @return {void}\n */\n navigate(e) {\n const currentIndex = e.target.closest('[data-row]').dataset.index;\n const currentColumn = e.target.closest('[data-cell]').dataset.columnid;\n const allRows = document.querySelectorAll('[data-row]');\n for (let i = 0; i < allRows.length; i++) {\n if (allRows[i].dataset.index == currentIndex) {\n if (e.key === 'ArrowDown' && i < allRows.length - 1) {\n const nextInput = allRows[i + 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (nextInput) {\n nextInput.focus();\n }\n }\n if (e.key === 'ArrowUp' && i > 0) {\n const previousInput = allRows[i - 1].querySelector(`[data-columnid=\"${currentColumn}\"] input`);\n if (previousInput) {\n previousInput.focus();\n }\n }\n }\n }\n // This part is not working yet, it might not be accessible.\n if (e.key === 'ArrowRight') {\n const nextColumn = e.target.closest('[data-cell]').nextElementSibling;\n if (nextColumn) {\n nextColumn.focus();\n }\n }\n if (e.key === 'ArrowLeft') {\n const previousColumn = e.target.closest('[data-cell]').previousElementSibling;\n if (previousColumn) {\n previousColumn.focus();\n }\n }\n }\n}\n\n/*\n * Initialise\n * @param {HTMLElement} element The element.\n * @param {String} datafieldid The datafieldid\n * @return {Manager} The manager instance.\n */\nconst init = (element, datafieldid) => {\n componentInit();\n const manager = new Manager(element, datafieldid);\n initProgrammeForm(manager);\n initVisaForm(manager);\n return manager;\n};\n\nexport default {\n init: init,\n};\n"],"names":["Manager","constructor","element","datafieldid","parseInt","addEventListeners","getTableData","document","addEventListener","e","btn","target","closest","preventDefault","actions","dataset","action","form","querySelector","input","change","modulename","changeModule","tagName","textarea","style","height","scrollHeight","key","navigate","dragging","handle","dataTransfer","effectAllowed","parentNode","region","rect","getBoundingClientRect","clientY","top","insertBefore","nextSibling","rowId","index","prevRowId","previousElementSibling","moduleId","id","moveRow","response","Repository","getData","this","showrfc","modules","length","parseModules","columns","setValue","rfc","visainfo","canedit","sumtotals","getColumns","addModule","error","exception","forEach","mod","editor","rows","map","row","cells","cell","column","find","Object","assign","type","options","option","clonedOption","name","value","selected","changed","oldvalue","getRowObject","checkCellValue","substring","cleanCell","cellKeys","cleaned","cleanList","items","allowedKeys","item","validateModules","State","getValue","result","module","trim","deleted","checkRow","groups","group","push","keys","cleanModules","rowSpec","moduleid","modulesortorder","sortorder","disciplines","competencies","pending","Pending","async","saveConfirmButton","classList","add","resolve","cleanedModules","setData","update","modulesStatic","setTimeout","remove","set","actionMap","addRow","deleteRow","deleteModule","setTableData","showchanges","closeChanges","acceptRfc","rejectRfc","submitRfc","cancelRfc","removeRfc","resetRfc","closeForm","downloadCsv","augmentTable","call","rowid","m","createRow","splice","indexOf","r","resetRowSortorder","rowNumber","structuredClone","isnewcell","modulefound","rowIndex","findIndex","columnid","cellIndex","c","markchanges","columnsData","sum","newsum","hasnewsum","overaltotals","newsumtotals","parseFloat","totalsChanged","totalschanged","moduleIndex","createModule","moduleNumber","getRow","reduce","acc","concat","rowToMove","insertIndex","mindex","querySelectorAll","td","userid","rfcstate","changes","newvalue","event","CustomEvent","bubbles","composed","issubmitted","dispatchEvent","confirmationStrings","component","some","confirm","csv","csvData","blob","Blob","url","window","URL","createObjectURL","a","createElement","href","download","filename","click","revokeObjectURL","currentIndex","currentColumn","allRows","i","nextInput","focus","previousInput","nextColumn","nextElementSibling","previousColumn","init","manager"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/visa_form.min.js b/amd/build/visa_form.min.js new file mode 100644 index 0000000..10dae7a --- /dev/null +++ b/amd/build/visa_form.min.js @@ -0,0 +1,10 @@ +define("customfield_sprogramme/visa_form",["exports","core_form/modalform","core/str","core/notification","./local/repository"],(function(_exports,_modalform,_str,_notification,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * TODO describe module programme_form + * + * @module customfield_sprogramme/programme_form + * @copyright 2025 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification),_repository=_interopRequireDefault(_repository);const showVisaForm=(rfcid,manager)=>{const modalForm=new _modalform.default({modalConfig:{title:(0,_str.get_string)("addvisa","customfield_sprogramme")},formClass:"\\customfield_sprogramme\\local\\form\\visa_validate_form",args:{rfcid:rfcid},saveButtonText:(0,_str.get_string)("save")});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(async event=>{if(event.detail.result){let response;response="approved"===event.detail.statuscode?await _repository.default.acceptVisa({rfcid:rfcid,comment:event.detail.comment}):await _repository.default.rejectVisa({rfcid:rfcid,comment:event.detail.comment}),response&&manager.getTableData()}else _notification.default.addNotification({type:"error",message:event.detail.errors.join("
")})})),modalForm.show()};var _default=manager=>{document.addEventListener("click",(event=>{if(!event.target.closest('[data-action="addvisa"]'))return;const button=event.target.closest('[data-action="addvisa"]');event.preventDefault();const rfcid=button.dataset.rfcid;showVisaForm(rfcid,manager)}))};return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=visa_form.min.js.map \ No newline at end of file diff --git a/amd/build/visa_form.min.js.map b/amd/build/visa_form.min.js.map new file mode 100644 index 0000000..38147da --- /dev/null +++ b/amd/build/visa_form.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"visa_form.min.js","sources":["../src/visa_form.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 * TODO describe module programme_form\n *\n * @module customfield_sprogramme/programme_form\n * @copyright 2025 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\nimport Notification from \"core/notification\";\nimport Repository from \"./local/repository\";\n\nconst initVisaForm = (manager) => {\n document.addEventListener('click', (event) => {\n if (!event.target.closest('[data-action=\"addvisa\"]')) {\n return;\n }\n const button = event.target.closest('[data-action=\"addvisa\"]');\n event.preventDefault();\n const rfcid = button.dataset.rfcid;\n showVisaForm(rfcid, manager);\n });\n};\n/**\n * Init the visa form.\n *\n * @param {number} rfcid\n * @param {Object} manager\n */\nconst showVisaForm = (rfcid, manager) => {\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('addvisa', 'customfield_sprogramme'),\n },\n formClass: '\\\\customfield_sprogramme\\\\local\\\\form\\\\visa_validate_form',\n args: {\n rfcid: rfcid,\n },\n saveButtonText: getString('save'),\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, async (event) => {\n if (event.detail.result) {\n let response;\n if (event.detail.statuscode === 'approved') {\n response = await Repository.acceptVisa({\n rfcid: rfcid, comment: event.detail.comment\n });\n } else {\n response = await Repository.rejectVisa({\n rfcid: rfcid, comment: event.detail.comment\n });\n }\n if (response) {\n manager.getTableData();\n }\n } else {\n Notification.addNotification({\n type: 'error',\n message: event.detail.errors.join('
')\n });\n }\n });\n modalForm.show();\n};\n\nexport default initVisaForm;"],"names":["showVisaForm","rfcid","manager","modalForm","ModalForm","modalConfig","title","formClass","args","saveButtonText","addEventListener","events","FORM_SUBMITTED","async","event","detail","result","response","statuscode","Repository","acceptVisa","comment","rejectVisa","getTableData","addNotification","type","message","errors","join","show","document","target","closest","button","preventDefault","dataset"],"mappings":";;;;;;;6OA6CMA,aAAe,CAACC,MAAOC,iBACnBC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,mBAAU,UAAW,2BAEhCC,UAAW,4DACXC,KAAM,CACFP,MAAOA,OAEXQ,gBAAgB,mBAAU,UAE9BN,UAAUO,iBAAiBP,UAAUQ,OAAOC,gBAAgBC,MAAAA,WACpDC,MAAMC,OAAOC,OAAQ,KACjBC,SAEAA,SAD4B,aAA5BH,MAAMC,OAAOG,iBACIC,oBAAWC,WAAW,CACnCnB,MAAOA,MAAOoB,QAASP,MAAMC,OAAOM,gBAGvBF,oBAAWG,WAAW,CACnCrB,MAAOA,MAAOoB,QAASP,MAAMC,OAAOM,UAGxCJ,UACAf,QAAQqB,0CAGCC,gBAAgB,CACzBC,KAAM,QACNC,QAASZ,MAAMC,OAAOY,OAAOC,KAAK,aAI9CzB,UAAU0B,qBAlDQ3B,UAClB4B,SAASpB,iBAAiB,SAAUI,YAC3BA,MAAMiB,OAAOC,QAAQ,wCAGpBC,OAASnB,MAAMiB,OAAOC,QAAQ,2BACpClB,MAAMoB,uBACAjC,MAAQgC,OAAOE,QAAQlC,MAC7BD,aAAaC,MAAOC"} \ No newline at end of file diff --git a/amd/src/manager.js b/amd/src/manager.js index c6f9a5f..27c8736 100644 --- a/amd/src/manager.js +++ b/amd/src/manager.js @@ -30,6 +30,7 @@ import componentInit from './local/components/table'; import Pending from 'core/pending'; // For Behat to make sure that async calls are finished. import './tagmanager'; import initProgrammeForm from './programme_form'; +import initVisaForm from "./visa_form"; /** * Manager class. @@ -446,7 +447,6 @@ class Manager { actions(action, element) { const actionMap = { 'addrow': this.addRow, - 'acceptvisa': this.acceptVisa, 'deleterow': this.deleteRow, 'addmodule': this.addModule, 'deletemodule': this.deleteModule, @@ -455,7 +455,6 @@ class Manager { 'closechanges': this.closeChanges, 'acceptrfc': this.acceptRfc, 'rejectrfc': this.rejectRfc, - 'rejectvisa': this.rejectVisa, 'submitrfc': this.submitRfc, 'cancelrfc': this.cancelRfc, 'removerfc': this.removeRfc, @@ -875,38 +874,6 @@ class Manager { pending.resolve(); } - /** - * Reject the Visa. - * @param {object} btn The button that was clicked. - * @return {void} - */ - async rejectVisa(btn) { - const pending = new Pending('customfield_sprogramme/manager:rejectVisa'); - const rfcId = btn.dataset.rfcId; - - const response = await Repository.rejectVisa({rfcid: rfcId, comment: ''}); - if (response) { - await this.getTableData(); - } - pending.resolve(); - } - - /** - * Accept the Visa. - * @param {object} btn The button that was clicked. - * @return {void} - */ - async acceptVisa(btn) { - const pending = new Pending('customfield_sprogramme/manager:rejectVisa'); - const rfcId = btn.dataset.rfcId; - - const response = await Repository.acceptVisa({rfcid: rfcId, comment: ''}); - if (response) { - await this.getTableData(); - } - pending.resolve(); - } - /** * Submit the RFC for approval. * @param {object} btn The button that was clicked. @@ -1122,6 +1089,7 @@ const init = (element, datafieldid) => { componentInit(); const manager = new Manager(element, datafieldid); initProgrammeForm(manager); + initVisaForm(manager); return manager; }; diff --git a/amd/src/visa_form.js b/amd/src/visa_form.js new file mode 100644 index 0000000..ebf01ef --- /dev/null +++ b/amd/src/visa_form.js @@ -0,0 +1,82 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * TODO describe module programme_form + * + * @module customfield_sprogramme/programme_form + * @copyright 2025 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import ModalForm from 'core_form/modalform'; +import {get_string as getString} from 'core/str'; +import Notification from "core/notification"; +import Repository from "./local/repository"; + +const initVisaForm = (manager) => { + document.addEventListener('click', (event) => { + if (!event.target.closest('[data-action="addvisa"]')) { + return; + } + const button = event.target.closest('[data-action="addvisa"]'); + event.preventDefault(); + const rfcid = button.dataset.rfcid; + showVisaForm(rfcid, manager); + }); +}; +/** + * Init the visa form. + * + * @param {number} rfcid + * @param {Object} manager + */ +const showVisaForm = (rfcid, manager) => { + const modalForm = new ModalForm({ + modalConfig: { + title: getString('addvisa', 'customfield_sprogramme'), + }, + formClass: '\\customfield_sprogramme\\local\\form\\visa_validate_form', + args: { + rfcid: rfcid, + }, + saveButtonText: getString('save'), + }); + modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, async (event) => { + if (event.detail.result) { + let response; + if (event.detail.statuscode === 'approved') { + response = await Repository.acceptVisa({ + rfcid: rfcid, comment: event.detail.comment + }); + } else { + response = await Repository.rejectVisa({ + rfcid: rfcid, comment: event.detail.comment + }); + } + if (response) { + manager.getTableData(); + } + } else { + Notification.addNotification({ + type: 'error', + message: event.detail.errors.join('
') + }); + } + }); + modalForm.show(); +}; + +export default initVisaForm; \ No newline at end of file diff --git a/classes/event/rfc_visa_updated.php b/classes/event/rfc_visa_updated.php new file mode 100644 index 0000000..f012a67 --- /dev/null +++ b/classes/event/rfc_visa_updated.php @@ -0,0 +1,70 @@ +. + +namespace customfield_sprogramme\event; + +use customfield_sprogramme\local\persistent\sprogramme_rfc; + +/** + * An rfc has been accepted. + * + * @package customfield_sprogramme + * @copyright 2023 - CALL Learning - Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class rfc_visa_updated extends \core\event\base { + /** + * Returns the event name. + * @return string + */ + public static function get_name() { + return get_string('event_rfc_visa_updated', 'customfield_sprogramme'); + } + + /** + * Returns the object id mapping. + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => sprogramme_rfc::TABLE, 'restore' => 'rfc']; + } + + /** + * Returns the event description. + * @return string + */ + public function get_description() { + return "The user with id {$this->userid} has updated the visa an rfc with id {$this->objectid}."; + } + + /** + * Returns the event URL. + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/customfield/field/sprogramme/rfc.php', ['id' => $this->objectid]); + } + + /** + * Initializes other event data. + * @return void + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = sprogramme_rfc::TABLE; + } +} diff --git a/classes/external/get_data.php b/classes/external/get_data.php index d986ca1..d486239 100644 --- a/classes/external/get_data.php +++ b/classes/external/get_data.php @@ -84,11 +84,7 @@ public static function execute(int $datafieldid, bool $showrfc = false): array { $data['modules'] = json_decode($rfcdata, true); $data['rfc'] = $rfcmanager->get_data(); $visamanager = new visa_manager($rfc->get('id')); - $data['visainfo'] = [ - 'canvisa' => $visamanager->can_visa($USER->id), - 'rfcid' => $rfc->get('id'), - 'visas' => $visamanager->get_visa_data(), - ]; + $data['visainfo'] = $visamanager->get_visa_data(); } } @@ -193,10 +189,13 @@ public static function execute_returns(): external_single_structure { 'visainfo' => new external_single_structure([ 'canvisa' => new external_value(PARAM_BOOL, 'Can apply a visa', VALUE_OPTIONAL), 'rfcid' => new external_value(PARAM_INT, 'RFC id', VALUE_OPTIONAL), + 'todo' => new external_value(PARAM_INT, 'Todo count', VALUE_OPTIONAL), + 'done' => new external_value(PARAM_INT, 'Done count', VALUE_OPTIONAL), + 'approved' => new external_value(PARAM_INT, 'Approved count', VALUE_OPTIONAL), + 'rejected' => new external_value(PARAM_INT, 'Rejected count', VALUE_OPTIONAL), 'visas' => new external_multiple_structure( new external_single_structure([ 'comment' => new external_value(PARAM_BOOL, 'Comment', VALUE_OPTIONAL), - 'status' => new external_value(PARAM_BOOL, 'Status', VALUE_OPTIONAL), 'statustext' => new external_value(PARAM_TEXT, 'Status text', VALUE_OPTIONAL), 'visauser' => new external_single_structure([ 'id' => new external_value(PARAM_INT, 'UserId', VALUE_REQUIRED), diff --git a/classes/local/api/notifications.php b/classes/local/api/notifications.php index c8e6bae..ccffdff 100644 --- a/classes/local/api/notifications.php +++ b/classes/local/api/notifications.php @@ -165,6 +165,10 @@ private static function get_recipients(string $type, int $datafieldid, array $co } } break; + case 'rfc_visa_all_done': + // For RFC visa all done, we want to send the email to the approvers. + $emails = $approveremails; + break; } return array_unique($emails); } diff --git a/classes/local/form/programme_upload_form.php b/classes/local/form/programme_upload_form.php index 8a3f374..edbdf7e 100644 --- a/classes/local/form/programme_upload_form.php +++ b/classes/local/form/programme_upload_form.php @@ -102,8 +102,8 @@ protected function check_access_for_dynamic_submission(): void { * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { - $cmid = $this->optional_param('cmid', null, PARAM_INT); - return new moodle_url('/mod/competvet/view.php', ['pagetype' => 'manageplanning', 'id' => $cmid, 'return' => true]); + $context = $this->get_context_for_dynamic_submission(); + return new moodle_url('/course/edit.php', ['id' => $context->instanceid]); } /** diff --git a/classes/local/form/visa_validate_form.php b/classes/local/form/visa_validate_form.php new file mode 100644 index 0000000..8ad7492 --- /dev/null +++ b/classes/local/form/visa_validate_form.php @@ -0,0 +1,142 @@ +. + +namespace customfield_sprogramme\local\form; + +use context; +use context_course; +use context_user; +use core_form\dynamic_form; +use core_text; +use csv_import_reader; +use customfield_sprogramme\local\importer\programme_importer; +use customfield_sprogramme\local\persistent\sprogramme_rfc; +use customfield_sprogramme\local\persistent\sprogramme_visa; +use customfield_sprogramme\local\programme_manager; +use customfield_sprogramme\local\visa_manager; +use customfield_sprogramme\utils; +use moodle_exception; +use moodle_url; + +/** + * Class planning_upload_form + * + * @package customfield_sprogramme + * @copyright 2025 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class visa_validate_form extends dynamic_form { + /** + * Process the form submission + * + * @return array + * @throws moodle_exception + */ + public function process_dynamic_submission(): array { + $data = $this->get_data(); + return [ + 'result' => true, + 'statuscode' => $data->status == sprogramme_rfc::RFC_ACCEPTED ? 'approved' : 'rejected', + 'comment' => $data->comment, + ]; + } + + /** + * Get context + * + * @return context + */ + protected function get_context_for_dynamic_submission(): context { + $rfcid = $this->optional_param('rfcid', null, PARAM_INT); + $rfc = sprogramme_rfc::get_record(['id' => $rfcid]); + return $rfc->get_context(); + } + + /** + * TODO, find a better capability + * + * @return void + * @throws moodle_exception + */ + protected function check_access_for_dynamic_submission(): void { + if (!has_capability('moodle/course:update', $this->get_context_for_dynamic_submission())) { + throw new moodle_exception('invalidaccess'); + } + } + + /** + * Get page URL + * + * @return moodle_url + */ + protected function get_page_url_for_dynamic_submission(): moodle_url { + $context = $this->get_context_for_dynamic_submission(); + return new moodle_url('/course/edit.php', ['id' => $context->instanceid]); + } + + /** + * Form definition + * + * @return void + */ + protected function definition() { + $mform = $this->_form; + $rfcid = $this->optional_param('rfcid', null, PARAM_INT); + $mform->addElement('hidden', 'rfcid', $rfcid); + $mform->setType('rfcid', PARAM_INT); + $mform->addElement('textarea', 'comment', get_string('comment', 'customfield_sprogramme')); + $mform->setType('comment', PARAM_TEXT); + // Add radio buttons for accept/reject. + $mform->addElement( + 'radio', + 'status', + '', + get_string('accept', 'customfield_sprogramme'), + sprogramme_rfc::RFC_ACCEPTED + ); + $mform->addElement( + 'radio', + 'status', + '', + get_string('reject', 'customfield_sprogramme'), + sprogramme_rfc::RFC_REJECTED + ); + $mform->setType('status', PARAM_INT); + } + + /** + * Set data for dynamic submission + * + * @return void + */ + public function set_data_for_dynamic_submission(): void { + global $USER; + $rfcid = $this->optional_param('rfcid', 0, PARAM_INT); + $defaultstatus = sprogramme_rfc::RFC_ACCEPTED; + $defaultcomment = ''; + $visa = sprogramme_visa::get_record(['rfcid' => $rfcid, 'visauser' => $USER->id]); + if (!empty($visa)) { + $defaultstatus = intval($visa->get('status')); + $defaultcomment = $visa->get('comment'); + } + $data = [ + 'rfcid' => $rfcid, + 'status' => $defaultstatus, + 'comment' => $defaultcomment, + ]; + parent::set_data((object) $data); + } +} diff --git a/classes/local/observers/rfc_visa_observer.php b/classes/local/observers/rfc_visa_observer.php new file mode 100644 index 0000000..f039cd1 --- /dev/null +++ b/classes/local/observers/rfc_visa_observer.php @@ -0,0 +1,52 @@ +. + +namespace customfield_sprogramme\local\observers; + +use customfield_sprogramme\event\rfc_accepted; +use customfield_sprogramme\event\rfc_created; +use customfield_sprogramme\event\rfc_submitted; +use customfield_sprogramme\event\rfc_visa_updated; +use customfield_sprogramme\local\api\notifications; +use customfield_sprogramme\local\visa_manager; + +/** + * Monitor event related to rfc (request for change) + * + * @package customfield_sprogramme + * @copyright 2025 - CALL Learning - Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class rfc_visa_observer { + + /** + * An rfc has been created. + * + * @param rfc_visa_updated $event + */ + public static function rfc_visa_updated(rfc_visa_updated $event): void { + $eventdata = $event->get_data(); + $userid = $eventdata['userid']; + $rfcid = $eventdata['objectid']; + $visamanager = new visa_manager($rfcid); + $visastodo = $visamanager->get_visas_total_todo(); + $visadone = $visamanager->get_visas_total_done(); + if ($visadone >= $visastodo) { + $datafieldid = $visamanager->get_datafield_id(); + notifications::add_notification('rfc_visa_all_done', $userid, $datafieldid); + } + } +} diff --git a/classes/local/persistent/sprogramme_rfc.php b/classes/local/persistent/sprogramme_rfc.php index 8b138f8..53cb0ba 100644 --- a/classes/local/persistent/sprogramme_rfc.php +++ b/classes/local/persistent/sprogramme_rfc.php @@ -16,6 +16,7 @@ namespace customfield_sprogramme\local\persistent; +use core\context; use core\persistent; use customfield_sprogramme\utils; use lang_string; @@ -188,4 +189,13 @@ public static function count_rfc(int $datafieldid = 0, int $type = self::RFC_SUB } return self::count_records($params); } + + /** + * Get the course id for this RFC. + * @return context + * @throws \coding_exception + */ + public function get_context(): context { + return utils::get_context_from_datafieldid($this->raw_get('datafieldid')); + } } diff --git a/classes/local/visa_manager.php b/classes/local/visa_manager.php index c07eb4a..ba4787d 100644 --- a/classes/local/visa_manager.php +++ b/classes/local/visa_manager.php @@ -32,10 +32,17 @@ class visa_manager { /** * The context identifier for the RFC. * - * @var context The context of the RFC. - */ + * @var context The context of the RFC. + */ private \context $context; + /** + * The datafield id for the RFC. + * + * @var int The datafield id of the RFC. + */ + private int $datafieldid; + /** * Constructor * @@ -45,11 +52,11 @@ public function __construct( /** @var int $rfcid */ private int $rfcid, ) { - if (!sprogramme_rfc::record_exists($rfcid) ) { + if (!sprogramme_rfc::record_exists($rfcid)) { throw new \moodle_exception('invalidrfcid', 'customfield_sprogramme'); } - $datafieldid = sprogramme_rfc::get_record(['id' => $rfcid])->get('datafieldid'); - $this->context = utils::get_context_from_datafieldid($datafieldid) ?? context_system::instance(); + $this->datafieldid = sprogramme_rfc::get_record(['id' => $rfcid])->get('datafieldid'); + $this->context = utils::get_context_from_datafieldid($this->datafieldid) ?? context_system::instance(); } /** @@ -67,22 +74,35 @@ public function get_visas(): array { * @return array */ public function get_visa_data(): array { + global $USER; $visas = $this->get_visas(); - $data = []; + $data = [ + 'canvisa' => $this->can_visa($USER->id), + 'rfcid' => $this->rfcid, + 'visas' => [], + 'todo' => $this->get_visas_total_todo(), + 'approved' => 0, + 'rejected' => 0, + ]; + foreach ($visas as $visa) { $visauser = \core_user::get_user($visa->get('visauser')); - - $data[] = [ + $status = $visa->get('status'); + $data['visas'][] = [ 'id' => $visa->get('id'), 'rfcid' => $visa->get('rfcid'), 'visauser' => [ 'id' => $visauser->id, 'fullname' => fullname($visauser), ], - 'status' => $visa->get('status'), 'statustext' => $visa->get_status_string(), 'timemodified' => $visa->get('timemodified'), ]; + if ($status == sprogramme_visa::STATUS_APPROVED) { + $data['approved']++; + } else if ($status == sprogramme_visa::STATUS_REJECTED) { + $data['rejected']++; + } } return $data; } @@ -105,12 +125,15 @@ public function can_visa($userid): bool { * @return bool */ public function accept_visa(int $userid, string $comment) { - $visa = $this->get_visa($userid); + $visa = $this->get_visa_for_user($userid); $visa->set('status', sprogramme_visa::STATUS_APPROVED); $visa->set('comment', $comment); + $visa->set('timemodified', time()); $visa->update(); + $this->trigger_visa_updated_event($visa); return true; } + /** * Accept a visa for the current RFC. * @@ -119,20 +142,46 @@ public function accept_visa(int $userid, string $comment) { * @return bool */ public function reject_visa(int $userid, string $comment) { - $visa = $this->get_visa($userid); + $visa = $this->get_visa_for_user($userid); $visa->set('status', sprogramme_visa::STATUS_REJECTED); $visa->set('comment', $comment); $visa->update(); + $this->trigger_visa_updated_event($visa); return true; } + /** + * Get the datafield id for the current RFC. + * + * @return int + */ + public function get_datafield_id() { + return $this->datafieldid; + } + + protected function trigger_visa_updated_event(sprogramme_visa $visa) { + // Now send an event. + $event = \customfield_sprogramme\event\rfc_visa_updated::create( + [ + 'context' => $this->context, + 'objectid' => $this->rfcid, + 'other' => [ + 'visaid' => $visa->get('id'), + 'status' => $visa->get('status'), + 'usercreated' => $visa->get('visauser'), + ], + ] + ); + $event->trigger(); + } + /** * Get or create a visa for a user and the current RFC. * * @param int $userid * @return sprogramme_visa */ - private function get_visa(int $userid): sprogramme_visa { + private function get_visa_for_user(int $userid): sprogramme_visa { $visa = sprogramme_visa::get_record(['rfcid' => $this->rfcid, 'visauser' => $userid]); if (!$visa) { $visa = new sprogramme_visa(0, (object) [ @@ -144,4 +193,39 @@ private function get_visa(int $userid): sprogramme_visa { } return $visa; } -} \ No newline at end of file + + /** + * Get the total number of visas that still need to be done for the current RFC. * + * This is based on the number of responsible users for the course. + * + * @return int + */ + public function get_visas_total_todo() { + // Count the responsible users. + if (!$this->context || !$this->context->instanceid || $this->context->contextlevel != CONTEXT_COURSE) { + return 0; + } + $responsibleusers = utils::get_responsible_visa_reviewer_for_course( + $this->context->instanceid + ); + $responsibleuserids = array_map(fn($user) => $user->id, $responsibleusers); + array_unique($responsibleuserids); + return count($responsibleuserids); + } + + /** + * Get the total number of visas that are not yet approved for the current RFC. + * + * @return int + */ + public function get_visas_total_done() { + $visas = $this->get_visas(); + $total = 0; + foreach ($visas as $visa) { + if ($visa->get('status') != sprogramme_visa::STATUS_APPROVED) { + $total++; + } + } + return $total; + } +} diff --git a/db/events.php b/db/events.php index 00760fb..3f87782 100644 --- a/db/events.php +++ b/db/events.php @@ -36,4 +36,8 @@ 'eventname' => '\customfield_sprogramme\event\rfc_accepted', 'callback' => \customfield_sprogramme\local\observers\rfc_observer::class . '::rfc_accepted', ], + [ + 'eventname' => '\customfield_sprogramme\event\rfc_visa_updated', + 'callback' => \customfield_sprogramme\local\observers\rfc_visa_observer::class . '::rfc_visa_updated', + ], ]; diff --git a/lang/en/customfield_sprogramme.php b/lang/en/customfield_sprogramme.php index e2717cc..801548e 100644 --- a/lang/en/customfield_sprogramme.php +++ b/lang/en/customfield_sprogramme.php @@ -27,19 +27,22 @@ $string['aas_help'] = 'Supervised Self-Learning: Teaching including sequences of individual autonomous learning where students use available teaching materials (and can obtain, upon request, occasional help from teachers) and self-evaluate (e-learning for example).'; $string['accept'] = 'Accept'; -$string['acceptvisa'] = 'Validate the modification'; $string['accepted'] = 'Accepted'; +$string['acceptvisa'] = 'Validate'; $string['addmodule'] = 'Add module'; $string['addrow'] = 'Add row'; +$string['addvisa'] = 'Add visa'; $string['alreadyset'] = 'Already set for this row.'; $string['approvalemail'] = 'Approval email'; $string['approvalemail_desc'] = 'Email address to send approval requests to. This is a comma separated list of email addresses.'; +$string['approved'] = 'Approved'; $string['cachedef_columntotals'] = 'Column totals'; $string['cachedef_programmedata'] = 'Programme data cache'; $string['cancel'] = 'Cancel'; $string['cancelrfc'] = 'Cancel change request'; $string['closewithoutsaving'] = 'Close without saving'; $string['cm_help'] = 'Main Lecture: Theoretical teaching given to a whole or partial group of students. Teaching can be with or without the aid of teaching materials, demonstration animals, or specimens. The essential characteristic is that there is no practical involvement of students in the material discussed. They listen and do not physically manipulate.'; +$string['comment'] = 'Comment'; $string['competencies'] = 'Competencies'; $string['competencies_help'] = 'This field indicates the competencies (1 to 3 maximum) from the national framework that are concerned by the session/exercise, and their respective percentages within the session. The sum must equal 100%.'; $string['competency:name'] = 'Name'; @@ -96,6 +99,22 @@

Best regards

EOF; $string['email:rfc_submitted:subject'] = '[Syllabus] Request for programme change for: {$a->coursename}'; +$string['email:rfc_visa_all_done'] = <<<'EOF' +

Hello,

+

All visas have been completed for the programme change request concerning the following course unit:

{$a->coursename}

+
    +
  • UC Responsible(s): [{$a->responsibles}]
  • +
  • Requester: {$a->requester}
  • +
  • Department: {$a->department}
  • +
+

The change request is now ready for final validation by the director of training.

+

To view the history of validated changes for this UC, please follow the link below and +click on the "History" button of the programme:

+{$a->programmelink}

+

Best regards

'; +EOF; +$string['email:rfc_visa_all_done:subject'] = '[Syllabus] All visas completed for programme change request for: {$a->coursename}'; $string['emailsenabled'] = 'Activate email notifications'; $string['emailsenabled_desc'] = 'If enabled, email notifications will be sent when a change request is submitted.'; $string['encoding'] = 'Encoding'; @@ -121,6 +140,7 @@ $string['notifications'] = 'Notifications'; $string['overaltotals'] = 'Overall totals'; $string['overaltotals_help'] = 'Total of all columns in the table. This is the sum of all the columns for each row.'; +$string['pending'] = 'Pending'; $string['perso_ap_help'] = 'Estimated personal work time needed to assimilate the session/exercise. This work time includes the time spent revising for the mid-term assessment and/or final exam.'; $string['perso_av_help'] = 'Estimated personal work time needed to prepare in advance for the session/exercise. This work time includes, among other things, the time spent completing prerequisite self-assessments before the session.'; $string['pluginname'] = 'Programme customfield'; @@ -150,7 +170,7 @@ $string['programme:usermodified'] = 'Modified by'; $string['reject'] = 'Reject'; $string['rejected'] = 'Rejected'; -$string['rejectvisa'] = 'Do not validate the modification'; +$string['rejectvisa'] = 'Reject'; $string['removerfc'] = 'Reset all changes'; $string['report:competencies'] = 'Competencies Report'; $string['report:disciplines'] = 'Disciplines Report'; @@ -185,8 +205,8 @@ $string['rfc:selectstatus'] = 'Select status'; $string['rfc:status'] = 'Status'; $string['rfc:submitted'] = 'Submitted'; -$string['rfc:type'] = 'Modification type'; $string['rfc:timecreated'] = 'Time created'; +$string['rfc:type'] = 'Modification type'; $string['rfc:unknown'] = 'Unknown status'; $string['rfc:user'] = 'Submitted by'; $string['rfc:validator'] = 'Validator'; @@ -202,9 +222,15 @@ $string['supports_help'] = 'This field indicates the essential teaching materials needed for preparing for the session/exercise and for revision. Only the teaching materials listed in this field are considered essential. If it is not essential, it is only optional and complementary.'; $string['tc_help'] = 'Clinical Work: Practical teaching sessions performed by students in a clinical environment (individual or collective medicine) including clinical rotations both in-house and off-site (including ambulatory) under the supervision of a teacher, and autopsy.'; $string['td_help'] = 'Directed Work: Teaching sessions where students work alone or in teams on theoretical aspects, prepared from documents, articles, etc. Students reflect and interact on concepts. The session is animated by exercises, discussions, and, if possible, case studies (problem-solving learning for example).'; +$string['total'] = 'Total'; $string['tp_help'] = 'Practical Work non-clinical: Teaching sessions where students themselves manipulate teaching resources (software, microscopes, lab experiments, etc.) without handling animals, organs, or mannequins.'; $string['tpa_help'] = 'Practical Work on healthy animals: Teaching sessions where students work themselves on healthy animals, anatomical parts, mannequins, carcasses, etc. (for example: ante mortem and post mortem inspection, food hygiene, etc.). All VetSims activities are included in this category.'; $string['unsavedchanges'] = 'You have unsaved changes. Do you want to close the form without saving?'; $string['uploadcsv'] = 'Upload CSV file'; $string['usernotfound'] = 'User not found'; $string['value'] = 'Value'; +$string['visa'] = 'Visa'; +$string['visa_help'] = 'When a programme change request is submitted, it must be validated by the relevant department head and +the course leader before the changes are approved by the administrators. By clicking "Validate", you certify that the +proposed changes comply with pedagogical and administrative requirements, and that you approve their +implementation in the curriculum.'; diff --git a/lang/fr/customfield_sprogramme.php b/lang/fr/customfield_sprogramme.php index 6d14771..1438646 100644 --- a/lang/fr/customfield_sprogramme.php +++ b/lang/fr/customfield_sprogramme.php @@ -27,19 +27,22 @@ $string['aas_help'] = 'Auto-Apprentissage Supervisé : Enseignement comprenant des séquences d’apprentissage individuel en autonomie où les élèves utilisent un matériel pédagogique disponible (et peuvent obtenir, à leur demande, une aide ponctuelle des enseignants) et s\'auto-évaluent (e-learning par exemple).'; $string['accept'] = 'Accepter'; -$string['acceptvisa'] = 'Valider la modification'; $string['accepted'] = 'Accepté'; +$string['acceptvisa'] = 'Valider'; $string['addmodule'] = 'Ajouter un module'; $string['addrow'] = 'Ajouter une ligne'; +$string['addvisa'] = 'Ajouter visa'; $string['alreadyset'] = 'Déjà définie pour cette ligne.'; $string['approvalemail'] = 'Adresse e-mail d’approbation'; $string['approvalemail_desc'] = 'Adresse e-mail à laquelle envoyer les demandes d’approbation. Utilisez une liste séparée par des virgules.'; +$string['approved'] = 'Approuvé'; $string['cachedef_columntotals'] = 'Totaux des colonnes'; $string['cachedef_programmedata'] = 'Cache des données du programme'; $string['cancel'] = 'Annuler'; $string['cancelrfc'] = 'Annuler la demande de modification'; $string['closewithoutsaving'] = 'Fermer sans enregistrer'; $string['cm_help'] = 'Cours Magistral : Enseignement théorique dispensé à un groupe entier ou partiel d\'étudiants. L\'enseignement peut être avec ou sans l\'aide de matériel pédagogique, d\'animaux de démonstration ou de spécimens. La caractéristique essentielle est qu\'il n\'y a pas d\'implication pratique des étudiants dans le matériel discuté. Ils écoutent et ne manipulent pas physiquement.'; +$string['comment'] = 'Commentaire'; $string['competencies'] = 'Compétences'; $string['competencies_help'] = 'Cette case indique les compétences (de 1 à 3 maximum) du référentiel national qui sont concernées par la séance / l’exercice, et leurs % respectifs au sein de la séance. La somme doit faire 100%.'; $string['competency:name'] = 'Nom'; @@ -101,7 +104,21 @@

Bien cordialement

EOF; $string['email:rfc_submitted:subject'] = '[Syllabus] Demande de modification de programme pour l\'UC :{$a->coursename}'; - +$string['email:rfc_visa_all_done:subject'] = '[Syllabus] Tous les visas complétés pour la demande de modification de programme pour : {$a->coursename}'; +$string['email:rfc_visa_all_done'] = <<<'EOF' +

Bonjour,

+

Tous les visas ont été complétés pour la demande de modification de programme concernant l’unité d’enseignement suivante :

+

{$a->coursename}

+
    +
  • Responsable(s) de l’UC : [{$a->responsibles}]
  • +
  • Demandeur : {$a->requester}
  • +
  • Département : {$a->department}
  • +
+

La demande de modification est maintenant prête pour la validation finale par le directeur des formations.

+

Pour consulter l’historique des modifications validées pour cette UC, veuillez suivre le lien ci-dessous et cliquer sur le bouton "Historique" du programme :

+{$a->programmelink}

+

Bien cordialement

+EOF; $string['emailsenabled'] = 'Activer les notifications par e-mail'; $string['emailsenabled_desc'] = 'Si activé, les notifications par e-mail seront envoyées lors de la soumission de demandes de modification de programme.'; $string['encoding'] = 'Encodage'; @@ -127,6 +144,7 @@ $string['notifications'] = 'Notifications'; $string['overaltotals'] = 'Totaux'; $string['overaltotals_help'] = 'Total de toutes les colonnes du tableau. Il s’agit de la somme de toutes les colonnes pour chaque ligne.'; +$string['pending'] = 'En attente'; $string['perso_ap_help'] = 'Temps de travail personnel estimé nécessaire pour assimiler la séance / l’exercice. Ce temps de travail inclut le temps passé à réviser pour l’évaluation intermédiaire et/ou l’examen final.'; $string['perso_av_help'] = 'Temps de travail personnel estimé nécessaire pour préparer en amont la séance / l’exercice. Ce temps de travail inclut entre autres le temps passé à réaliser des auto-évaluations de pré-requis avant la séance.'; $string['pluginname'] = 'Champ personnalisé Programme'; @@ -156,7 +174,7 @@ $string['programme:usermodified'] = 'Modifié par'; $string['reject'] = 'Rejeter'; $string['rejected'] = 'Rejeté'; -$string['rejectvisa'] = 'Ne pas valider la modification'; +$string['rejectvisa'] = 'Reject'; $string['removerfc'] = 'Réinitialiser toutes les modifications'; $string['report:competencies'] = 'Rapport des compétences'; $string['report:disciplines'] = 'Rapport des disciplines'; @@ -195,8 +213,8 @@ $string['rfc:type'] = 'Type de modification'; $string['rfc:unknown'] = 'Statut inconnu'; $string['rfc:user'] = 'Soumission par'; -$string['rfc:view'] = 'Voir'; $string['rfc:validator'] = 'Validateur'; +$string['rfc:view'] = 'Voir'; $string['rfcs'] = 'Demandes {$a}'; $string['row'] = 'Ligne {$a}'; $string['saving'] = 'Enregistrement...'; @@ -208,9 +226,15 @@ $string['supports_help'] = 'Cette case renseigne sur les supports pédagogiques indispensables à la préparation de la séance / l’exercice et à sa révision. Seul le matériel pédagogique listé dans cette case est considéré comme indispensable. S’il ne l’est pas, il n’est que facultatif et complémentaire.'; $string['tc_help'] = 'Travaux Cliniques : Séances d\'enseignement pratique effectuées par les étudiants dans un environnement clinique (médecine individuelle ou collective) incluant les rotations cliniques intra et extra-muros (dont ambulante) sous la supervision d’un enseignant, et l’autopsie.'; $string['td_help'] = 'Travaux Dirigés : Séances d’enseignement dirigé au cours desquelles les étudiants travaillent seuls ou en équipe sur des aspects théoriques, préparés à partir de documents, d’articles, etc. Les étudiants réfléchissent et interagissent sur des concepts. La séance est animée par des exercices, des discussions et, si possible, des études de cas (apprentissage par résolution de problèmes par exemple).'; +$string['total'] = 'Total'; $string['tp_help'] = 'Travaux Pratiques non cliniques : Séances d’enseignement où les étudiants manipulent eux-mêmes les ressources pédagogiques (logiciels, microscopes, expé en labo, etc) sans manipulation d’animaux, d’organes ou de mannequins.'; $string['tpa_help'] = 'TP sur animaux sains : Séances d’enseignement où les étudiants travaillent eux-mêmes sur des animaux sains, des pièces anatomiques, des mannequins, des carcasses, etc. (par exemple : inspection ante mortem et post mortem, hygiène alimentaire, etc.). Toutes les activités VetSims sont incluses dans cette catégorie.'; $string['unsavedchanges'] = 'Vous avez des modifications non enregistrées. Voulez-vous fermer le formulaire sans enregistrer ?'; $string['uploadcsv'] = 'Charger un fichier CSV'; $string['usernotfound'] = 'Utilisateur non trouvé'; $string['value'] = 'Valeur'; +$string['visa'] = 'Visa'; +$string['visa_help'] = 'Quand une demande de modification de programme est soumise, elle doit être validée par le chef +de département concerné et le responsable de cours avant que les modifications ne soient validées par les administrateur. +En cliquant sur "Valider", vous certifiez que les modifications proposées sont conformes aux exigences pédagogiques +et administratives, et que vous approuvez leur mise en œuvre dans le programme d’études.'; diff --git a/scss/styles.scss b/scss/styles.scss index 9ca153d..119a155 100644 --- a/scss/styles.scss +++ b/scss/styles.scss @@ -591,3 +591,9 @@ border-radius: 100%; } } + +.visainfo { + position:absolute; + top: 1em; + right:0.5em; +} \ No newline at end of file diff --git a/styles.css b/styles.css index e1d3cd2..0ed72ee 100644 --- a/styles.css +++ b/styles.css @@ -32,8 +32,7 @@ border-radius: 0; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } -.customfield-sprogramme .programm-input::placeholder, -.customfield-sprogramme .programm-input::-webkit-input-placeholder { +.customfield-sprogramme .programm-input::placeholder, .customfield-sprogramme .programm-input::-webkit-input-placeholder { color: #d7d7d8; } .customfield-sprogramme .programm-input:focus { @@ -162,7 +161,6 @@ .customfield-sprogramme .programm-table .deletepending .actions button .newvalue, .customfield-sprogramme .programm-table .deletepending .static .newvalue { text-decoration: none; - background-color: initial; } .customfield-sprogramme .programm-table .deletepending .actions button, .customfield-sprogramme .programm-table .deletepending .disciplines button, @@ -223,8 +221,7 @@ max-width: 170px; padding: 0.4rem; } -.customfield-sprogramme .programm-table th.float, -.customfield-sprogramme .programm-table th.int { +.customfield-sprogramme .programm-table th.float, .customfield-sprogramme .programm-table th.int { background-color: #f7f7f7; width: 55.55px; max-width: 55.55px; @@ -282,8 +279,7 @@ margin-left: 1rem; width: calc(100% - 1rem); } -.customfield-sprogramme.syllabuspage .programm-table th.float, -.customfield-sprogramme.syllabuspage .programm-table th.int, +.customfield-sprogramme.syllabuspage .programm-table th.float, .customfield-sprogramme.syllabuspage .programm-table th.int, .customfield-sprogramme.syllabuspage .programm-table td.float, .customfield-sprogramme.syllabuspage .programm-table td.int { width: 30px; @@ -298,8 +294,7 @@ .customfield-sprogramme.syllabuspage .programm-table tr.tags { display: none; } -.customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-disciplines, -.customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-competencies { +.customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-disciplines, .customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-competencies { display: table-row; } .customfield-sprogramme.syllabuspage .programm-table tr.show-disciplines, @@ -388,8 +383,7 @@ .customfield-sprogramme .tag-form .badge .btn { display: block; } -.customfield-sprogramme .tag-form .btn.btn-icon:hover, -.customfield-sprogramme .tag-form .btn.btn-icon:focus { +.customfield-sprogramme .tag-form .btn.btn-icon:hover, .customfield-sprogramme .tag-form .btn.btn-icon:focus { background-color: #ead9d9; } .customfield-sprogramme .tag-form input, @@ -500,4 +494,10 @@ width: 3rem; height: 3rem; border-radius: 100%; +} + +.visainfo { + position: absolute; + top: 1em; + right: 0.5em; } \ No newline at end of file diff --git a/templates/formfield.mustache b/templates/formfield.mustache index abb738c..54c6f6e 100644 --- a/templates/formfield.mustache +++ b/templates/formfield.mustache @@ -38,7 +38,7 @@
-
+
diff --git a/templates/table/visainfo.mustache b/templates/table/visainfo.mustache index e469d4f..48b6f9c 100644 --- a/templates/table/visainfo.mustache +++ b/templates/table/visainfo.mustache @@ -36,22 +36,98 @@ ] } }} -
+
{{#visainfo}} - {{#visas}} -
- {{#str}}visa:changerequestby, customfield_sprogramme, {{visauser.fullname}}{{/str}} -

{{#str}} - submitdate, customfield_sprogramme{{/str}} {{#userdate}} {{timemodified}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}

-
- {{/visas}} - {{#canvisa}} - - - {{/canvisa}} +
+ +
+
+ {{#visas}} +
+
+
+ {{visauser.fullname}} +
+
+ ({{{statustext}}}) +
+
+ {{#userdate}} {{timemodified}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} +
+
+ + {{#comment}} +
+ {{comment}} +
+ {{/comment}} +
+ {{/visas}} + +
+ {{#canvisa}} + + {{/canvisa}} +
{{/visainfo}}
diff --git a/tests/local/observers/rfc_visa_observer_test.php b/tests/local/observers/rfc_visa_observer_test.php new file mode 100644 index 0000000..24b0e47 --- /dev/null +++ b/tests/local/observers/rfc_visa_observer_test.php @@ -0,0 +1,135 @@ +. +namespace customfield_sprogramme\local\observers; + +use core_customfield\data_controller; +use customfield_sprogramme\local\persistent\notification; +use customfield_sprogramme\local\persistent\sprogramme_rfc; +use customfield_sprogramme\local\visa_manager; + +/** + * + * RFC Visa observer test class. + * + * @package customfield_sprogramme + * @copyright 2025 - CALL Learning - Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \customfield_sprogramme\local\observers\rfc_observer + */ +final class rfc_visa_observer_test extends \advanced_testcase { + /** + * Custom field data + * + * @var data_controller $cfdata ; + */ + protected data_controller $cfdata; + + /** + * Course data + * + * @var \stdClass $course ; + */ + protected \stdClass $course; + + /** + * Setup test environment + */ + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $cfgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); + $cfcat = $cfgenerator->create_category(); + + $cfield = $cfgenerator->create_field( + ['categoryid' => $cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'sprogramme'] + ); + $this->course = $this->getDataGenerator()->create_course(); + $this->cfdata = $cfgenerator->add_instance_data($cfield, $this->course->id, 1); + set_config('emailsenabled', true, 'customfield_sprogramme'); + set_config('approvalemail', 'admin@example.com,otheruser@example.com', 'customfield_sprogramme'); + set_config('defaultlang', 'en', 'customfield_sprogramme'); + } + + /** + * Test that a notification is created when all visas are completed. + */ + public function test_rfc_visa_all_done_notification_created(): void { + $generator = $this->getDataGenerator(); + $pgenerator = $this->getDataGenerator()->get_plugin_generator('customfield_sprogramme'); + $creator = $generator->create_user(['username' => 'creator1']); + + $rfc = $pgenerator->create_rfc( + $this->cfdata->get('id'), + type: sprogramme_rfc::RFC_SUBMITTED, + snapshot: '{}', + usercreated: $creator->id, + ); + + $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); + $generator->create_role( + [ + 'shortname' => $responsiblerolename, + 'name' => 'Responsible', + 'archetype' => 'editingteacher', + ] + ); + $responsible1 = $generator->create_and_enrol($this->course, $responsiblerolename, [ + 'username' => 'responsible1', + 'email' => 'responsible1@example.com', + ]); + $responsible2 = $generator->create_and_enrol($this->course, $responsiblerolename, [ + 'username' => 'responsible2', + 'email' => 'responsible2@example.com', + ]); + + $hoduser = $generator->create_user([ + 'username' => 'headofdepartment1', + 'email' => 'headofdepartment@example.com', + 'firstname' => 'Head of', + 'lastname' => 'Department', + ]); + $departmentheadrolename = get_config('customfield_sprogramme', 'departmentheadrolename'); + $hodroleid = $generator->create_role( + [ + 'shortname' => $departmentheadrolename, + 'name' => 'Head of Department', + 'archetype' => 'teacher', + ] + ); + $generator->role_assign($hodroleid, $hoduser->id, \context_coursecat::instance($this->course->category)); + + $visamanager = new visa_manager($rfc->id); + $this->assertCount(0, notification::get_records(['notification' => 'rfc_visa_all_done'])); + + $this->setUser($responsible1); + $visamanager->reject_visa($responsible1->id, 'No'); + $this->assertCount(0, notification::get_records(['notification' => 'rfc_visa_all_done'])); + + $this->setUser($responsible2); + $visamanager->reject_visa($responsible2->id, 'No'); + $this->assertCount(0, notification::get_records(['notification' => 'rfc_visa_all_done'])); + + $this->setUser($hoduser); + $visamanager->reject_visa($hoduser->id, 'No'); + + $notifications = notification::get_records(['notification' => 'rfc_visa_all_done']); + $this->assertCount(2, $notifications); + $recipients = array_map(fn($n) => $n->get('recipient'), $notifications); + $this->assertContains('admin@example.com', $recipients); + $this->assertContains('otheruser@example.com', $recipients); + } + +} diff --git a/tests/local/visa_manager_test.php b/tests/local/visa_manager_test.php new file mode 100644 index 0000000..bdf5d04 --- /dev/null +++ b/tests/local/visa_manager_test.php @@ -0,0 +1,198 @@ +. + +namespace customfield_sprogramme\local; + +use customfield_sprogramme\local\persistent\sprogramme_rfc; +use customfield_sprogramme\local\persistent\sprogramme_visa; + +/** + * Functional test for visa manager. + * + * @package customfield_sprogramme + * @copyright 2025 - CALL Learning - Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \customfield_sprogramme\local\visa_manager + */ +final class visa_manager_test extends \advanced_testcase { + /** + * @var \core_customfield\category_controller The custom field category controller. + */ + protected \core_customfield\category_controller $cfcat; + /** + * @var \core_customfield\field_controller The custom field instance. + */ + protected \core_customfield\field_controller $cfield; + + /** + * @var \core_customfield\data_controller The custom field data. + */ + protected \core_customfield\data_controller $cfdata; + /** + * @var \stdClass The course. + */ + protected \stdClass $course; + + /** + * Set up the test environment. + */ + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $cfgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); + $this->cfcat = $cfgenerator->create_category(); + $this->cfield = $cfgenerator->create_field( + ['categoryid' => $this->cfcat->get('id'), 'shortname' => 'myfield1', 'type' => 'sprogramme'] + ); + $this->course = $this->getDataGenerator()->create_course(); + $this->cfdata = $cfgenerator->add_instance_data($this->cfield, $this->course->id, 1); + $this->setUser($this->getDataGenerator()->create_user('user1')); + } + + /** + * Create a RFC for the current custom field data. + * + * @return \stdClass + */ + private function create_rfc(): \stdClass { + $pgenerator = $this->getDataGenerator()->get_plugin_generator('customfield_sprogramme'); + return $pgenerator->create_rfc($this->cfdata->get('id'), sprogramme_rfc::RFC_SUBMITTED, '{}'); + } + + /** + * Test invalid rfc id. + */ + public function test_invalid_rfcid(): void { + $this->expectException(\moodle_exception::class); + new visa_manager(999999); + } + + /** + * Test can_visa and datafield id. + */ + public function test_can_visa_and_datafield_id(): void { + $rfc = $this->create_rfc(); + $generator = $this->getDataGenerator(); + $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); + $generator->create_role( + [ + 'shortname' => $responsiblerolename, + 'name' => 'Responsible', + 'archetype' => 'editingteacher', + ] + ); + $responsible1 = $generator->create_and_enrol($this->course, $responsiblerolename); + $responsible2 = $generator->create_and_enrol($this->course, $responsiblerolename); + $regular = $generator->create_and_enrol($this->course); + + $visamanager = new visa_manager($rfc->id); + $this->assertTrue($visamanager->can_visa($responsible1->id)); + $this->assertTrue($visamanager->can_visa($responsible2->id)); + $this->assertFalse($visamanager->can_visa($regular->id)); + $this->assertEquals($this->cfdata->get('id'), $visamanager->get_datafield_id()); + } + + /** + * Test total todo using responsible and head of department role assignments. + */ + public function test_get_visas_total_todo(): void { + $rfc = $this->create_rfc(); + $generator = $this->getDataGenerator(); + + $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); + $generator->create_role( + [ + 'shortname' => $responsiblerolename, + 'name' => 'Responsible', + 'archetype' => 'editingteacher', + ] + ); + $generator->create_and_enrol($this->course, $responsiblerolename); + $generator->create_and_enrol($this->course, $responsiblerolename); + + $departmentheadrolename = get_config('customfield_sprogramme', 'departmentheadrolename'); + $hodroleid = $generator->create_role( + [ + 'shortname' => $departmentheadrolename, + 'name' => 'Head of Department', + 'archetype' => 'teacher', + ] + ); + $hoduser = $generator->create_user([ + 'username' => 'headofdepartment1', + 'email' => 'headofdepartment@example.com', + 'firstname' => 'Head of', + 'lastname' => 'Department', + ]); + $generator->role_assign($hodroleid, $hoduser->id, \context_coursecat::instance($this->course->category)); + + $visamanager = new visa_manager($rfc->id); + $this->assertEquals(3, $visamanager->get_visas_total_todo()); + } + + /** + * Test accept/reject and visa data. + */ + public function test_accept_reject_and_get_visa_data(): void { + $rfc = $this->create_rfc(); + $visamanager = new visa_manager($rfc->id); + + $user1 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => 'One']); + $user2 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => 'Two']); + $user3 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => 'Three']); + + $visamanager->accept_visa($user1->id, 'approved'); + $visamanager->reject_visa($user2->id, 'rejected'); + $visa = new sprogramme_visa(0, (object) [ + 'rfcid' => $rfc->id, + 'visauser' => $user3->id, + 'comment' => '', + 'status' => sprogramme_visa::STATUS_PENDING, + ]); + $visa->create(); + + $visas = $visamanager->get_visas(); + $this->assertCount(3, $visas); + + $visasbyuser = []; + foreach ($visas as $record) { + $visasbyuser[$record->get('visauser')] = $record; + } + $this->assertEquals(sprogramme_visa::STATUS_APPROVED, $visasbyuser[$user1->id]->get('status')); + $this->assertEquals('approved', $visasbyuser[$user1->id]->get('comment')); + $this->assertEquals(sprogramme_visa::STATUS_REJECTED, $visasbyuser[$user2->id]->get('status')); + $this->assertEquals('rejected', $visasbyuser[$user2->id]->get('comment')); + $this->assertEquals(sprogramme_visa::STATUS_PENDING, $visasbyuser[$user3->id]->get('status')); + + $this->assertEquals(2, $visamanager->get_visas_total_done()); + + $this->setUser($user1); + $data = $visamanager->get_visa_data(); + $this->assertEquals($rfc->id, $data['rfcid']); + $this->assertEquals(0, $data['todo']); + $this->assertEquals(1, $data['approved']); + $this->assertEquals(1, $data['rejected']); + $this->assertCount(3, $data['visas']); + + $user1data = array_values(array_filter( + $data['visas'], + fn($item) => $item['visauser']['id'] === $user1->id + )); + $this->assertCount(1, $user1data); + $this->assertEquals(fullname($user1), $user1data[0]['visauser']['fullname']); + $this->assertEquals(get_string('approved', 'customfield_sprogramme'), $user1data[0]['statustext']); + } +} From 78a59e07423cb66403c014eec69bf032f4d44a60 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Fri, 13 Feb 2026 07:15:49 +0100 Subject: [PATCH 13/15] Fix behat and phpunit tests Fix CI issues --- amd/build/visa_form.min.js.map | 2 +- amd/src/visa_form.js | 2 +- classes/external/get_data.php | 2 +- classes/local/observers/rfc_visa_observer.php | 1 - classes/local/visa_manager.php | 5 +++++ classes/privacy/provider.php | 3 ++- lang/en/customfield_sprogramme.php | 14 +++++++------- lang/fr/customfield_sprogramme.php | 2 +- styles.css | 15 ++++++++++----- tests/local/api/notifications_test.php | 5 +++-- tests/local/observers/rfc_observer_test.php | 11 ++++++++--- tests/local/observers/rfc_visa_observer_test.php | 2 +- 12 files changed, 40 insertions(+), 24 deletions(-) diff --git a/amd/build/visa_form.min.js.map b/amd/build/visa_form.min.js.map index 38147da..8b3a950 100644 --- a/amd/build/visa_form.min.js.map +++ b/amd/build/visa_form.min.js.map @@ -1 +1 @@ -{"version":3,"file":"visa_form.min.js","sources":["../src/visa_form.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 * TODO describe module programme_form\n *\n * @module customfield_sprogramme/programme_form\n * @copyright 2025 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\nimport Notification from \"core/notification\";\nimport Repository from \"./local/repository\";\n\nconst initVisaForm = (manager) => {\n document.addEventListener('click', (event) => {\n if (!event.target.closest('[data-action=\"addvisa\"]')) {\n return;\n }\n const button = event.target.closest('[data-action=\"addvisa\"]');\n event.preventDefault();\n const rfcid = button.dataset.rfcid;\n showVisaForm(rfcid, manager);\n });\n};\n/**\n * Init the visa form.\n *\n * @param {number} rfcid\n * @param {Object} manager\n */\nconst showVisaForm = (rfcid, manager) => {\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('addvisa', 'customfield_sprogramme'),\n },\n formClass: '\\\\customfield_sprogramme\\\\local\\\\form\\\\visa_validate_form',\n args: {\n rfcid: rfcid,\n },\n saveButtonText: getString('save'),\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, async (event) => {\n if (event.detail.result) {\n let response;\n if (event.detail.statuscode === 'approved') {\n response = await Repository.acceptVisa({\n rfcid: rfcid, comment: event.detail.comment\n });\n } else {\n response = await Repository.rejectVisa({\n rfcid: rfcid, comment: event.detail.comment\n });\n }\n if (response) {\n manager.getTableData();\n }\n } else {\n Notification.addNotification({\n type: 'error',\n message: event.detail.errors.join('
')\n });\n }\n });\n modalForm.show();\n};\n\nexport default initVisaForm;"],"names":["showVisaForm","rfcid","manager","modalForm","ModalForm","modalConfig","title","formClass","args","saveButtonText","addEventListener","events","FORM_SUBMITTED","async","event","detail","result","response","statuscode","Repository","acceptVisa","comment","rejectVisa","getTableData","addNotification","type","message","errors","join","show","document","target","closest","button","preventDefault","dataset"],"mappings":";;;;;;;6OA6CMA,aAAe,CAACC,MAAOC,iBACnBC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,mBAAU,UAAW,2BAEhCC,UAAW,4DACXC,KAAM,CACFP,MAAOA,OAEXQ,gBAAgB,mBAAU,UAE9BN,UAAUO,iBAAiBP,UAAUQ,OAAOC,gBAAgBC,MAAAA,WACpDC,MAAMC,OAAOC,OAAQ,KACjBC,SAEAA,SAD4B,aAA5BH,MAAMC,OAAOG,iBACIC,oBAAWC,WAAW,CACnCnB,MAAOA,MAAOoB,QAASP,MAAMC,OAAOM,gBAGvBF,oBAAWG,WAAW,CACnCrB,MAAOA,MAAOoB,QAASP,MAAMC,OAAOM,UAGxCJ,UACAf,QAAQqB,0CAGCC,gBAAgB,CACzBC,KAAM,QACNC,QAASZ,MAAMC,OAAOY,OAAOC,KAAK,aAI9CzB,UAAU0B,qBAlDQ3B,UAClB4B,SAASpB,iBAAiB,SAAUI,YAC3BA,MAAMiB,OAAOC,QAAQ,wCAGpBC,OAASnB,MAAMiB,OAAOC,QAAQ,2BACpClB,MAAMoB,uBACAjC,MAAQgC,OAAOE,QAAQlC,MAC7BD,aAAaC,MAAOC"} \ No newline at end of file +{"version":3,"file":"visa_form.min.js","sources":["../src/visa_form.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 * TODO describe module programme_form\n *\n * @module customfield_sprogramme/programme_form\n * @copyright 2025 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\nimport Notification from \"core/notification\";\nimport Repository from \"./local/repository\";\n\nconst initVisaForm = (manager) => {\n document.addEventListener('click', (event) => {\n if (!event.target.closest('[data-action=\"addvisa\"]')) {\n return;\n }\n const button = event.target.closest('[data-action=\"addvisa\"]');\n event.preventDefault();\n const rfcid = button.dataset.rfcid;\n showVisaForm(rfcid, manager);\n });\n};\n/**\n * Init the visa form.\n *\n * @param {number} rfcid\n * @param {Object} manager\n */\nconst showVisaForm = (rfcid, manager) => {\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('addvisa', 'customfield_sprogramme'),\n },\n formClass: '\\\\customfield_sprogramme\\\\local\\\\form\\\\visa_validate_form',\n args: {\n rfcid: rfcid,\n },\n saveButtonText: getString('save'),\n });\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, async(event) => {\n if (event.detail.result) {\n let response;\n if (event.detail.statuscode === 'approved') {\n response = await Repository.acceptVisa({\n rfcid: rfcid, comment: event.detail.comment\n });\n } else {\n response = await Repository.rejectVisa({\n rfcid: rfcid, comment: event.detail.comment\n });\n }\n if (response) {\n manager.getTableData();\n }\n } else {\n Notification.addNotification({\n type: 'error',\n message: event.detail.errors.join('
')\n });\n }\n });\n modalForm.show();\n};\n\nexport default initVisaForm;"],"names":["showVisaForm","rfcid","manager","modalForm","ModalForm","modalConfig","title","formClass","args","saveButtonText","addEventListener","events","FORM_SUBMITTED","async","event","detail","result","response","statuscode","Repository","acceptVisa","comment","rejectVisa","getTableData","addNotification","type","message","errors","join","show","document","target","closest","button","preventDefault","dataset"],"mappings":";;;;;;;6OA6CMA,aAAe,CAACC,MAAOC,iBACnBC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,mBAAU,UAAW,2BAEhCC,UAAW,4DACXC,KAAM,CACFP,MAAOA,OAEXQ,gBAAgB,mBAAU,UAE9BN,UAAUO,iBAAiBP,UAAUQ,OAAOC,gBAAgBC,MAAAA,WACpDC,MAAMC,OAAOC,OAAQ,KACjBC,SAEAA,SAD4B,aAA5BH,MAAMC,OAAOG,iBACIC,oBAAWC,WAAW,CACnCnB,MAAOA,MAAOoB,QAASP,MAAMC,OAAOM,gBAGvBF,oBAAWG,WAAW,CACnCrB,MAAOA,MAAOoB,QAASP,MAAMC,OAAOM,UAGxCJ,UACAf,QAAQqB,0CAGCC,gBAAgB,CACzBC,KAAM,QACNC,QAASZ,MAAMC,OAAOY,OAAOC,KAAK,aAI9CzB,UAAU0B,qBAlDQ3B,UAClB4B,SAASpB,iBAAiB,SAAUI,YAC3BA,MAAMiB,OAAOC,QAAQ,wCAGpBC,OAASnB,MAAMiB,OAAOC,QAAQ,2BACpClB,MAAMoB,uBACAjC,MAAQgC,OAAOE,QAAQlC,MAC7BD,aAAaC,MAAOC"} \ No newline at end of file diff --git a/amd/src/visa_form.js b/amd/src/visa_form.js index ebf01ef..0a41ddc 100644 --- a/amd/src/visa_form.js +++ b/amd/src/visa_form.js @@ -54,7 +54,7 @@ const showVisaForm = (rfcid, manager) => { }, saveButtonText: getString('save'), }); - modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, async (event) => { + modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, async(event) => { if (event.detail.result) { let response; if (event.detail.statuscode === 'approved') { diff --git a/classes/external/get_data.php b/classes/external/get_data.php index d486239..580592f 100644 --- a/classes/external/get_data.php +++ b/classes/external/get_data.php @@ -84,7 +84,7 @@ public static function execute(int $datafieldid, bool $showrfc = false): array { $data['modules'] = json_decode($rfcdata, true); $data['rfc'] = $rfcmanager->get_data(); $visamanager = new visa_manager($rfc->get('id')); - $data['visainfo'] = $visamanager->get_visa_data(); + $data['visainfo'] = $visamanager->get_visa_data(); } } diff --git a/classes/local/observers/rfc_visa_observer.php b/classes/local/observers/rfc_visa_observer.php index f039cd1..bffb0f2 100644 --- a/classes/local/observers/rfc_visa_observer.php +++ b/classes/local/observers/rfc_visa_observer.php @@ -31,7 +31,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rfc_visa_observer { - /** * An rfc has been created. * diff --git a/classes/local/visa_manager.php b/classes/local/visa_manager.php index ba4787d..6f0a2a2 100644 --- a/classes/local/visa_manager.php +++ b/classes/local/visa_manager.php @@ -159,6 +159,11 @@ public function get_datafield_id() { return $this->datafieldid; } + /** + * Trigger an event when a visa is updated. + * + * @param sprogramme_visa $visa + */ protected function trigger_visa_updated_event(sprogramme_visa $visa) { // Now send an event. $event = \customfield_sprogramme\event\rfc_visa_updated::create( diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index a8f1338..86b3e29 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -18,6 +18,7 @@ use core_customfield\data_controller; use core_customfield\privacy\customfield_provider; +use core_privacy\local\metadata\null_provider; use core_privacy\local\request\writer; /** @@ -27,7 +28,7 @@ * @copyright 2024 CALL Learning * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider implements customfield_provider, \core_privacy\local\metadata\null_provider { +class provider implements customfield_provider, null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. diff --git a/lang/en/customfield_sprogramme.php b/lang/en/customfield_sprogramme.php index 801548e..29733d6 100644 --- a/lang/en/customfield_sprogramme.php +++ b/lang/en/customfield_sprogramme.php @@ -130,7 +130,7 @@ $string['invaliddata'] = 'Invalid data: {$a}'; $string['invalidpagetype'] = 'Invalid page type'; $string['invalidvalue'] = 'Invalid value for column {$a->column}: {$a->value}'; -$string['maxdisciplines'] = 'You cannot add more'; +$string['maxdisciplines'] = 'You can not any more, max allowed reached'; $string['maxpercentage'] = 'Max allowed {$a} The sum of the percentages must be 100'; $string['module:name'] = 'Module Name'; $string['module:sortorder'] = 'Module'; @@ -147,18 +147,18 @@ $string['programme:aas'] = 'AAS'; $string['programme:cct_ept'] = 'CCT EPT'; $string['programme:cm'] = 'CM'; -$string['programme:consignes'] = 'Work instructions to prepare for the session'; +$string['programme:consignes'] = 'Instructions to prepare for the session'; $string['programme:datafieldid'] = 'Data field id'; $string['programme:dd_rse'] = 'DD / RSE'; $string['programme:enabled'] = '{$a} enabled'; -$string['programme:enabledbydefault'] = 'Programme enabled by default'; +$string['programme:enabledbydefault'] = 'Programme Enabled By default'; $string['programme:fmp'] = 'FMP'; -$string['programme:intitule_seance'] = 'Session/exercise title'; -$string['programme:perso_ap'] = 'Personal work after'; -$string['programme:perso_av'] = 'Personal work before'; +$string['programme:intitule_seance'] = 'Session title or exercise'; +$string['programme:perso_ap'] = 'Perso ap'; +$string['programme:perso_av'] = 'Perso av'; $string['programme:sequence'] = 'Sequence'; $string['programme:sortorder'] = 'Sort order'; -$string['programme:supports'] = 'Essential teaching supports'; +$string['programme:supports'] = 'Essential teaching materials'; $string['programme:tc'] = 'TC'; $string['programme:td'] = 'TD'; $string['programme:timecreated'] = 'Time created'; diff --git a/lang/fr/customfield_sprogramme.php b/lang/fr/customfield_sprogramme.php index 1438646..363c7bc 100644 --- a/lang/fr/customfield_sprogramme.php +++ b/lang/fr/customfield_sprogramme.php @@ -104,7 +104,6 @@

Bien cordialement

EOF; $string['email:rfc_submitted:subject'] = '[Syllabus] Demande de modification de programme pour l\'UC :{$a->coursename}'; -$string['email:rfc_visa_all_done:subject'] = '[Syllabus] Tous les visas complétés pour la demande de modification de programme pour : {$a->coursename}'; $string['email:rfc_visa_all_done'] = <<<'EOF'

Bonjour,

Tous les visas ont été complétés pour la demande de modification de programme concernant l’unité d’enseignement suivante :

@@ -119,6 +118,7 @@ {$a->programmelink}

Bien cordialement

EOF; +$string['email:rfc_visa_all_done:subject'] = '[Syllabus] Tous les visas complétés pour la demande de modification de programme pour : {$a->coursename}'; $string['emailsenabled'] = 'Activer les notifications par e-mail'; $string['emailsenabled_desc'] = 'Si activé, les notifications par e-mail seront envoyées lors de la soumission de demandes de modification de programme.'; $string['encoding'] = 'Encodage'; diff --git a/styles.css b/styles.css index 0ed72ee..c36c17a 100644 --- a/styles.css +++ b/styles.css @@ -32,7 +32,8 @@ border-radius: 0; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } -.customfield-sprogramme .programm-input::placeholder, .customfield-sprogramme .programm-input::-webkit-input-placeholder { +.customfield-sprogramme .programm-input::placeholder, +.customfield-sprogramme .programm-input::-webkit-input-placeholder { color: #d7d7d8; } .customfield-sprogramme .programm-input:focus { @@ -221,7 +222,8 @@ max-width: 170px; padding: 0.4rem; } -.customfield-sprogramme .programm-table th.float, .customfield-sprogramme .programm-table th.int { +.customfield-sprogramme .programm-table th.float, +.customfield-sprogramme .programm-table th.int { background-color: #f7f7f7; width: 55.55px; max-width: 55.55px; @@ -279,7 +281,8 @@ margin-left: 1rem; width: calc(100% - 1rem); } -.customfield-sprogramme.syllabuspage .programm-table th.float, .customfield-sprogramme.syllabuspage .programm-table th.int, +.customfield-sprogramme.syllabuspage .programm-table th.float, +.customfield-sprogramme.syllabuspage .programm-table th.int, .customfield-sprogramme.syllabuspage .programm-table td.float, .customfield-sprogramme.syllabuspage .programm-table td.int { width: 30px; @@ -294,7 +297,8 @@ .customfield-sprogramme.syllabuspage .programm-table tr.tags { display: none; } -.customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-disciplines, .customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-competencies { +.customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-disciplines, +.customfield-sprogramme.syllabuspage .programm-table tr.tags.show.has-competencies { display: table-row; } .customfield-sprogramme.syllabuspage .programm-table tr.show-disciplines, @@ -383,7 +387,8 @@ .customfield-sprogramme .tag-form .badge .btn { display: block; } -.customfield-sprogramme .tag-form .btn.btn-icon:hover, .customfield-sprogramme .tag-form .btn.btn-icon:focus { +.customfield-sprogramme .tag-form .btn.btn-icon:hover, +.customfield-sprogramme .tag-form .btn.btn-icon:focus { background-color: #ead9d9; } .customfield-sprogramme .tag-form input, diff --git a/tests/local/api/notifications_test.php b/tests/local/api/notifications_test.php index bc123f2..61d820b 100644 --- a/tests/local/api/notifications_test.php +++ b/tests/local/api/notifications_test.php @@ -239,7 +239,8 @@ public function test_add_global_context(): void { $cfgenerator->add_instance_data($cfielddept, $this->course->id, 'DSPB'); set_config('departmentcustomfieldname', 'newdept', 'customfield_sprogramme'); - // Responsible and department head roles are required for the context, so we create them and enrol some users with these roles. + // Responsible and department head roles are required for the context, so we create them and enrol + // some users with these roles. $responsiblerolename = get_config('customfield_sprogramme', 'responsiblerolename'); $generator->create_role( [ @@ -287,7 +288,7 @@ public function test_add_global_context(): void { ]); $context = $method->invoke( null, - ['usercreated' => $user->id,], + ['usercreated' => $user->id], $this->cfdata->get('id'), $user->id ); diff --git a/tests/local/observers/rfc_observer_test.php b/tests/local/observers/rfc_observer_test.php index 86399f6..1ebfc8d 100644 --- a/tests/local/observers/rfc_observer_test.php +++ b/tests/local/observers/rfc_observer_test.php @@ -209,8 +209,13 @@ public function test_rfc_accepted_email_sent(): void { '[Syllabus] Programme change validated for UC: tc_1 - Test course 1', $email->subject ); - $this->assertStringContainsString('UC Responsible(s): [Responsible One, Responsible Two]', $email->body); - $this->assertStringContainsString('Requester: Teacher One', $email->body); - $this->assertStringContainsString('Department: DSPB', $email->body); + $emailwithoutlinebreaks = str_replace(["\r", "\n"], ' ', $email->body); + $emailwithoutlinebreaks = preg_replace('/\s+/', ' ', $emailwithoutlinebreaks); + $this->assertStringContainsString('UC Responsible(s)', $emailwithoutlinebreaks); + $this->assertStringContainsString('Head of Department', $emailwithoutlinebreaks); + $this->assertStringContainsString('Responsible One', $emailwithoutlinebreaks); + $this->assertStringContainsString('Responsible Two', $emailwithoutlinebreaks); + $this->assertStringContainsString('Requester: Teacher One', $emailwithoutlinebreaks); + $this->assertStringContainsString('Department: DSPB', $emailwithoutlinebreaks); } } diff --git a/tests/local/observers/rfc_visa_observer_test.php b/tests/local/observers/rfc_visa_observer_test.php index 24b0e47..c83e41f 100644 --- a/tests/local/observers/rfc_visa_observer_test.php +++ b/tests/local/observers/rfc_visa_observer_test.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + namespace customfield_sprogramme\local\observers; use core_customfield\data_controller; @@ -131,5 +132,4 @@ public function test_rfc_visa_all_done_notification_created(): void { $this->assertContains('admin@example.com', $recipients); $this->assertContains('otheruser@example.com', $recipients); } - } From 6ca346cea1ab07bb9bd4a131436ea25d43e29d0a Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Mon, 16 Feb 2026 12:49:51 +0100 Subject: [PATCH 14/15] plan-801 re-edit rfc --- classes/local/rfc_manager.php | 34 ++++++++++++++++++++++-------- lang/en/customfield_sprogramme.php | 1 + lang/fr/customfield_sprogramme.php | 1 + templates/formfield.mustache | 2 +- templates/table/modules.mustache | 4 ++-- templates/table/rfc.mustache | 3 ++- templates/table/rows.mustache | 8 +++---- tests/local/rfc_manager_test.php | 2 +- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/classes/local/rfc_manager.php b/classes/local/rfc_manager.php index 5b83140..b8172d2 100644 --- a/classes/local/rfc_manager.php +++ b/classes/local/rfc_manager.php @@ -162,7 +162,7 @@ public function can_cancel(int $userid): bool { } $cancancel = has_capability('customfield/sprogramme:edit', $this->context); - $cancancel = $cancancel && $changerecord->get('type') == sprogramme_rfc::RFC_SUBMITTED; + $cancancel = $cancancel && ($changerecord->get('type') !== sprogramme_rfc::RFC_ACCEPTED); $cancancel = $cancancel && (($USER->id == $userid) || has_capability('customfield/sprogramme:editall', $this->context)); $cancancel = $cancancel && $changerecord->get('usercreated') == $userid; @@ -193,6 +193,9 @@ public function can_add(): bool { public function has_submitted(): bool { $changerecord = $this->get_current(); if ($changerecord) { + if ($changerecord->get('type') == sprogramme_rfc::RFC_CANCELLED) { + return false; // If the only rfc found is cancelled, we consider that there are no submitted rfcs. + } return true; // If there is a change record for the course, it means there are submitted rfcs. } return false; @@ -261,6 +264,16 @@ public function create(mixed $data): sprogramme_rfc { ] ); + if (!$rfc) { + $rfc = sprogramme_rfc::get_record( + [ + 'datafieldid' => $this->datafieldid, + 'usercreated' => $USER->id, + 'type' => sprogramme_rfc::RFC_CANCELLED, + ] + ); + } + if (!$rfc) { $rfc = new sprogramme_rfc(); $rfc->set('datafieldid', $this->datafieldid); @@ -272,6 +285,7 @@ public function create(mixed $data): sprogramme_rfc { } else { // If the rfc already exists, update the snapshot. $rfc->set('snapshot', json_encode($data)); + $rfc->set('type', sprogramme_rfc::RFC_REQUESTED); $rfc->save(); } return $rfc; @@ -286,14 +300,12 @@ public function create(mixed $data): sprogramme_rfc { public function cancel(int $userid): bool { global $USER; $result = false; - $record = sprogramme_rfc::get_record( - [ - 'datafieldid' => $this->datafieldid, - 'usercreated' => $userid, - 'type' => sprogramme_rfc::RFC_SUBMITTED, - ] - ); - if ($record) { + $record = $this->get_current($userid); + if ( + $record && ($record->get('type') == sprogramme_rfc::RFC_REQUESTED + || $record->get('type') == sprogramme_rfc::RFC_SUBMITTED + || $record->get('type') == sprogramme_rfc::RFC_REJECTED) + ) { $record->set('type', sprogramme_rfc::RFC_CANCELLED); $record->set('adminid', $USER->id); $record->save(); @@ -347,6 +359,10 @@ public function get_data(): array { $data = []; $usercreated = $changerecord->get('usercreated'); $issubmitted = $changerecord->get('type') == sprogramme_rfc::RFC_SUBMITTED; + $iscancelled = $changerecord->get('type') == sprogramme_rfc::RFC_CANCELLED; + if ($iscancelled) { + return []; + } $data['issubmitted'] = $issubmitted; $data['timemodified'] = $changerecord->get('timemodified'); diff --git a/lang/en/customfield_sprogramme.php b/lang/en/customfield_sprogramme.php index 29733d6..c05f702 100644 --- a/lang/en/customfield_sprogramme.php +++ b/lang/en/customfield_sprogramme.php @@ -168,6 +168,7 @@ $string['programme:type_ae'] = 'AE type'; $string['programme:uc'] = 'Course unit'; $string['programme:usermodified'] = 'Modified by'; +$string['reeditrfc'] = 'Continue editing'; $string['reject'] = 'Reject'; $string['rejected'] = 'Rejected'; $string['rejectvisa'] = 'Reject'; diff --git a/lang/fr/customfield_sprogramme.php b/lang/fr/customfield_sprogramme.php index 363c7bc..e06e0a8 100644 --- a/lang/fr/customfield_sprogramme.php +++ b/lang/fr/customfield_sprogramme.php @@ -172,6 +172,7 @@ $string['programme:type_ae'] = 'Type AE'; $string['programme:uc'] = 'UC'; $string['programme:usermodified'] = 'Modifié par'; +$string['reeditrfc'] = 'Continuer à modifier'; $string['reject'] = 'Rejeter'; $string['rejected'] = 'Rejeté'; $string['rejectvisa'] = 'Reject'; diff --git a/templates/formfield.mustache b/templates/formfield.mustache index 54c6f6e..da6ebad 100644 --- a/templates/formfield.mustache +++ b/templates/formfield.mustache @@ -27,7 +27,7 @@ }}
{{#hashistory}} - {{#str}}history, customfield_sprogramme, {{#numrfcs}}({{numrfcs}}){{/numrfcs}}{{/str}} + {{#str}}history, customfield_sprogramme, {{#numrfcs}}({{numrfcs}}){{/numrfcs}}{{/str}} {{/hashistory}} {{#canedit}} {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}{{#str}}editprogramme, customfield_sprogramme{{/str}} diff --git a/templates/table/modules.mustache b/templates/table/modules.mustache index 79e3bf6..abfdddb 100644 --- a/templates/table/modules.mustache +++ b/templates/table/modules.mustache @@ -390,7 +390,7 @@
- + {{/editor}} {{^editor}}

{{{modulename}}}

@@ -417,4 +417,4 @@ textarea.style.height = `${textarea.scrollHeight + 1}px`; // Set height to scrollHeight to fit content. textarea.dataset.height = textarea.scrollHeight + 1; // Store the height in a data attribute. }); -{{/js}} \ No newline at end of file +{{/js}} diff --git a/templates/table/rfc.mustache b/templates/table/rfc.mustache index 2c47142..edf411c 100644 --- a/templates/table/rfc.mustache +++ b/templates/table/rfc.mustache @@ -48,11 +48,12 @@ {{#str}}removerfc, customfield_sprogramme{{/str}} {{/canremove}} {{#cansubmit}} + {{#str}}reeditrfc, customfield_sprogramme{{/str}} {{#str}}submitrfc, customfield_sprogramme{{/str}} {{/cansubmit}} {{#issubmitted}} {{#cancancel}} - {{#str}}cancelrfc, customfield_sprogramme{{/str}} + {{#str}}cancelrfc, customfield_sprogramme{{/str}} {{/cancancel}} {{/issubmitted}}
diff --git a/templates/table/rows.mustache b/templates/table/rows.mustache index 9da5182..a88a824 100644 --- a/templates/table/rows.mustache +++ b/templates/table/rows.mustache @@ -200,13 +200,13 @@ {{#editor}}
{{/editor}} {{/rows}} {{#mustacheonly}}
- - - + + +
-{{/mustacheonly}} \ No newline at end of file +{{/mustacheonly}} diff --git a/tests/local/rfc_manager_test.php b/tests/local/rfc_manager_test.php index 11229ec..ceea6a4 100644 --- a/tests/local/rfc_manager_test.php +++ b/tests/local/rfc_manager_test.php @@ -301,7 +301,7 @@ public function test_get_data(): void { $this->assertFalse($data['issubmitted']); $this->assertFalse($data['canaccept']); // Teacher 1 is not admin so cannot accept. $this->assertTrue($data['cansubmit']); // Teacher 1 can submit. - $this->assertFalse($data['cancancel']); // Teacher 1 cannot cancel as nothing submitted. + $this->assertTrue($data['cancancel']); // Teacher 1 cannot cancel as nothing submitted. $this->assertTrue($data['canremove']); // Teacher 1 can remove the rfc as not submitted. $this->assertFalse($data['canadd']); // Teacher 1 cannot add as we have already a rfc. From 5ce53811bf6ea270930553491c35f19d7a602375 Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Tue, 17 Feb 2026 10:21:54 +0100 Subject: [PATCH 15/15] Add behat test for RFCs --- tests/behat/behat_customfield_sprogramme.php | 100 ++++++++- tests/behat/create_rfc.feature | 203 +++++++++++++++++++ tests/behat/edit_programme_data.feature | 2 +- 3 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 tests/behat/create_rfc.feature diff --git a/tests/behat/behat_customfield_sprogramme.php b/tests/behat/behat_customfield_sprogramme.php index cb62dfa..5ba0ac2 100644 --- a/tests/behat/behat_customfield_sprogramme.php +++ b/tests/behat/behat_customfield_sprogramme.php @@ -89,12 +89,17 @@ public function set_cell_value_to($modulenr, $rownr, $columnname, $value) { public function check_cell_value($modulenr, $rownr, $columnname, $value) { $cell = $this->find_cell($modulenr, $rownr, $columnname); $cellinput = $cell->find('css', 'input, select, textarea'); + $cellvalue = null; if (!$cellinput) { // Maybe it is a readonly field, so just check the text. if ($newvalue = $cell->find('css', '.newvalue')) { $cellvalue = trim($newvalue->getText()); - } else { - $cellvalue = trim($value); + } + if ($oldvalue = $cell->find('css', '.staticdata')) { + $cellvalue = trim($oldvalue->getText()); + } + if ($tagvalue = $cell->find('css', '.name')) { + $cellvalue = trim($tagvalue->getText()); } } else { $fieldinstance = behat_field_manager::get_field_instance('field', $cellinput, $this->getSession()); @@ -327,6 +332,49 @@ public function close_programme_editing_form(): void { $closebutton->click(); } + /** + * Checks that a cell in the programme table is editable (contains an input, select, or textarea). + * + * @Then /^mod "(?P(?:[^"]|\\")*)" row "(?P(?:[^"]|\\")*)" column "(?P(?:[^"]|\\")*)" should be editable$/ + * @param string $modulenr The module number + * @param string $rownr The row number + * @param string $columnname The column name + * @throws ExpectationException + */ + public function cell_should_be_editable(string $modulenr, string $rownr, string $columnname): void { + $cell = $this->find_cell($modulenr, $rownr, $columnname); + $cellinput = $cell->find('css', 'input, select, textarea'); + if (!$cellinput) { + throw new ExpectationException( + 'Cell in module ' . $modulenr . ' row ' . $rownr . ' column ' . $columnname . + ' is not editable (no input/select/textarea found)', + $this->getSession() + ); + } + } + + /** + * Checks that a cell in the programme table is not editable (has class "static" and no input elements). + * + * @Then /^mod "(?P(?:[^"]|\\")*)" row "(?P(?:[^"]|\\")*)" column "(?P(?:[^"]|\\")*)" should not be editable$/ + * @param string $modulenr The module number + * @param string $rownr The row number + * @param string $columnname The column name + * @throws ExpectationException + */ + public function cell_should_not_be_editable(string $modulenr, string $rownr, string $columnname): void { + $cell = $this->find_cell($modulenr, $rownr, $columnname); + $cellinput = $cell->find('css', 'input, select, textarea'); + $hasstaticclass = strpos($cell->getAttribute('class') ?? '', 'static') !== false; + if ($cellinput || !$hasstaticclass) { + throw new ExpectationException( + 'Cell in module ' . $modulenr . ' row ' . $rownr . ' column ' . $columnname . + ' is editable but should not be (found input or missing static class)', + $this->getSession() + ); + } + } + /** * Find an element in the app table. * @@ -353,13 +401,26 @@ private function find_cell(string $modulenr, string $rownr, string $columnname): // Find the row in the modules rows in [data-region="rows"]. $row = $this->find_row($modulenr, $rownr); - // Find the cell. - $cell = $this->find( - 'css', - 'td[data-columnid="' . $columnnr . '"]', - new ExpectationException('Cell not found in row ' . $rownr . ' and column ' . $columnname, $this->getSession()), - $row - ); + // Special handling for Disciplines and Competencies columns which use data-region instead of data-columnid. + $specialcolumns = [ + 'Disciplines' => 'data-disciplines', + 'Competencies' => 'data-competencies', + ]; + if (isset($specialcolumns[$columnname])) { + $cell = $this->find( + 'css', + 'td[' . $specialcolumns[$columnname] . ']', + new ExpectationException('Cell not found in row ' . $rownr . ' and column ' . $columnname, $this->getSession()), + $row + ); + } else { + $cell = $this->find( + 'css', + 'td[data-columnid="' . $columnnr . '"]', + new ExpectationException('Cell not found in row ' . $rownr . ' and column ' . $columnname, $this->getSession()), + $row + ); + } return $cell; } @@ -386,6 +447,27 @@ private function find_row(string $modulenr, string $rownr) { return $row; } + /** + * Clicks the element with a specific data-action attribute value. + * + * @When /^I click on the "(?P(?:[^"]|\\")*)" data action$/ + * @param string $action The data-action value to click + * @throws ElementNotFoundException + */ + public function i_click_on_data_action(string $action): void { + $selector = '[data-action="' . $action . '"]'; + $element = $this->find('css', $selector); + if (!$element) { + throw new ElementNotFoundException( + $this->getSession(), + 'element', + 'css', + $selector + ); + } + $element->click(); + } + /** * Clicks the edit button for the programme field. * diff --git a/tests/behat/create_rfc.feature b/tests/behat/create_rfc.feature new file mode 100644 index 0000000..6062a12 --- /dev/null +++ b/tests/behat/create_rfc.feature @@ -0,0 +1,203 @@ +@customfield @customfield_sprogramme @javascript +Feature: As a teacher I can create and manage RFCs (Request for Change) in customfield_sprogramme + + Background: + Given the following "courses" exist: + | fullname | shortname | enablecompletion | + | Syllabus Course 1 | SYLL1 | 1 | + And the following "custom field categories" exist: + | name | component | area | itemid | + | Course fields | core_course | course | 0 | + And the following "custom fields" exist: + | name | category | type | shortname | description | + | SProgramme field | Course fields | sprogramme | sprogramme | SProgramme | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | SYLL1 | editingteacher | + + Scenario: Create and submit an RFC + Given I log in as "teacher1" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I set the field "SProgramme field enabled" to "1" + And I click on "Save and display" "button" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And I set mod "1" row "1" column "Session title or exercise" to "Séance 1" + And I set mod "1" row "1" column "CM" to "2.1" + And I set mod "1" row "1" column "TD" to "3.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I should see mod "1" row "1" column "Session title or exercise" with value "Séance 1" + And I should see mod "1" row "1" column "TD" with value "3.5" + And mod "1" row "1" column "TD" should not be editable + And I should see "Submit change request" in the "Edit" "dialogue" + And I should not see "Save" in the "Edit" "dialogue" + And I click on the "submitrfc" data action + And I should see "Cancel change request" in the "Edit" "dialogue" + + Scenario: Cancel a submitted RFC + Given I log in as "teacher1" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I set the field "SProgramme field enabled" to "1" + And I click on "Save and display" "button" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And I set mod "1" row "1" column "Session title or exercise" to "Séance 1" + And I set mod "1" row "1" column "TD" to "3.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I click on the "submitrfc" data action + And I should see "Cancel change request" in the "Edit" "dialogue" + And "[data-action='cancelrfc']" "css_element" should exist in the "Edit" "dialogue" + And I click on the "cancelrfc" data action + Then mod "1" row "1" column "TD" should be editable + And I should see "Save" in the "Edit" "dialogue" + + Scenario: Cancel a submitted RFC then edit and resubmit + Given I log in as "teacher1" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I set the field "SProgramme field enabled" to "1" + And I click on "Save and display" "button" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And I set mod "1" row "1" column "Session title or exercise" to "Séance 1" + And I set mod "1" row "1" column "TD" to "3.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I click on the "submitrfc" data action + And I click on the "cancelrfc" data action + # Edit the data after canceling the RFC + And I set mod "1" row "1" column "TD" to "4.0" + And I add a new row to mod "1" + And I set mod "1" row "2" column "CM" to "1.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I should see mod "1" row "1" column "TD" with value "4.0" + And I should see mod "1" row "2" column "CM" with value "1.5" + And I click on the "submitrfc" data action + And I should see mod "1" row "1" column "TD" with value "4.0" + And I should see mod "1" row "2" column "CM" with value "1.5" + And I should see "Cancel change request" in the "Edit" "dialogue" + And "[data-action='cancelrfc']" "css_element" should exist in the "Edit" "dialogue" + And I should see mod "1" row "1" column "TD" with value "4.0" + And I should see mod "1" row "2" column "CM" with value "1.5" + + Scenario: Save and re-edit before submitting + Given I log in as "teacher1" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I set the field "SProgramme field enabled" to "1" + And I click on "Save and display" "button" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And I set mod "1" row "1" column "Session title or exercise" to "Séance 1" + And I set mod "1" row "1" column "TD" to "3.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I should see "Continue editing" in the "Edit" "dialogue" + And "[data-action='cancelrfc']" "css_element" should exist in the "Edit" "dialogue" + And I click on the "cancelrfc" data action + Then mod "1" row "1" column "TD" should be editable + And I set mod "1" row "1" column "TD" to "5.0" + And I set mod "1" row "1" column "Perso av" to "2.3" + And I click on "Save" "button" in the "Edit" "dialogue" + And I should see mod "1" row "1" column "TD" with value "5.0" + And I should see mod "1" row "1" column "Perso av" with value "2.3" + + Scenario: Admin accepts a submitted RFC + Given I log in as "teacher1" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I set the field "SProgramme field enabled" to "1" + And I click on "Save and display" "button" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And I set mod "1" row "1" column "Session title or exercise" to "Séance 1" + And I set mod "1" row "1" column "CM" to "2.1" + And I add a new row to mod "1" + And I set mod "1" row "2" column "TD" to "3.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I click on the "submitrfc" data action + And I should see "Cancel change request" in the "Edit" "dialogue" + And I log out + # Admin reviews the RFC + And I log in as "admin" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And "[data-action='acceptrfc']" "css_element" should exist in the "Edit" "dialogue" + And "[data-action='rejectrfc']" "css_element" should exist in the "Edit" "dialogue" + And I click on the "acceptrfc" data action + And I should see mod "1" row "1" column "Session title or exercise" with value "Séance 1" + And I should see mod "1" row "1" column "CM" with value "2.1" + And I should see mod "1" row "2" column "TD" with value "3.5" + And "[data-action='acceptrfc']" "css_element" should not exist in the "Edit" "dialogue" + And "[data-action='rejectrfc']" "css_element" should not exist in the "Edit" "dialogue" + + Scenario: Admin rejects a submitted RFC then teacher re-edits and resubmits + Given I log in as "teacher1" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I set the field "SProgramme field enabled" to "1" + And I click on "Save and display" "button" + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And I set mod "1" row "1" column "Session title or exercise" to "Séance 1" + And I set mod "1" row "1" column "CM" to "2.1" + And I add a new row to mod "1" + And I set mod "1" row "2" column "TD" to "3.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I click on the "submitrfc" data action + And I log out + # Admin rejects the RFC + And I log in as "admin" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And "[data-action='acceptrfc']" "css_element" should exist in the "Edit" "dialogue" + And "[data-action='rejectrfc']" "css_element" should exist in the "Edit" "dialogue" + And I click on the "rejectrfc" data action + And I log out + # Teacher sees cancel and submit options, cancels and re-edits + And I log in as "teacher1" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And "[data-action='cancelrfc']" "css_element" should exist in the "Edit" "dialogue" + And "[data-action='submitrfc']" "css_element" should exist in the "Edit" "dialogue" + And I click on the "cancelrfc" data action + Then mod "1" row "1" column "TD" should be editable + # Edit and save new values + And I set mod "1" row "1" column "TD" to "4.0" + And I set mod "1" row "2" column "CM" to "1.5" + And I click on "Save" "button" in the "Edit" "dialogue" + And I should see mod "1" row "1" column "TD" with value "4.0" + And I should see mod "1" row "2" column "CM" with value "1.5" + And I click on the "submitrfc" data action + And I log out + # Admin sees accept and reject links again + And I log in as "admin" + And I am on "SYLL1" course homepage + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I click the programme edit button + And "[data-action='acceptrfc']" "css_element" should exist in the "Edit" "dialogue" + And "[data-action='rejectrfc']" "css_element" should exist in the "Edit" "dialogue" + And I should see mod "1" row "1" column "TD" with value "4.0" + And I should see mod "1" row "2" column "CM" with value "1.5" diff --git a/tests/behat/edit_programme_data.feature b/tests/behat/edit_programme_data.feature index c4bcfb8..c97eb03 100644 --- a/tests/behat/edit_programme_data.feature +++ b/tests/behat/edit_programme_data.feature @@ -83,4 +83,4 @@ Feature: As a teacher I can edit a Programme data in customfield_sprogramme And I click on "Save" "button" in the "tagform" "customfield_sprogramme > Competencies Form" Then I click on "Save" "button" in the "Edit" "dialogue" And I should see mod "1" row "1" column "Session title or exercise" with value "Séance 1" - And I should see mod "1" row "1" column "Competencies" with value "COPREV1 - Évaluer l'état général, le bien-être et l'état nutritionnel d'un animal ou d'un groupe d'animaux" + And I should see mod "1" row "2" column "Competencies" with value "COPREV1 - Évaluer l'état général, le bien-être et l'état nutritionnel d'un animal ou d'un groupe d'animaux"