{"version":3,"file":"studyplan-editor-components.min.js","sources":["../src/studyplan-editor-components.js"],"sourcesContent":["/* eslint no-var: \"error\"*/\n/* eslint no-console: \"off\"*/\n/* eslint no-unused-vars: warn */\n/* eslint max-len: [\"error\", { \"code\": 160 }] */\n/* eslint promise/no-nesting: \"off\" */\n/* eslint no-trailing-spaces: warn */\n/* eslint max-depth: [\"error\", 6] */\n/* eslint-env es6*/\n\nimport {SimpleLine} from \"./simpleline/simpleline\";\nimport {call} from 'core/ajax';\nimport notification from 'core/notification';\nimport {loadStringKeys, loadStrings, strformat} from './util/string-helper';\nimport {formatDate, addDays, datespaninfo} from './util/date-helper';\nimport {objCopy, transportItem} from './studyplan-processor';\nimport Debugger from './util/debugger';\nimport Config from 'core/config';\nimport {download, upload} from './downloader';\nimport {processStudyplan, processStudyplanPage} from './studyplan-processor';\nimport FitTextVue from './util/fittext-vue';\nimport {settings} from \"./util/settings\";\nimport TSComponents from './treestudyplan-components';\nimport mFormComponents from \"./util/mform-helper\";\nimport pSideBarComponents from \"./util/psidebar-vue\";\n\nimport {Drag, Drop, DropList} from './vue-easy-dnd/vue-easy-dnd.esm';\n\nconst STUDYPLAN_EDITOR_FIELDS =\n['name', 'shortname', 'description', 'idnumber', 'context_id', 'aggregation', 'aggregation_config'];\nconst PERIOD_EDITOR_FIELDS =\n['fullname', 'shortname', 'startdate', 'enddate'];\n\nconst LINE_GRAVITY = 1.3;\n\nexport default {\n install(Vue/* ,options */) {\n Vue.component('drag', Drag);\n Vue.component('drop', Drop);\n Vue.component('drop-list', DropList);\n Vue.use(TSComponents);\n Vue.use(mFormComponents);\n Vue.use(pSideBarComponents);\n Vue.use(FitTextVue);\n let debug = new Debugger(\"treestudyplan-editor\");\n /* **********************************\n * *\n * Treestudyplan Editor components *\n * *\n * **********************************/\n\n /**\n * Check if element is visible\n * @param {Object} elem The element to check\n * @returns {boolean} True if visible\n */\n function isVisible(elem) {\n return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);\n }\n\n // Create new eventbus for interaction between item components\n const ItemEventBus = new Vue();\n\n /*\n // Add event listener for the edit mode event so we can react to it, or at the very least ignore it\n document.addEventListener(editSwEventTypes.editModeSet,(e) => {\n e.preventDefault();\n ItemEventBus.$emit('editModeSet', e.detail.editMode);\n });\n */\n\n let stringKeys = loadStringKeys({\n conditions: [\n {value: 'ALL', textkey: 'condition_all'},\n {value: 'ANY', textkey: 'condition_any'},\n ],\n });\n\n let strings = loadStrings({\n studyplanText: {\n 'studyline_editmode': 'studyline_editmode',\n 'toolbox_toggle': 'toolbox_toggle',\n 'editmode_modules_hidden': 'editmode_modules_hidden',\n 'studyline_add': 'studyline_add',\n add: 'add@core',\n edit: 'edit@core',\n 'delete': \"delete@core\",\n 'studyline_name': 'studyline_name',\n 'studyline_name_ph': 'studyline_name_ph',\n 'studyline_shortname': 'studyline_shortname',\n 'studyline_shortname_ph': 'studyline_shortname_ph',\n 'studyline_enrollable': 'studyline_enrollable',\n 'studyline_enrolroles': 'studyline_enrolroles',\n 'studyline_color': 'studyline_color',\n associations: 'associations',\n 'associated_cohorts': 'associated_cohorts',\n 'associated_users': 'associated_users',\n 'studyline_edit': 'studyline_edit',\n 'studyplan_name': 'studyplan_name',\n 'studyplan_name_ph': 'studyplan_name_ph',\n 'studyplan_shortname': 'studyplan_shortname',\n 'studyplan_shortname_ph': 'studyplan_shortname_ph',\n 'studyplan_description': 'studyplan_description',\n 'studyplan_description_ph': 'studyplan_description_ph',\n 'studyplan_idnumber': 'studyplan_idnumber',\n 'studyplan_idnumber_ph': 'studyplan_idnumber_ph',\n 'studyplan_slots': 'studyplan_slots',\n 'studyplan_startdate': 'studyplan_startdate',\n 'studyplan_enddate': 'studyplan_enddate',\n 'line_enrollable_0': 'line_enrollable:0',\n 'line_enrollable_1': 'line_enrollable:1',\n 'line_enrollable_2': 'line_enrollable:2',\n 'line_enrollable_3': 'line_enrollable:3',\n drophere: 'drophere',\n studylineConfirmRemove: 'studyline_confirm_remove',\n studyplanConfirmRemove: 'studyplan_confirm_remove',\n\n },\n studyplanAdvanced: {\n 'advanced_tools': 'advanced_tools',\n 'confirm_cancel': 'confirm_cancel',\n 'confirm_ok': 'confirm_ok',\n success: 'success@core',\n error: 'failed@completion',\n 'advanced_converted': 'advanced_converted',\n 'advanced_skipped': 'advanced_skipped',\n 'advanced_failed': 'advanced_failed',\n 'advanced_locked': 'advanced_locked',\n 'advanced_multiple': 'advanced_multiple',\n 'advanced_error': 'advanced_error',\n 'advanced_tools_heading': 'advanced_tools_heading',\n 'advanced_warning_title': 'advanced_warning_title',\n 'advanced_warning': 'advanced_warning',\n 'advanced_pick_scale': 'advanced_pick_scale',\n 'advanced_course_manipulation_title': 'advanced_course_manipulation_title',\n 'advanced_bulk_course_timing': 'advanced_bulk_course_timing',\n 'advanced_bulk_course_timing_desc': 'advanced_bulk_course_timing_desc',\n 'advanced_force_scale_title': 'advanced_force_scale_title',\n 'advanced_force_scale_desc': 'advanced_force_scale_desc',\n 'advanced_force_scale_button': 'advanced_force_scale_button',\n 'advanced_confirm_header': 'advanced_confirm_header',\n 'advanced_force_scale_confirm': 'advanced_force_scale_confirm',\n 'advanced_backup_restore': 'advanced_backup_restore',\n 'advanced_restore': 'advanced_restore',\n 'advanced_backup': 'advanced_backup',\n 'advanced_restore_pages': 'advanced_restore_pages',\n 'advanced_restore_lines': 'advanced_restore_lines',\n 'advanced_backup_plan': 'advanced_backup_plan',\n 'advanced_backup_page': 'advanced_backup_page',\n 'advanced_export': 'advanced_export',\n 'advanced_export_csv_plan': 'advanced_export_csv_plan',\n 'advanced_export_csv_page': 'advanced_export_csv_page',\n 'advanced_import_from_file': 'advanced_import_from_file',\n 'advanced_purge': \"advanced_purge\",\n 'advanced_purge_plan': \"advanced_purge_plan\",\n 'advanced_purge_plan_expl': \"advanced_purge_plan_expl\",\n 'advanced_purge_page': \"advanced_purge_page\",\n 'advanced_purge_page_expl': \"advanced_purge_page_expl\",\n 'advanced_cascade_cohortsync_title': \"advanced_cascade_cohortsync_title\",\n 'advanced_cascade_cohortsync_desc': \"advanced_cascade_cohortsync_desc\",\n 'advanced_cascade_cohortsync': \"advanced_cascade_cohortsync\",\n currentpage: \"currentpage\",\n },\n studyplanEdit: {\n studyplanEdit: 'studyplan_edit',\n 'studyplan_add': 'studyplan_add',\n 'studyplanpage_add': 'studyplanpage_add',\n 'studyplanpage_edit': 'studyplanpage_edit',\n 'info_periodsextended': 'studyplanpage_info_periodsextended',\n warning: 'warning@core',\n },\n periodEdit: {\n edit: 'period_edit',\n fullname: 'studyplan_name',\n shortname: 'studyplan_shortname',\n startdate: 'studyplan_startdate',\n enddate: 'studyplan_enddate',\n },\n courseTiming: {\n title: 'course_timing_title',\n desc: 'course_timing_desc',\n question: 'course_timing_question',\n warning: 'course_timing_warning',\n 'timing_ok': 'course_timing_ok',\n 'timing_off': 'course_timing_off',\n course: 'course@core',\n period: 'period',\n yes: 'yes$core',\n no: 'no$core',\n duration: 'duration',\n years: 'years$core',\n year: 'year$core',\n weeks: 'weeks$core',\n week: 'week$core',\n days: 'days$core',\n day: 'day$core',\n rememberchoice: 'course_timing_rememberchoice',\n hidewarning: 'course_timing_hidewarning',\n periodspan: 'course_period_span',\n periods: 'periods',\n 'periodspan_desc': 'course_period_span_desc',\n },\n studyplanAssociate: {\n 'associations': 'associations',\n 'associated_cohorts': 'associated_cohorts',\n 'associated_users': 'associated_users',\n 'associated_coaches': 'associated_coaches',\n 'associate_cohorts': 'associate_cohorts',\n 'associate_users': 'associate_users',\n 'associate_coached': 'associate_coaches',\n 'add_association': 'add_association',\n 'delete_association': 'delete_association',\n 'associations_empty': 'associations_empty',\n 'associations_search': 'associations_search',\n cohorts: 'cohorts',\n users: 'users',\n coaches: 'coaches',\n selected: 'selected',\n name: 'name',\n context: 'context',\n search: 'search',\n },\n itemText: {\n 'select_conditions': \"select_conditions\",\n 'item_configuration': \"item_configuration\",\n ok: \"ok@core\",\n 'delete': \"delete@core\",\n 'item_delete_message': \"item_delete_message\",\n 'type_course': \"course@core\",\n 'type_junction': \"tool-junction\",\n 'type_start': \"tool-start\",\n 'type_finish': \"tool-finish\",\n 'type_badge': \"tool-badge\",\n 'type_invalid': \"course-invalid\",\n },\n itemCourseText: {\n 'select_conditions': \"select_conditions\",\n 'select_grades': \"select_grades\",\n 'coursetiming_past': \"coursetiming_past\",\n 'coursetiming_present': \"coursetiming_present\",\n 'coursetiming_future': \"coursetiming_future\",\n 'grade_include': \"grade_include\",\n 'grade_require': \"grade_require\",\n ok: \"ok@core\",\n cancel: \"cancel@core\",\n 'delete': \"delete@core\",\n noenddate: \"noenddate\",\n },\n invalid: {\n error: 'error',\n },\n completion: {\n 'completion_completed': \"completion_completed\",\n 'completion_incomplete': \"completion_incomplete\",\n 'aggregation_all': \"aggregation_all\",\n 'aggregation_any': \"aggregation_any\",\n 'aggregation_overall_all': \"aggregation_overall_all\",\n 'aggregation_overall_any': \"aggregation_overall_any\",\n 'completion_not_configured': \"completion_not_configured\",\n 'configure_completion': \"configure_completion\",\n },\n competency: {\n 'competency_not_configured': \"competency_not_configured\",\n 'configure_competency': \"configure_competency\",\n when: \"when\",\n required: \"required\",\n points: \"points@core_grades\",\n heading: \"competency_heading\",\n details: \"competency_details\",\n },\n badge: {\n 'share_badge': \"share_badge\",\n dateissued: \"dateissued\",\n dateexpire: \"dateexpire\",\n badgeinfo: \"badgeinfo\",\n },\n toolbox: {\n toolbox: 'toolbox',\n toolbarRight: 'toolbar-right',\n courses: 'courses',\n flow: 'flow',\n toolJunction: 'tool-junction',\n toolFinish: 'tool-finish',\n toolStart: 'tool-start',\n badges: 'badges',\n relatedbadges: 'relatedbages@badges', /* [sic] as in badges translation file */\n filter: 'filter@core',\n sitebadges: 'sitebadges@badges',\n }\n });\n\n /*\n * T-STUDYPLAN-ADVANCED\n */\n Vue.component('t-studyplan-advanced', {\n props: {\n value: {\n type: Object,\n default() {\n return null;\n },\n },\n selectedpage: {\n type: Object,\n default() {\n return null;\n },\n }\n\n },\n data() {\n return {\n forceScales: {\n selectedScale: null,\n result: [],\n },\n text: strings.studyplanAdvanced,\n };\n },\n computed: {\n scales() {\n return [{\n id: null,\n disabled: true,\n name: this.text.advanced_pick_scale,\n }].concat(this.value.advanced.force_scales.scales);\n },\n },\n methods: {\n forceScalesStart() {\n // Set confirmation box\n const self = this;\n this.$bvModal.msgBoxConfirm(this.text.advanced_force_scale_confirm, {\n title: this.text.advanced_force_scale_confirm,\n okVariant: 'danger',\n okTitle: this.text.confirm_ok,\n cancelTitle: this.text.confirm_cancel,\n }).then(value => {\n if (value == true) {\n call([{\n methodname: 'local_treestudyplan_force_studyplan_scale',\n args: {\n 'studyplan_id': this.value.id,\n 'scale_id': this.forceScales.selectedScale,\n }\n }])[0].then((response) => {\n self.forceScales.result = response;\n return;\n }).catch(notification.exception);\n }\n return;\n }).catch(notification.exception);\n },\n exportPage(format) {\n const self = this;\n if (format == undefined || ![\"json\", \"csv\"].includes(format)) {\n format = \"json\";\n }\n call([{\n methodname: 'local_treestudyplan_export_page',\n args: {\n 'page_id': this.selectedpage.id,\n format: format,\n },\n }])[0].then((response) => {\n download(self.value.shortname + \".page.\" + format, response.content, response.format);\n return;\n }).catch(notification.exception);\n },\n exportPlan() {\n const self = this;\n call([{\n methodname: 'local_treestudyplan_export_plan',\n args: {\n 'studyplan_id': this.value.id,\n format: \"json\",\n },\n }])[0].then((response) => {\n download(self.value.shortname + \".plan.json\", response.content, response.format);\n return;\n }).catch(notification.exception);\n },\n bulkCourseTiming() {\n const self = this;\n call([{\n methodname: 'local_treestudyplan_bulk_course_timing',\n args: {\n 'page_id': this.selectedpage.id,\n },\n }])[0].then((response) => {\n if (response.success) {\n // Reloading the webpage saves trouble reloading the specific page updated.\n location.reload();\n } else {\n self.$bvModal.msgBoxOk(response.msg, {title: \"Could not set bulk course timing\"});\n debug.error(\"Could not set bulk course timing: \", response.msg);\n }\n return;\n }).catch(notification.exception);\n },\n importStudylines() {\n const self = this;\n upload((filename, content)=>{\n call([{\n methodname: 'local_treestudyplan_import_studylines',\n args: {\n 'page_id': this.selectedpage.id,\n content: content,\n format: \"application/json\",\n },\n }])[0].then((response) => {\n if (response.success) {\n location.reload();\n } else {\n self.$bvModal.msgBoxOk(response.msg, {title: \"Import failed\"});\n debug.error(\"Import failed: \", response.msg);\n }\n return;\n }).catch(notification.exception);\n }, \"application/json\");\n },\n importPages() {\n const self = this;\n upload((filename, content)=>{\n call([{\n methodname: 'local_treestudyplan_import_pages',\n args: {\n 'studyplan_id': this.value.id,\n content: content,\n format: \"application/json\",\n },\n }])[0].then((response) => {\n if (response.success) {\n location.reload();\n } else {\n self.$bvModal.msgBoxOk(response.msg, {title: \"Import failed\"});\n debug.error(\"Import failed: \", response.msg);\n }\n return;\n }).catch(notification.exception);\n }, \"application/json\");\n },\n purgeStudyplan() {\n const self = this;\n call([{\n methodname: 'local_treestudyplan_delete_studyplan',\n args: {\n id: this.value.id,\n force: true,\n },\n }])[0].then((response) => {\n if (response.success) {\n location.reload();\n } else {\n self.$bvModal.msgBoxOk(response.msg, {title: \"Could not delete plan \"});\n debug.error(\"Could not delete plan: \", response.msg);\n }\n return;\n }).catch(notification.exception);\n },\n purgeStudyplanpage() {\n const self = this;\n if (this.selectedpage) {\n call([{\n methodname: 'local_treestudyplan_delete_studyplanpage',\n args: {\n id: this.selectedpage.id,\n force: true,\n },\n }])[0].then((response) => {\n if (response.success) {\n location.reload();\n } else {\n self.$bvModal.msgBoxOk(response.msg, {title: \"Could not delete page\"});\n debug.error(\"Could not delete page: \", response.msg);\n }\n return;\n }).catch(notification.exception);\n }\n },\n cascadeCohortsync() {\n const self = this;\n call([{\n methodname: 'local_treestudyplan_cascade_cohortsync',\n args: {\n 'studyplan_id': this.value.id,\n },\n }])[0].then((response) => {\n self.$bvModal.msgBoxOk(response.success ? self.text.success : self.text.error,\n {title: self.text.advanced_cascade_cohortsync});\n return;\n }).catch(notification.exception);\n },\n modalClose() {\n this.forceScales.result = [];\n }\n },\n template:\n `\n \n {{text.advanced_tools}}\n \n \n \n {{ text.advanced_warning}}\n \n \n

{{ text.advanced_cascade_cohortsync_title}}

\n

{{ text.advanced_cascade_cohortsync_desc}}

\n

{{ text.advanced_cascade_cohortsync}}

\n

{{ text.advanced_bulk_course_timing}}

\n

{{ text.advanced_bulk_course_timing_desc}}

\n

{{text.currentpage}} {{selectedpage.fullname}}

\n

{{ text.advanced_bulk_course_timing}}

\n \n
\n \n

{{ text.advanced_backup }}

\n

{{ text.advanced_backup_page }}\n {{text.currentpage}} {{selectedpage.fullname}}

\n

{{ text.advanced_backup_plan }}

\n

{{ text.advanced_restore }}

\n

{{ text.advanced_restore_lines}}

\n

{{ text.advanced_restore_pages }}

\n

{{ text.advanced_export }}

\n

{{ text.advanced_export_csv_page }}\n {{text.currentpage}} {{selectedpage.fullname}}

\n
\n \n

{{text.advanced_purge_page_expl}}

\n

{{text.currentpage}} {{selectedpage.fullname}}

\n

{{ text.advanced_purge_page}}

\n

{{text.advanced_purge_plan_expl}}

\n

{{ text.advanced_purge_plan}}

\n
\n
\n
\n
\n `\n });\n\n\n /*\n * T-STUDYPLAN-EDIT\n */\n Vue.component('t-studyplan-edit', {\n props: {\n 'value': {\n type: Object,\n default() {\n return null;\n },\n },\n 'mode': {\n type: String,\n default() {\n return \"edit\";\n },\n },\n 'type': {\n type: String,\n default() {\n return \"link\";\n },\n },\n 'variant': {\n type: String,\n default() {\n return \"\";\n },\n },\n 'contextid': {\n type: Number,\n 'default': 1\n },\n },\n data() {\n return {\n text: strings.studyplanEdit,\n };\n },\n computed: {\n },\n methods: {\n planSaved(updatedplan) {\n const self = this;\n debug.info(\"Got new plan data\", updatedplan);\n\n if (self.mode == 'create') {\n // Inform parent of the details of the newly created plan\n self.$emit(\"created\", updatedplan);\n } else {\n // Determine if the plan moved context...\n const movedFrom = self.value.context_id;\n const movedTo = updatedplan.context_id;\n const moved = (movedFrom != movedTo);\n\n if (updatedplan.pages[0].periods != self.value.pages[0].periods) {\n // If the pages changed, just reload the entire model for the plan\n call([{\n methodname: 'local_treestudyplan_get_studyplan_map',\n args: {id: self.value.id}\n }])[0].then((response) => {\n self.value = processStudyplan(response, true);\n debug.info('studyplan processed');\n self.$emit('input', self.value);\n return;\n }).catch(function(error) {\n notification.exception(error);\n });\n } else {\n // Copy updated fields and trigger update\n objCopy(self.value, updatedplan, STUDYPLAN_EDITOR_FIELDS);\n self.$emit('input', self.value);\n if (moved) {\n self.$emit('moved', self.value, movedFrom, movedTo);\n }\n }\n }\n },\n },\n template:\n `\n \n \n \n `\n });\n\n /*\n * T-STUDYPLAN-EDIT\n */\n Vue.component('t-studyplan-page-edit', {\n props: {\n 'value': {\n type: Object,\n default() {\n return null;\n },\n },\n 'mode': {\n type: String,\n default() {\n return \"edit\";\n },\n },\n 'type': {\n type: String,\n default() {\n return \"link\";\n },\n },\n 'variant': {\n type: String,\n default() {\n return \"\";\n },\n },\n 'studyplan': {\n type: Object,\n },\n },\n data() {\n return {\n text: strings.studyplanEdit,\n };\n },\n computed: {\n },\n methods: {\n planSaved(updatedpage) {\n const self = this;\n\n if (self.mode == 'create') {\n // Inform parent of the details of the newly created plan\n self.$emit(\"created\", updatedpage);\n } else {\n const page = processStudyplanPage(updatedpage);\n debug.info('studyplan page processed');\n\n if (self.value.periods < page.periods) {\n this.$bvModal.msgBoxOk(this.text.info_periodsextended, {\n title: this.text.warning,\n okVariant: 'success',\n centered: true\n });\n }\n self.$emit('input', page);\n\n }\n },\n },\n template:\n `\n \n \n \n `\n });\n\n\n /*\n * T-STUDYPLAN-ASSOCIATE\n */\n Vue.component('t-studyplan-associate', {\n props: ['value'],\n data() {\n return {\n show: false,\n config: {\n userfields: [\n {key: \"selected\"},\n {key: \"firstname\", \"sortable\": true},\n {key: \"lastname\", \"sortable\": true},\n ],\n cohortfields: [\n {key: \"selected\"},\n {key: \"name\", \"sortable\": true},\n {key: \"context\", \"sortable\": true},\n ]\n },\n association: {\n cohorts: [],\n users: [],\n coaches: []\n },\n loading: {\n cohorts: false,\n users: false,\n coaches: false,\n },\n search: {users: [], cohorts: [], coaches: []},\n selected: {\n search: {users: [], cohorts: [], coaches: []},\n associated: {users: [], cohorts: [], coaches: []}\n },\n text: strings.studyplanAssociate,\n };\n },\n methods: {\n showModal() {\n this.show = true;\n this.loadAssociations();\n },\n cohortOptionModel(c) {\n return {\n value: c.id,\n text: c.name + ' (' + c.context.path.join(' / ') + ')',\n };\n },\n userOptionModel(u) {\n return {\n value: u.id,\n text: u.firstname + ' ' + u.lastname,\n };\n },\n loadAssociations() {\n const self = this;\n self.loading.cohorts = true;\n self.loading.users = true;\n call([{\n methodname: 'local_treestudyplan_associated_users',\n args: {\n 'studyplan_id': self.value.id,\n }\n }])[0].then((response) => {\n self.association.users = response.map(self.userOptionModel);\n self.loading.users = false;\n return;\n }).catch(notification.exception);\n\n call([{\n methodname: 'local_treestudyplan_associated_cohorts',\n args: {\n 'studyplan_id': self.value.id,\n }\n }])[0].then((response) => {\n self.association.cohorts = response.map(self.cohortOptionModel);\n self.loading.cohorts = false;\n return;\n }).catch(notification.exception);\n\n self.loading.coaches = true;\n call([{\n methodname: 'local_treestudyplan_associated_coaches',\n args: {\n 'studyplan_id': self.value.id,\n }\n }])[0].then((response) => {\n self.association.coaches = response.map(self.userOptionModel);\n self.loading.coaches = false;\n return;\n }).catch(notification.exception);\n },\n searchCohorts(searchtext) {\n const self = this;\n\n if (searchtext.length > 0) {\n call([{\n methodname: 'local_treestudyplan_list_cohort',\n args: {\n like: searchtext,\n 'studyplan_id': self.value.id\n }\n }])[0].then((response) => {\n self.search.cohorts = response.map(self.cohortOptionModel);\n return;\n }).catch(notification.exception);\n } else {\n self.search.cohorts = [];\n }\n },\n cohortAssociate() {\n const self = this;\n let requests = [];\n const associated = self.association.cohorts;\n const search = self.search.cohorts;\n const searchselected = self.selected.search.cohorts;\n for (const i in searchselected) {\n const r = searchselected[i];\n call([{\n methodname: 'local_treestudyplan_connect_cohort',\n args: {\n 'studyplan_id': self.value.id,\n 'cohort_id': r,\n },\n }])[0].then((response) => {\n if (response.success) {\n transportItem(associated, search, r);\n }\n return;\n }).catch(notification.exception);\n }\n call(requests);\n },\n cohortDisassociate() {\n const self = this;\n const associatedselected = self.selected.associated.cohorts;\n const associated = self.association.cohorts;\n const search = self.search.cohorts;\n for (const i in associatedselected) {\n const r = associatedselected[i];\n call([{\n methodname: 'local_treestudyplan_disconnect_cohort',\n args: {\n 'studyplan_id': self.value.id,\n 'cohort_id': r,\n }\n }])[0].then((response) => {\n if (response.success) {\n transportItem(search, associated, r);\n }\n return;\n }).catch(notification.exception);\n }\n },\n searchUsers(searchtext) {\n const self = this;\n if (searchtext.length > 0) {\n call([{\n methodname: 'local_treestudyplan_find_user',\n args: {\n like: searchtext,\n 'studyplan_id': self.value.id\n }\n }])[0].then((response) => {\n self.search.users = response.map(self.userOptionModel);\n return;\n }).catch(notification.exception);\n } else {\n self.search.users = [];\n }\n },\n userAssociate() {\n const self = this;\n const associated = self.association.users;\n const search = self.search.users;\n const searchselected = self.selected.search.users;\n for (const i in searchselected) {\n const r = searchselected[i];\n call([{\n methodname: 'local_treestudyplan_connect_user',\n args: {\n 'studyplan_id': self.value.id,\n 'user_id': r,\n },\n }])[0].then((response) => {\n if (response.success) {\n transportItem(associated, search, r);\n }\n return;\n }).catch(notification.exception);\n }\n },\n userDisassociate() {\n const self = this;\n const associated = self.association.users;\n const associatedselected = self.selected.associated.users;\n const search = self.search.users;\n for (const i in associatedselected) {\n const r = associatedselected[i];\n call([{\n methodname: 'local_treestudyplan_disconnect_user',\n args: {\n 'studyplan_id': self.value.id,\n 'user_id': r,\n }\n }])[0].then((response) => {\n if (response.success) {\n transportItem(search, associated, r);\n }\n return;\n }).catch(notification.exception);\n }\n },\n searchCoaches(searchtext) {\n const self = this;\n if (searchtext.length > 0) {\n call([{\n methodname: 'local_treestudyplan_find_coach',\n args: {\n like: searchtext,\n 'studyplan_id': self.value.id,\n }\n }])[0].then((response) => {\n self.search.coaches = response.map(self.userOptionModel);\n return;\n }).catch(notification.exception);\n } else {\n self.search.coaches = [];\n }\n },\n coachAssociate() {\n const self = this;\n const associated = self.association.coaches;\n const search = self.search.coaches;\n const searchselected = self.selected.search.coaches;\n for (const i in searchselected) {\n const r = searchselected[i];\n\n call([{\n methodname: 'local_treestudyplan_connect_coach',\n args: {\n 'studyplan_id': self.value.id,\n 'user_id': r,\n },\n }])[0].then((response) => {\n if (response.success) {\n transportItem(associated, search, r);\n }\n return;\n }).catch(notification.exception);\n }\n },\n coachDisassociate() {\n const self = this;\n const associated = self.association.coaches;\n const associatedselected = self.selected.associated.coaches;\n const search = self.search.coaches;\n for (const i in associatedselected) {\n const r = associatedselected[i];\n\n call([{\n methodname: 'local_treestudyplan_disconnect_coach',\n args: {\n 'studyplan_id': self.value.id,\n 'user_id': r,\n }\n }])[0].then((response) => {\n if (response.success) {\n transportItem(search, associated, r);\n }\n return;\n }).catch(notification.exception);\n }\n },\n },\n template:\n`\n\n \n \n \n \n \n {{text.associated_cohorts}}\n {{text.associate_cohorts}}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n  {{text.delete_association}}\n \n \n  {{text.add_association}}\n \n \n \n \n \n \n \n {{text.associated_users}}\n {{text.associate_users}}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n  {{text.delete_association}}\n \n \n  {{text.add_association}}\n \n \n \n \n \n \n \n {{text.associated_coaches}}\n {{text.associate_coaches}}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n  {{text.delete_association}}\n \n \n  {{text.add_association}}\n \n \n \n \n \n \n\n`\n });\n\n /* * ****************\n *\n * Period editor\n *\n *************/\n\n Vue.component('t-period-edit', {\n props: {\n 'value': {\n type: Object,\n default() {\n return null;\n },\n },\n 'type': {\n type: String,\n default() {\n return \"link\";\n },\n },\n 'variant': {\n type: String,\n default() {\n return \"\";\n },\n },\n 'minstart': {\n type: String,\n default() {\n return null;\n },\n },\n 'maxend': {\n type: String,\n default() {\n return null;\n },\n }\n },\n data() {\n return {\n show: false,\n editdata: {\n fullname: '',\n shortname: '',\n startdate: (new Date()).getFullYear() + '-08-01',\n enddate: ((new Date()).getFullYear() + 1) + '-08-01',\n },\n text: strings.periodEdit,\n };\n },\n methods: {\n editStart() {\n objCopy(this.editdata, this.value, PERIOD_EDITOR_FIELDS);\n this.show = true;\n },\n editFinish() {\n const self = this;\n let args = {'id': this.value.id};\n\n objCopy(args, this.editdata, PERIOD_EDITOR_FIELDS);\n\n call([{\n methodname: 'local_treestudyplan_edit_period',\n args: args\n }])[0].then((response) => {\n objCopy(self.value, response, PERIOD_EDITOR_FIELDS);\n self.$emit('input', self.value);\n self.$emit('edited', self.value);\n return;\n }).catch(notification.exception);\n },\n refresh() {\n const self = this;\n call([{\n methodname: 'local_treestudyplan_get_period',\n args: {'id': this.value.id},\n }])[0].then((response) => {\n objCopy(self.value, response, PERIOD_EDITOR_FIELDS);\n self.$emit('input', self.value);\n return;\n }).catch(notification.exception);\n },\n addDay(date, days) {\n if (days === undefined) {\n days = 1;\n }\n return addDays(date, days);\n },\n subDay(date, days) {\n if (days === undefined) {\n days = 1;\n }\n return addDays(date, 0 - days);\n },\n },\n template:\n `\n \n \n \n \n \n \n {{ text.fullname}}\n \n \n \n \n \n {{ text.shortname}}\n \n \n \n \n \n {{ text.studyplan_startdate}}\n \n \n \n \n \n {{ text.studyplan_enddate}}\n \n \n \n \n \n \n \n `\n });\n\n // TAG: Start studyplan component\n /*\n * T-STUDYPLAN\n */\n Vue.component('t-studyplan', {\n props: {\n 'value': {\n type: Object,\n },\n 'coaching': {\n type: Boolean,\n 'default': false,\n },\n },\n data() {\n return {\n config: {\n userfields: [\n {key: \"selected\"},\n {key: \"firstname\", \"sortable\": true},\n {key: \"lastname\", \"sortable\": true},\n ],\n cohortfields: [\n {key: \"selected\"},\n {key: \"name\", \"sortable\": true},\n {key: \"context\", \"sortable\": true},\n ]\n },\n create: {\n studyline: {\n name: '',\n shortname: '',\n color: '#DDDDDD',\n enrol: {\n enrollable: 0,\n enrolroles: [],\n }\n },\n page: {\n id: -1,\n name: '',\n shortname: ''\n }\n },\n edit: {\n toolboxShown: false,\n studyline: {\n editmode: false,\n data: {\n name: '',\n shortname: '',\n color: '#DDDDDD',\n enrol: {\n enrollable: 0,\n enrolroles: [],\n }\n },\n original: {},\n availableroles: [],\n },\n studyplan: {\n data: {\n name: '',\n shortname: '',\n description: '',\n slots: 4,\n startdate: '2020-08-01',\n enddate: '',\n aggregation: '',\n 'aggregation_config': '',\n 'aggregation_info': {\n useRequiredGrades: true,\n useItemCondition: false,\n },\n\n },\n original: {},\n }\n },\n text: strings.studyplanText,\n cache: {\n linelayers: {},\n },\n selectedpageindex: 0,\n emptyline: {\n id: -1,\n name: '',\n shortname: '',\n color: '#FF0000',\n filterslots: [{}],\n courseslots: [{}]\n },\n availableroles: [],\n };\n },\n created() {\n const self = this;\n // Listener for the signal that a new connection was made and needs to be drawn\n // Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines\n ItemEventBus.$on('coursechange', () => {\n self.$emit('pagechanged', this.selectedpage);\n });\n },\n mounted() {\n const self = this;\n if (this.value.pages[0].studylines.length == 0 && !this.coaching) {\n // Start in editmode if studylines on first page are empty\n this.edit.studyline.editmode = true;\n }\n\n if (!self.coaching) {\n // Retrieve available roles (only needed as manager)\n call([{\n methodname: 'local_treestudyplan_list_roles',\n args: {\n 'studyplan_id': this.value.id,\n }\n }])[0].then((response) => {\n self.availableroles = response;\n return;\n }).catch(notification.exception);\n } else {\n self.edit.toolboxShown = true; // Defaults to on in coching view.\n }\n this.$root.$emit('redrawLines');\n this.$emit('pagechanged', this.selectedpage);\n },\n beforeUnmount() {\n this.edit.toolboxShown = false;\n debug.info(\"Hiding toolbar because of destroy\");\n },\n deactivated() {\n this.edit.toolboxShown = false;\n debug.info(\"Hiding toolbar because of deactivation\");\n },\n activated() {\n if (this.coaching) {\n self.edit.toolboxShown = true; // Defaults to on in coching view.\n }\n\n },\n updated() {\n this.$root.$emit('redrawLines');\n ItemEventBus.$emit('redrawLines');\n },\n computed: {\n selectedpage() {\n return this.value.pages[this.selectedpageindex];\n },\n hivizdrop() {\n return settings(\"hivizdropslots\");\n },\n },\n methods: {\n columns(page) {\n return 1 + (page.periods * 2);\n },\n columnsStylerule(page) {\n // Uses css variables, so width for slots and filters can be configured in css\n let s = \"grid-template-columns: var(--studyplan-filter-width)\"; // Use css variable here\n for (let i = 0; i < page.periods; i++) {\n s += \" var(--studyplan-course-width) var(--studyplan-filter-width)\";\n }\n return s + \";\";\n },\n trashbinAccepts(type) {\n if (type.item) {\n return true;\n } else {\n return false;\n }\n },\n countLineLayers(line, page) {\n // For some optimization, we cache the value of this calculation for about a second\n // Would be a lot nicer if we could use a computed property for this.....\n if (this.cache.linelayers[line.id]\n && ((new Date()) - this.cache.linelayers[line.id].timestamp < 1000)\n ) {\n return this.cache.linelayers[line.id].value;\n } else {\n let maxLayer = -1;\n for (let i = 0; i <= page.periods; i++) {\n if (line.slots[i]) {\n // Determine the amount of used layers in a studyline slot\n for (const ix in line.slots[i].courses) {\n const item = line.slots[i].courses[ix];\n if (item.layer > maxLayer) {\n maxLayer = item.layer;\n }\n }\n for (const ix in line.slots[i].filters) {\n const item = line.slots[i].filters[ix];\n if (item.layer > maxLayer) {\n maxLayer = item.layer;\n }\n }\n\n }\n }\n this.cache.linelayers[line.id] = {\n value: (maxLayer + 1),\n timestamp: (new Date()),\n };\n return maxLayer + 1;\n }\n },\n slotsempty(slots) {\n if (Array.isArray(slots)) {\n let count = 0;\n for (let i = 0; i < slots.length; i++) {\n if (Array.isArray(slots[i].courses)) {\n count += slots[i].courses.length;\n }\n if (Array.isArray(slots[i].filters)) {\n count += slots[i].filters.length;\n }\n }\n return (count == 0);\n } else {\n return false;\n }\n },\n movedStudyplan(plan, from, to) {\n this.$emit('moved', plan, from, to); // Throw the event up....\n },\n addStudyLine(page, newlineinfo) {\n call([{\n methodname: 'local_treestudyplan_add_studyline',\n args: {\n 'page_id': page.id,\n 'name': newlineinfo.name,\n 'shortname': newlineinfo.shortname,\n 'color': newlineinfo.color,\n 'sequence': page.studylines.length,\n 'enrollable': newlineinfo.enrol.enrollable,\n 'enrolroles': newlineinfo.enrol.enrolroles\n }\n }])[0].then((response) => {\n page.studylines.push(response);\n newlineinfo.name = '';\n newlineinfo.shortname = '';\n newlineinfo.color = \"#dddddd\";\n newlineinfo.enrol.enrollable = 0;\n newlineinfo.enrol.enrolroles = [];\n return;\n }).catch(notification.exception);\n },\n editLineStart(line) {\n const page = this.value.pages[this.selectedpageindex];\n debug.info(\"Starting line edit\", line);\n Object.assign(this.edit.studyline.data, line);\n this.edit.studyline.original = line;\n this.$bvModal.show('modal-edit-studyline-' + page.id);\n },\n editLineFinish() {\n let editedline = this.edit.studyline.data;\n let originalline = this.edit.studyline.original;\n call([{\n methodname: 'local_treestudyplan_edit_studyline',\n args: {'id': editedline.id,\n 'name': editedline.name,\n 'shortname': editedline.shortname,\n 'color': editedline.color,\n 'enrollable': editedline.enrol.enrollable,\n 'enrolroles': editedline.enrol.enrolroles\n }\n }])[0].then((response) => {\n originalline.name = response.name;\n originalline.shortname = response.shortname;\n originalline.color = response.color;\n originalline.enrol.enrollable = response.enrol.enrollable;\n originalline.enrol.enrolroles = response.enrol.enrolroles;\n return;\n }).catch(notification.exception);\n },\n deleteLine(page, line) {\n const self = this;\n\n self.$bvModal.msgBoxConfirm(this.text.studylineConfirmRemove.replace('{$a}', line.name), {\n okTitle: this.text.delete,\n okVariant: 'danger',\n }).then((modalresponse) => {\n if (modalresponse) {\n call([{\n methodname: 'local_treestudyplan_delete_studyline',\n args: {'id': line.id}\n }])[0].then((response) => {\n if (response.success == true) {\n let index = page.studylines.indexOf(line);\n page.studylines.splice(index, 1);\n }\n return;\n }).catch(notification.exception);\n }\n return;\n }).catch(notification.exception);\n },\n reorderLines(event, lines) {\n\n // Apply reordering\n event.apply(lines);\n // Send the new sequence to the server\n let sequence = [];\n for (let idx in lines) {\n sequence.push({'id': lines[idx].id, 'sequence': idx});\n }\n call([{\n methodname: 'local_treestudyplan_reorder_studylines',\n args: {'sequence': sequence}\n }])[0].then(() => {\n return;\n }).catch(notification.exception);\n },\n deletePlan(studyplan) {\n const self = this;\n self.$bvModal.msgBoxConfirm(this.text.studyplabConfirmRemove.replace('{$a}', studyplan.name), {\n okTitle: this.text.delete,\n okVariant: 'danger',\n }).then(function(modalresponse) {\n if (modalresponse) {\n call([{\n methodname: 'local_treestudyplan_delete_studyplan',\n args: {'id': studyplan.id, force: true}\n }])[0].then((response) => {\n if (response.success == true) {\n self.$root.$emit(\"studyplanRemoved\", studyplan);\n }\n return;\n }).catch(notification.exception);\n }\n return;\n }).catch(notification.exception);\n },\n deleteStudyItem(event) {\n let item = event.data;\n call([{\n methodname: 'local_treestudyplan_delete_studyitem',\n args: {'id': item.id}\n }])[0].then((response) => {\n if (response.success == true) {\n event.source.$emit('cut', event);\n }\n return;\n }).catch(notification.exception);\n\n },\n showslot(page, line, index, layeridx, type) {\n // Check if the slot should be hidden because a previous slot has an item with a span\n // so big that it hides this slot\n const forGradable = (type == 'gradable') ? true : false;\n const periods = page.periods;\n let show = true;\n for (let i = 0; i < periods; i++) {\n if (line.slots[index - i] && line.slots[index - i].courses) {\n const list = line.slots[index - i].courses;\n for (const ix in list) {\n const item = list[ix];\n if (item.layer == layeridx) {\n if (forGradable) {\n if (i > 0 && (item.span - i) > 0) {\n show = false;\n }\n } else {\n if ((item.span - i) > 1) {\n show = false;\n }\n }\n }\n }\n }\n }\n\n return show;\n },\n periodEdited(pi) {\n const prev = this.$refs[\"periodeditor-\" + (pi.period - 1)];\n if (prev && prev[0]) {\n prev[0].refresh();\n }\n const next = this.$refs[\"periodeditor-\" + (pi.period + 1)];\n if (next && next[0]) {\n next[0].refresh();\n }\n },\n addDay(date, days) {\n if (days === undefined) {\n days = 1;\n }\n return addDays(date, days);\n },\n subDay(date, days) {\n if (days === undefined) {\n days = 1;\n }\n return addDays(date, 0 - days);\n },\n toolboxSwitched(event) {\n this.$emit('toggletoolbox', event);\n },\n pagecreated(page) {\n this.value.pages.push(page);\n },\n selectedpageChanged(newTabIndex /* , prevTabIndex*/) {\n const page = this.value.pages[newTabIndex];\n this.$emit('pagechanged', page);\n },\n sumLineLayers(idx, page) {\n if (idx < 0 || page.studylines.count == 0) {\n return 0;\n } else {\n let sum = 0;\n for (let i = 0; i < idx; i++) {\n sum += this.countLineLayers(page.studylines[i], page) + 1;\n }\n return sum;\n }\n },\n span(line, slot, layer) {\n let span = 1;\n for (const course of line.slots[slot].courses) {\n if (course.slot == slot && course.layer == layer) {\n span = course.span;\n }\n }\n return span;\n },\n onDrop(event, line, slot) {\n debug.info(\"dropping\", event, line, slot);\n const self = this;\n if (event.type.component) { // Double check in case filter fails\n debug.info(\"Adding new component\");\n if (event.type.type == \"gradable\") {\n // Determine first available layer;\n const lineslot = line.slots[slot].courses;\n let nextlayer = 0;\n for (const itm of lineslot) {\n if (itm.layer >= nextlayer) {\n nextlayer = itm.layer + 1;\n }\n }\n\n call([{\n methodname: 'local_treestudyplan_add_studyitem',\n args: {\n \"line_id\": line.id,\n \"slot\": slot,\n \"layer\": nextlayer,\n \"type\": 'course',\n \"details\": {\n \"competency_id\": null,\n 'conditions': '',\n 'course_id': event.data.id,\n 'badge_id': null,\n 'continuation_id': null,\n }\n }\n }])[0].then((response) => {\n let item = response;\n lineslot.push(item);\n self.$emit(\"input\", self.value);\n\n // Call the validate period function on next tick,\n // since it paints the item in the slot first\n this.$nextTick(() => {\n if (this.$refs.timingChecker) {\n this.$refs.timingChecker.validateCoursePeriod();\n }\n });\n ItemEventBus.$emit('coursechange');\n return;\n }).catch(notification.exception);\n } else if (event.type.type == \"filter\") {\n debug.info(\"Adding new filter compenent\");\n // Determine first available layer;\n const lineslot = line.slots[slot].filters;\n let nextlayer = 0;\n for (const itm of lineslot) {\n if (itm.layer >= nextlayer) {\n nextlayer = itm.layer + 1;\n }\n }\n call([{\n methodname: 'local_treestudyplan_add_studyitem',\n args: {\n \"line_id\": line.id,\n \"slot\": slot,\n \"type\": event.data.type,\n \"layer\": nextlayer,\n \"details\": {\n \"badge_id\": event.data.badge ? event.data.badge.id : undefined,\n }\n }\n }])[0].then((response) => {\n let item = response;\n lineslot.push(item);\n self.$emit(\"input\", self.value);\n return;\n }).catch(notification.exception);\n }\n }\n },\n checkTypeCourse(type) {\n if (type.type == \"gradable\") {\n if (settings(\"hivizdropslots\") && !type.item) {\n return true;\n } else {\n return false;\n }\n } else {\n return false;\n }\n },\n checkTypeFilter(type) {\n if (type.type == \"filter\") {\n if (settings(\"hivizdropslots\") && !type.item) {\n return true;\n } else {\n return false;\n }\n } else {\n return false;\n }\n }\n },\n template:\n `\n
\n \n
\n
\n {{ text.studyline_editmode }}\n {{ text.toolbox_toggle}}\n \n \n
\n
\n \n \n \n \n  {{text.associations}}\n \n \n  {{text.edit}}\n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n \n \n \n
\n
\n \n
\n \n \n \n
\n \n
\n
\n \n \n\n \n \n