/* eslint no-var: "error"*/
/* eslint no-console: "off"*/
/* eslint no-unused-vars: warn */
/* eslint max-len: ["error", { "code": 160 }] */
/* eslint no-trailing-spaces: warn */
/* eslint max-depth: ["error", 6] */
/* eslint-env es6*/
import {SimpleLine} from "./simpleline/simpleline";
import {call} from 'core/ajax';
import notification from 'core/notification';
import {loadStringKeys, loadStrings, strformat} from './util/string-helper';
import {formatDate, addDays, datespaninfo} from './util/date-helper';
import {objCopy, transportItem} from './studyplan-processor';
import Debugger from './util/debugger';
import Config from 'core/config';
import {download, upload} from './downloader';
import {processStudyplan, processStudyplanPage} from './studyplan-processor';
import {premiumenabled} from "./util/premium";
import FitTextVue from './util/fittext-vue';
import {settings} from "./util/settings";
import TSComponents from './treestudyplan-components';
import mFormComponents from "./util/mform-helper";
import pSideBarComponents from "./util/psidebar-vue";
import {Drag, Drop, DropList} from './vue-easy-dnd/vue-easy-dnd.esm';
const STUDYPLAN_EDITOR_FIELDS =
['name', 'shortname', 'description', 'idnumber', 'context_id', 'aggregation', 'aggregation_config'];
const PERIOD_EDITOR_FIELDS =
['fullname', 'shortname', 'startdate', 'enddate'];
const LINE_GRAVITY = 1.3;
export default {
install(Vue/* ,options */) {
Vue.component('drag', Drag);
Vue.component('drop', Drop);
Vue.component('drop-list', DropList);
Vue.use(TSComponents);
Vue.use(mFormComponents);
Vue.use(pSideBarComponents);
Vue.use(FitTextVue);
let debug = new Debugger("treestudyplan-editor");
/* **********************************
* *
* Treestudyplan Editor components *
* *
* **********************************/
/**
* Check if element is visible
* @param {Object} elem The element to check
* @returns {boolean} True if visible
*/
function isVisible(elem) {
return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
}
// Create new eventbus for interaction between item components
const ItemEventBus = new Vue();
/*
// Add event listener for the edit mode event so we can react to it, or at the very least ignore it
document.addEventListener(editSwEventTypes.editModeSet,(e) => {
e.preventDefault();
ItemEventBus.$emit('editModeSet', e.detail.editMode);
});
*/
let stringKeys = loadStringKeys({
conditions: [
{value: 'ALL', textkey: 'condition_all'},
{value: 'ANY', textkey: 'condition_any'},
],
});
let strings = loadStrings({
studyplanText: {
'studyline_editmode': 'studyline_editmode',
'toolbox_toggle': 'toolbox_toggle',
'editmode_modules_hidden': 'editmode_modules_hidden',
'studyline_add': 'studyline_add',
add: 'add@core',
edit: 'edit@core',
'delete': "delete@core",
'studyline_name': 'studyline_name',
'studyline_name_ph': 'studyline_name_ph',
'studyline_shortname': 'studyline_shortname',
'studyline_shortname_ph': 'studyline_shortname_ph',
'studyline_enrollable': 'studyline_enrollable',
'studyline_enrolroles': 'studyline_enrolroles',
'studyline_color': 'studyline_color',
associations: 'associations',
'associated_cohorts': 'associated_cohorts',
'associated_users': 'associated_users',
'studyline_edit': 'studyline_edit',
'studyplan_name': 'studyplan_name',
'studyplan_name_ph': 'studyplan_name_ph',
'studyplan_shortname': 'studyplan_shortname',
'studyplan_shortname_ph': 'studyplan_shortname_ph',
'studyplan_description': 'studyplan_description',
'studyplan_description_ph': 'studyplan_description_ph',
'studyplan_idnumber': 'studyplan_idnumber',
'studyplan_idnumber_ph': 'studyplan_idnumber_ph',
'studyplan_slots': 'studyplan_slots',
'studyplan_startdate': 'studyplan_startdate',
'studyplan_enddate': 'studyplan_enddate',
'line_enrollable_0': 'line_enrollable:0',
'line_enrollable_1': 'line_enrollable:1',
'line_enrollable_2': 'line_enrollable:2',
'line_enrollable_3': 'line_enrollable:3',
drophere: 'drophere',
studylineConfirmRemove: 'studyline_confirm_remove',
studyplanConfirmRemove: 'studyplan_confirm_remove',
},
studyplanAdvanced: {
'advanced_tools': 'advanced_tools',
'confirm_cancel': 'confirm_cancel',
'confirm_ok': 'confirm_ok',
success: 'success@core',
error: 'failed@completion',
'advanced_converted': 'advanced_converted',
'advanced_skipped': 'advanced_skipped',
'advanced_failed': 'advanced_failed',
'advanced_locked': 'advanced_locked',
'advanced_multiple': 'advanced_multiple',
'advanced_error': 'advanced_error',
'advanced_tools_heading': 'advanced_tools_heading',
'advanced_warning_title': 'advanced_warning_title',
'advanced_warning': 'advanced_warning',
'advanced_pick_scale': 'advanced_pick_scale',
'advanced_course_manipulation_title': 'advanced_course_manipulation_title',
'advanced_bulk_course_timing': 'advanced_bulk_course_timing',
'advanced_bulk_course_timing_desc': 'advanced_bulk_course_timing_desc',
'advanced_force_scale_title': 'advanced_force_scale_title',
'advanced_force_scale_desc': 'advanced_force_scale_desc',
'advanced_force_scale_button': 'advanced_force_scale_button',
'advanced_confirm_header': 'advanced_confirm_header',
'advanced_force_scale_confirm': 'advanced_force_scale_confirm',
'advanced_backup_restore': 'advanced_backup_restore',
'advanced_restore': 'advanced_restore',
'advanced_backup': 'advanced_backup',
'advanced_restore_pages': 'advanced_restore_pages',
'advanced_restore_lines': 'advanced_restore_lines',
'advanced_backup_plan': 'advanced_backup_plan',
'advanced_backup_page': 'advanced_backup_page',
'advanced_export': 'advanced_export',
'advanced_export_csv_plan': 'advanced_export_csv_plan',
'advanced_export_csv_page': 'advanced_export_csv_page',
'advanced_import_from_file': 'advanced_import_from_file',
'advanced_purge': "advanced_purge",
'advanced_purge_plan': "advanced_purge_plan",
'advanced_purge_plan_expl': "advanced_purge_plan_expl",
'advanced_purge_page': "advanced_purge_page",
'advanced_purge_page_expl': "advanced_purge_page_expl",
'advanced_cascade_cohortsync_title': "advanced_cascade_cohortsync_title",
'advanced_cascade_cohortsync_desc': "advanced_cascade_cohortsync_desc",
'advanced_cascade_cohortsync': "advanced_cascade_cohortsync",
currentpage: "currentpage",
},
studyplanEdit: {
studyplanEdit: 'studyplanEdit',
'studyplan_add': 'studyplan_add',
'studyplanpage_add': 'studyplanpage_add',
'studyplanpage_edit': 'studyplanpage_edit',
'info_periodsextended': 'studyplanpage_info_periodsextended',
warning: 'warning@core',
},
periodEdit: {
edit: 'period_edit',
fullname: 'studyplan_name',
shortname: 'studyplan_shortname',
startdate: 'studyplan_startdate',
enddate: 'studyplan_enddate',
},
courseTiming: {
title: 'course_timing_title',
desc: 'course_timing_desc',
question: 'course_timing_question',
warning: 'course_timing_warning',
'timing_ok': 'course_timing_ok',
'timing_off': 'course_timing_off',
course: 'course@core',
period: 'period',
yes: 'yes$core',
no: 'no$core',
duration: 'duration',
years: 'years$core',
year: 'year$core',
weeks: 'weeks$core',
week: 'week$core',
days: 'days$core',
day: 'day$core',
rememberchoice: 'course_timing_rememberchoice',
hidewarning: 'course_timing_hidewarning',
periodspan: 'course_period_span',
periods: 'periods',
'periodspan_desc': 'course_period_span_desc',
},
studyplanAssociate: {
'associations': 'associations',
'associated_cohorts': 'associated_cohorts',
'associated_users': 'associated_users',
'associated_coaches': 'associated_coaches',
'associate_cohorts': 'associate_cohorts',
'associate_users': 'associate_users',
'associate_coached': 'associate_coaches',
'add_association': 'add_association',
'delete_association': 'delete_association',
'associations_empty': 'associations_empty',
'associations_search': 'associations_search',
cohorts: 'cohorts',
users: 'users',
coaches: 'coaches',
selected: 'selected',
name: 'name',
context: 'context',
search: 'search',
},
itemText: {
'select_conditions': "select_conditions",
'item_configuration': "item_configuration",
ok: "ok@core",
'delete': "delete@core",
'item_delete_message': "item_delete_message",
'type_course': "course@core",
'type_junction': "tool-junction",
'type_start': "tool-start",
'type_finish': "tool-finish",
'type_badge': "tool-badge",
'type_invalid': "course-invalid",
},
itemCourseText: {
'select_conditions': "select_conditions",
'select_grades': "select_grades",
'coursetiming_past': "coursetiming_past",
'coursetiming_present': "coursetiming_present",
'coursetiming_future': "coursetiming_future",
'grade_include': "grade_include",
'grade_require': "grade_require",
ok: "ok@core",
cancel: "cancel@core",
'delete': "delete@core",
noenddate: "noenddate",
},
invalid: {
error: 'error',
},
completion: {
'completion_completed': "completion_completed",
'completion_incomplete': "completion_incomplete",
'aggregation_all': "aggregation_all",
'aggregation_any': "aggregation_any",
'aggregation_overall_all': "aggregation_overall_all",
'aggregation_overall_any': "aggregation_overall_any",
'completion_not_configured': "completion_not_configured",
'configure_completion': "configure_completion",
},
competency: {
'competency_not_configured': "competency_not_configured",
'configure_competency': "configure_competency",
when: "when",
required: "required",
points: "points@core_grades",
heading: "competency_heading",
details: "competency_details",
},
badge: {
'share_badge': "share_badge",
dateissued: "dateissued",
dateexpire: "dateexpire",
badgeinfo: "badgeinfo",
},
toolbox: {
toolbox: 'toolbox',
toolbarRight: 'toolbar-right',
courses: 'courses',
flow: 'flow',
toolJunction: 'tool-junction',
toolFinish: 'tool-finish',
toolStart: 'tool-start',
badges: 'badges',
relatedbages: 'relatedbages@badges',
filter: 'filter@core',
sitebadges: 'sitebadges@badges',
}
});
/*
* T-STUDYPLAN-ADVANCED
*/
Vue.component('t-studyplan-advanced', {
props: {
value: {
type: Object,
default() {
return null;
},
},
selectedpage: {
type: Object,
default() {
return null;
},
}
},
data() {
return {
forceScales: {
selectedScale: null,
result: [],
},
text: strings.studyplanAdvanced,
};
},
computed: {
scales() {
return [{
id: null,
disabled: true,
name: this.text.advanced_pick_scale,
}].concat(this.value.advanced.force_scales.scales);
},
},
methods: {
forceScalesStart() {
// Set confirmation box
const self = this;
this.$bvModal.msgBoxConfirm(this.text.advanced_force_scale_confirm, {
title: this.text.advanced_force_scale_confirm,
okVariant: 'danger',
okTitle: this.text.confirm_ok,
cancelTitle: this.text.confirm_cancel,
}).then(value => {
if (value == true) {
call([{
methodname: 'local_treestudyplan_force_studyplan_scale',
args: {
'studyplan_id': this.value.id,
'scale_id': this.forceScales.selectedScale,
}
}])[0].then((response) => {
self.forceScales.result = response;
return;
}).catch(notification.exception);
}
return;
}).catch(notification.exception);
},
exportPage(format) {
const self = this;
if (format == undefined || !["json", "csv"].includes(format)) {
format = "json";
}
call([{
methodname: 'local_treestudyplan_export_page',
args: {
'page_id': this.selectedpage.id,
format: format,
},
}])[0].then((response) => {
download(self.value.shortname + ".page." + format, response.content, response.format);
return;
}).catch(notification.exception);
},
exportPlan() {
const self = this;
call([{
methodname: 'local_treestudyplan_export_plan',
args: {
'studyplan_id': this.value.id,
format: "json",
},
}])[0].then((response) => {
download(self.value.shortname + ".plan.json", response.content, response.format);
return;
}).catch(notification.exception);
},
bulkCourseTiming() {
const self = this;
call([{
methodname: 'local_treestudyplan_bulk_course_timing',
args: {
'page_id': this.selectedpage.id,
},
}])[0].then((response) => {
if (response.success) {
// Reloading the webpage saves trouble reloading the specific page updated.
location.reload();
} else {
self.$bvModal.msgBoxOk(response.msg, {title: "Could not set bulk course timing"});
debug.error("Could not set bulk course timing: ", response.msg);
}
return;
}).catch(notification.exception);
},
importStudylines() {
const self = this;
upload((filename, content)=>{
call([{
methodname: 'local_treestudyplan_import_studylines',
args: {
'page_id': this.selectedpage.id,
content: content,
format: "application/json",
},
}])[0].then((response) => {
if (response.success) {
location.reload();
} else {
self.$bvModal.msgBoxOk(response.msg, {title: "Import failed"});
debug.error("Import failed: ", response.msg);
}
return;
}).catch(notification.exception);
}, "application/json");
},
importPages() {
const self = this;
upload((filename, content)=>{
call([{
methodname: 'local_treestudyplan_import_pages',
args: {
'studyplan_id': this.value.id,
content: content,
format: "application/json",
},
}])[0].then((response) => {
if (response.success) {
location.reload();
} else {
self.$bvModal.msgBoxOk(response.msg, {title: "Import failed"});
debug.error("Import failed: ", response.msg);
}
return;
}).catch(notification.exception);
}, "application/json");
},
purgeStudyplan() {
const self = this;
call([{
methodname: 'local_treestudyplan_delete_studyplan',
args: {
id: this.value.id,
force: true,
},
}])[0].then((response) => {
if (response.success) {
location.reload();
} else {
self.$bvModal.msgBoxOk(response.msg, {title: "Could not delete plan "});
debug.error("Could not delete plan: ", response.msg);
}
return;
}).catch(notification.exception);
},
purgeStudyplanpage() {
const self = this;
if (this.selectedpage) {
call([{
methodname: 'local_treestudyplan_delete_studyplanpage',
args: {
id: this.selectedpage.id,
force: true,
},
}])[0].then((response) => {
if (response.success) {
location.reload();
} else {
self.$bvModal.msgBoxOk(response.msg, {title: "Could not delete page"});
debug.error("Could not delete page: ", response.msg);
}
return;
}).catch(notification.exception);
}
},
cascadeCohortsync() {
const self = this;
call([{
methodname: 'local_treestudyplan_cascade_cohortsync',
args: {
'studyplan_id': this.value.id,
},
}])[0].then((response) => {
self.$bvModal.msgBoxOk(response.success ? self.text.success : self.text.error,
{title: self.text.advanced_cascade_cohortsync});
return;
}).catch(notification.exception);
},
modalClose() {
this.forceScales.result = [];
}
},
template:
`
{{text.advanced_tools}}
{{ text.advanced_warning}}
{{ text.advanced_cascade_cohortsync_title}}
{{ text.advanced_cascade_cohortsync_desc}}
{{ text.advanced_cascade_cohortsync}}
{{ text.advanced_bulk_course_timing}}
{{ text.advanced_bulk_course_timing_desc}}
{{text.currentpage}} {{selectedpage.fullname}}
{{ text.advanced_bulk_course_timing}}
{{ text.advanced_force_scale_title}}
{{ text.advanced_force_scale_desc}}
{{ text.advanced_force_scale_button}}
{{ text.advanced_backup }}
{{ text.advanced_backup_page }}
{{text.currentpage}} {{selectedpage.fullname}}
{{ text.advanced_backup_plan }}
{{ text.advanced_restore }}
{{ text.advanced_restore_lines}}
{{ text.advanced_restore_pages }}
{{ text.advanced_export }}
{{ text.advanced_export_csv_page }}
{{text.currentpage}} {{selectedpage.fullname}}
{{text.advanced_purge_page_expl}}
{{text.currentpage}} {{selectedpage.fullname}}
{{ text.advanced_purge_page}}
{{text.advanced_purge_plan_expl}}
{{ text.advanced_purge_plan}}
`
});
/*
* T-STUDYPLAN-EDIT
*/
Vue.component('t-studyplan-edit', {
props: {
'value': {
type: Object,
default() {
return null;
},
},
'mode': {
type: String,
default() {
return "edit";
},
},
'type': {
type: String,
default() {
return "link";
},
},
'variant': {
type: String,
default() {
return "";
},
},
'contextid': {
type: Number,
'default': 1
},
},
data() {
return {
text: strings.studyplanEdit,
};
},
computed: {
},
methods: {
planSaved(updatedplan) {
const self = this;
debug.info("Got new plan data", updatedplan);
if (self.mode == 'create') {
// Inform parent of the details of the newly created plan
self.$emit("created", updatedplan);
} else {
// Determine if the plan moved context...
const movedFrom = self.value.context_id;
const movedTo = updatedplan.context_id;
const moved = (movedFrom != movedTo);
if (updatedplan.pages[0].periods != self.value.pages[0].periods) {
// If the pages changed, just reload the entire model for the plan
call([{
methodname: 'local_treestudyplan_get_studyplan_map',
args: {id: self.value.id}
}])[0].then((response) => {
self.value = processStudyplan(response, true);
debug.info('studyplan processed');
self.$emit('input', self.value);
return;
}).catch(function(error) {
notification.exception(error);
});
} else {
// Copy updated fields and trigger update
objCopy(self.value, updatedplan, STUDYPLAN_EDITOR_FIELDS);
self.$emit('input', self.value);
if (moved) {
self.$emit('moved', self.value, movedFrom, movedTo);
}
}
}
},
},
template:
`
`
});
/*
* T-STUDYPLAN-EDIT
*/
Vue.component('t-studyplan-page-edit', {
props: {
'value': {
type: Object,
default() {
return null;
},
},
'mode': {
type: String,
default() {
return "edit";
},
},
'type': {
type: String,
default() {
return "link";
},
},
'variant': {
type: String,
default() {
return "";
},
},
'studyplan': {
type: Object,
},
},
data() {
return {
text: strings.studyplanEdit,
};
},
computed: {
},
methods: {
planSaved(updatedpage) {
const self = this;
if (self.mode == 'create') {
// Inform parent of the details of the newly created plan
self.$emit("created", updatedpage);
} else {
const page = processStudyplanPage(updatedpage);
debug.info('studyplan page processed');
if (self.value.periods < page.periods) {
this.$bvModal.msgBoxOk(this.text.info_periodsextended, {
title: this.text.warning,
okVariant: 'success',
centered: true
});
}
self.$emit('input', page);
}
},
},
template:
`
`
});
/*
* T-STUDYPLAN-ASSOCIATE
*/
Vue.component('t-studyplan-associate', {
props: ['value'],
data() {
return {
show: false,
config: {
userfields: [
{key: "selected"},
{key: "firstname", "sortable": true},
{key: "lastname", "sortable": true},
],
cohortfields: [
{key: "selected"},
{key: "name", "sortable": true},
{key: "context", "sortable": true},
]
},
association: {
cohorts: [],
users: [],
coaches: []
},
loading: {
cohorts: false,
users: false,
coaches: false,
},
search: {users: [], cohorts: [], coaches: []},
selected: {
search: {users: [], cohorts: [], coaches: []},
associated: {users: [], cohorts: [], coaches: []}
},
text: strings.studyplanAssociate,
};
},
methods: {
premiumenabled,
showModal() {
this.show = true;
this.loadAssociations();
},
cohortOptionModel(c) {
return {
value: c.id,
text: c.name + ' (' + c.context.path.join(' / ') + ')',
};
},
userOptionModel(u) {
return {
value: u.id,
text: u.firstname + ' ' + u.lastname,
};
},
loadAssociations() {
const self = this;
self.loading.cohorts = true;
self.loading.users = true;
call([{
methodname: 'local_treestudyplan_associated_users',
args: {
'studyplan_id': self.value.id,
}
}])[0].then((response) => {
self.association.users = response.map(self.userOptionModel);
self.loading.users = false;
return;
}).catch(notification.exception);
call([{
methodname: 'local_treestudyplan_associated_cohorts',
args: {
'studyplan_id': self.value.id,
}
}])[0].then((response) => {
self.association.cohorts = response.map(self.cohortOptionModel);
self.loading.cohorts = false;
return;
}).catch(notification.exception);
if (premiumenabled()) {
self.loading.coaches = true;
call([{
methodname: 'local_treestudyplan_associated_coaches',
args: {
'studyplan_id': self.value.id,
}
}])[0].then((response) => {
self.association.coaches = response.map(self.userOptionModel);
self.loading.coaches = false;
return;
}).catch(notification.exception);
}
},
searchCohorts(searchtext) {
const self = this;
if (searchtext.length > 0) {
call([{
methodname: 'local_treestudyplan_list_cohort',
args: {
like: searchtext,
'studyplan_id': self.value.id
}
}])[0].then((response) => {
self.search.cohorts = response.map(self.cohortOptionModel);
return;
}).catch(notification.exception);
} else {
self.search.cohorts = [];
}
},
cohortAssociate() {
const self = this;
let requests = [];
const associated = self.association.cohorts;
const search = self.search.cohorts;
const searchselected = self.selected.search.cohorts;
for (const i in searchselected) {
const r = searchselected[i];
call([{
methodname: 'local_treestudyplan_connect_cohort',
args: {
'studyplan_id': self.value.id,
'cohort_id': r,
},
}])[0].then((response) => {
if (response.success) {
transportItem(associated, search, r);
}
return;
}).catch(notification.exception);
}
call(requests);
},
cohortDisassociate() {
const self = this;
const associatedselected = self.selected.associated.cohorts;
const associated = self.association.cohorts;
const search = self.search.cohorts;
for (const i in associatedselected) {
const r = associatedselected[i];
call([{
methodname: 'local_treestudyplan_disconnect_cohort',
args: {
'studyplan_id': self.value.id,
'cohort_id': r,
}
}])[0].then((response) => {
if (response.success) {
transportItem(search, associated, r);
}
return;
}).catch(notification.exception);
}
},
searchUsers(searchtext) {
const self = this;
if (searchtext.length > 0) {
call([{
methodname: 'local_treestudyplan_find_user',
args: {
like: searchtext,
'studyplan_id': self.value.id
}
}])[0].then((response) => {
self.search.users = response.map(self.userOptionModel);
return;
}).catch(notification.exception);
} else {
self.search.users = [];
}
},
userAssociate() {
const self = this;
const associated = self.association.users;
const search = self.search.users;
const searchselected = self.selected.search.users;
for (const i in searchselected) {
const r = searchselected[i];
call([{
methodname: 'local_treestudyplan_connect_user',
args: {
'studyplan_id': self.value.id,
'user_id': r,
},
}])[0].then((response) => {
if (response.success) {
transportItem(associated, search, r);
}
return;
}).catch(notification.exception);
}
},
userDisassociate() {
const self = this;
const associated = self.association.users;
const associatedselected = self.selected.associated.users;
const search = self.search.users;
for (const i in associatedselected) {
const r = associatedselected[i];
call([{
methodname: 'local_treestudyplan_disconnect_user',
args: {
'studyplan_id': self.value.id,
'user_id': r,
}
}])[0].then((response) => {
if (response.success) {
transportItem(search, associated, r);
}
return;
}).catch(notification.exception);
}
},
searchCoaches(searchtext) {
if (premiumenabled()) {
const self = this;
if (searchtext.length > 0) {
call([{
methodname: 'local_treestudyplan_find_coach',
args: {
like: searchtext,
'studyplan_id': self.value.id,
}
}])[0].then((response) => {
self.search.coaches = response.map(self.userOptionModel);
return;
}).catch(notification.exception);
} else {
self.search.coaches = [];
}
}
},
coachAssociate() {
if (premiumenabled()) {
const self = this;
const associated = self.association.coaches;
const search = self.search.coaches;
const searchselected = self.selected.search.coaches;
for (const i in searchselected) {
const r = searchselected[i];
call([{
methodname: 'local_treestudyplan_connect_coach',
args: {
'studyplan_id': self.value.id,
'user_id': r,
},
}])[0].then((response) => {
if (response.success) {
transportItem(associated, search, r);
}
return;
}).catch(notification.exception);
}
}
},
coachDisassociate() {
if (premiumenabled()) {
const self = this;
const associated = self.association.coaches;
const associatedselected = self.selected.associated.coaches;
const search = self.search.coaches;
for (const i in associatedselected) {
const r = associatedselected[i];
call([{
methodname: 'local_treestudyplan_disconnect_coach',
args: {
'studyplan_id': self.value.id,
'user_id': r,
}
}])[0].then((response) => {
if (response.success) {
transportItem(search, associated, r);
}
return;
}).catch(notification.exception);
}
}
},
},
template:
`
{{text.associated_cohorts}}
{{text.associate_cohorts}}
{{text.delete_association}}
{{text.add_association}}
{{text.associated_users}}
{{text.associate_users}}
{{text.delete_association}}
{{text.add_association}}
{{text.associated_coaches}}
{{text.associate_coaches}}
{{text.delete_association}}
{{text.add_association}}
`
});
/* * ****************
*
* Period editor
*
*************/
Vue.component('t-period-edit', {
props: {
'value': {
type: Object,
default() {
return null;
},
},
'type': {
type: String,
default() {
return "link";
},
},
'variant': {
type: String,
default() {
return "";
},
},
'minstart': {
type: String,
default() {
return null;
},
},
'maxend': {
type: String,
default() {
return null;
},
}
},
data() {
return {
show: false,
editdata: {
fullname: '',
shortname: '',
startdate: (new Date()).getFullYear() + '-08-01',
enddate: ((new Date()).getFullYear() + 1) + '-08-01',
},
text: strings.periodEdit,
};
},
methods: {
editStart() {
objCopy(this.editdata, this.value, PERIOD_EDITOR_FIELDS);
this.show = true;
},
editFinish() {
const self = this;
let args = {'id': this.value.id};
objCopy(args, this.editdata, PERIOD_EDITOR_FIELDS);
call([{
methodname: 'local_treestudyplanEdit_period',
args: args
}])[0].then((response) => {
objCopy(self.value, response, PERIOD_EDITOR_FIELDS);
self.$emit('input', self.value);
self.$emit('edited', self.value);
return;
}).catch(notification.exception);
},
refresh() {
const self = this;
call([{
methodname: 'local_treestudyplan_get_period',
args: {'id': this.value.id},
}])[0].then((response) => {
objCopy(self.value, response, PERIOD_EDITOR_FIELDS);
self.$emit('input', self.value);
return;
}).catch(notification.exception);
},
addDay(date, days) {
if (days === undefined) {
days = 1;
}
return addDays(date, days);
},
subDay(date, days) {
if (days === undefined) {
days = 1;
}
return addDays(date, 0 - days);
},
},
template:
`
{{ text.fullname}}
{{ text.shortname}}
{{ text.studyplan_startdate}}
{{ text.studyplan_enddate}}
`
});
// TAG: Start studyplan component
/*
* T-STUDYPLAN
*/
Vue.component('t-studyplan', {
props: {
'value': {
type: Object,
},
'coaching': {
type: Boolean,
'default': false,
},
},
data() {
return {
config: {
userfields: [
{key: "selected"},
{key: "firstname", "sortable": true},
{key: "lastname", "sortable": true},
],
cohortfields: [
{key: "selected"},
{key: "name", "sortable": true},
{key: "context", "sortable": true},
]
},
create: {
studyline: {
name: '',
shortname: '',
color: '#DDDDDD',
enrol: {
enrollable: 0,
enrolroles: [],
}
},
page: {
id: -1,
name: '',
shortname: ''
}
},
edit: {
'toolbox_shown': false,
studyline: {
editmode: false,
data: {
name: '',
shortname: '',
color: '#DDDDDD',
enrol: {
enrollable: 0,
enrolroles: [],
}
},
original: {},
availableroles: [],
},
studyplan: {
data: {
name: '',
shortname: '',
description: '',
slots: 4,
startdate: '2020-08-01',
enddate: '',
aggregation: '',
'aggregation_config': '',
'aggregation_info': {
useRequiredGrades: true,
useItemCondition: false,
},
},
original: {},
}
},
text: strings.studyplanText,
cache: {
linelayers: {},
},
selectedpageindex: 0,
emptyline: {
id: -1,
name: '',
shortname: '',
color: '#FF0000',
filterslots: [{}],
courseslots: [{}]
},
availableroles: [],
};
},
created() {
const self = this;
// Listener for the signal that a new connection was made and needs to be drawn
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
ItemEventBus.$on('coursechange', () => {
self.$emit('pagechanged', this.selectedpage);
});
},
mounted() {
const self = this;
if (this.value.pages[0].studylines.length == 0 && !this.coaching) {
// Start in editmode if studylines on first page are empty
this.edit.studyline.editmode = true;
}
if (!self.coaching) {
// Retrieve available roles (only needed as manager)
call([{
methodname: 'local_treestudyplan_list_roles',
args: {
'studyplan_id': this.value.id,
}
}])[0].then((response) => {
self.availableroles = response;
return;
}).catch(notification.exception);
}
this.$root.$emit('redrawLines');
this.$emit('pagechanged', this.selectedpage);
},
updated() {
this.$root.$emit('redrawLines');
ItemEventBus.$emit('redrawLines');
},
computed: {
selectedpage() {
return this.value.pages[this.selectedpageindex];
},
hivizdrop() {
return settings("hivizdropslots");
},
},
methods: {
premiumenabled,
columns(page) {
return 1 + (page.periods * 2);
},
columnsStylerule(page) {
// Uses css variables, so width for slots and filters can be configured in css
let s = "grid-template-columns: var(--studyplan-filter-width)"; // Use css variable here
for (let i = 0; i < page.periods; i++) {
s += " var(--studyplan-course-width) var(--studyplan-filter-width)";
}
return s + ";";
},
trashbinAccepts(type) {
if (type.item) {
return true;
} else {
return false;
}
},
countLineLayers(line, page) {
// For some optimization, we cache the value of this calculation for about a second
// Would be a lot nicer if we could use a computed property for this.....
if (this.cache.linelayers[line.id]
&& ((new Date()) - this.cache.linelayers[line.id].timestamp < 1000)
) {
return this.cache.linelayers[line.id].value;
} else {
let maxLayer = -1;
for (let i = 0; i <= page.periods; i++) {
if (line.slots[i]) {
// Determine the amount of used layers in a studyline slot
for (const ix in line.slots[i].courses) {
const item = line.slots[i].courses[ix];
if (item.layer > maxLayer) {
maxLayer = item.layer;
}
}
for (const ix in line.slots[i].filters) {
const item = line.slots[i].filters[ix];
if (item.layer > maxLayer) {
maxLayer = item.layer;
}
}
}
}
this.cache.linelayers[line.id] = {
value: (maxLayer + 1),
timestamp: (new Date()),
};
return maxLayer + 1;
}
},
slotsempty(slots) {
if (Array.isArray(slots)) {
let count = 0;
for (let i = 0; i < slots.length; i++) {
if (Array.isArray(slots[i].courses)) {
count += slots[i].courses.length;
}
if (Array.isArray(slots[i].filters)) {
count += slots[i].filters.length;
}
}
return (count == 0);
} else {
return false;
}
},
movedStudyplan(plan, from, to) {
this.$emit('moved', plan, from, to); // Throw the event up....
},
addStudyLine(page, newlineinfo) {
call([{
methodname: 'local_treestudyplan_add_studyline',
args: {
'page_id': page.id,
'name': newlineinfo.name,
'shortname': newlineinfo.shortname,
'color': newlineinfo.color,
'sequence': page.studylines.length,
'enrollable': newlineinfo.enrol.enrollable,
'enrolroles': newlineinfo.enrol.enrolroles
}
}])[0].then((response) => {
page.studylines.push(response);
newlineinfo.name = '';
newlineinfo.shortname = '';
newlineinfo.color = "#dddddd";
newlineinfo.enrol.enrollable = 0;
newlineinfo.enrol.enrolroles = [];
return;
}).catch(notification.exception);
},
editLineStart(line) {
const page = this.value.pages[this.selectedpageindex];
debug.info("Starting line edit", line);
Object.assign(this.edit.studyline.data, line);
this.edit.studyline.original = line;
this.$bvModal.show('modal-edit-studyline-' + page.id);
},
editLineFinish() {
let editedline = this.edit.studyline.data;
let originalline = this.edit.studyline.original;
call([{
methodname: 'local_treestudyplanEdit_studyline',
args: {'id': editedline.id,
'name': editedline.name,
'shortname': editedline.shortname,
'color': editedline.color,
'enrollable': editedline.enrol.enrollable,
'enrolroles': editedline.enrol.enrolroles
}
}])[0].then((response) => {
originalline.name = response.name;
originalline.shortname = response.shortname;
originalline.color = response.color;
originalline.enrol.enrollable = response.enrol.enrollable;
originalline.enrol.enrolroles = response.enrol.enrolroles;
return;
}).catch(notification.exception);
},
deleteLine(page, line) {
const self = this;
self.$bvModal.msgBoxConfirm(this.text.studylineConfirmRemove.replace('{$a}', line.name), {
okTitle: this.text.delete,
okVariant: 'danger',
}).then((modalresponse) => {
if (modalresponse) {
call([{
methodname: 'local_treestudyplan_delete_studyline',
args: {'id': line.id}
}])[0].then((response) => {
if (response.success == true) {
let index = page.studylines.indexOf(line);
page.studylines.splice(index, 1);
}
return;
}).catch(notification.exception);
}
return;
}).catch(notification.exception);
},
reorderLines(event, lines) {
// Apply reordering
event.apply(lines);
// Send the new sequence to the server
let sequence = [];
for (let idx in lines) {
sequence.push({'id': lines[idx].id, 'sequence': idx});
}
call([{
methodname: 'local_treestudyplan_reorder_studylines',
args: {'sequence': sequence}
}])[0].then(() => {
return;
}).catch(notification.exception);
},
deletePlan(studyplan) {
const self = this;
self.$bvModal.msgBoxConfirm(this.text.studyplabConfirmRemove.replace('{$a}', studyplan.name), {
okTitle: this.text.delete,
okVariant: 'danger',
}).then(function(modalresponse) {
if (modalresponse) {
call([{
methodname: 'local_treestudyplan_delete_studyplan',
args: {'id': studyplan.id, force: true}
}])[0].then((response) => {
if (response.success == true) {
self.$root.$emit("studyplanRemoved", studyplan);
}
return;
}).catch(notification.exception);
}
return;
}).catch(notification.exception);
},
deleteStudyItem(event) {
let item = event.data;
call([{
methodname: 'local_treestudyplan_delete_studyitem',
args: {'id': item.id}
}])[0].then((response) => {
if (response.success == true) {
event.source.$emit('cut', event);
}
return;
}).catch(notification.exception);
},
showslot(page, line, index, layeridx, type) {
// Check if the slot should be hidden because a previous slot has an item with a span
// so big that it hides this slot
const forGradable = (type == 'gradable') ? true : false;
const periods = page.periods;
let show = true;
for (let i = 0; i < periods; i++) {
if (line.slots[index - i] && line.slots[index - i].courses) {
const list = line.slots[index - i].courses;
for (const ix in list) {
const item = list[ix];
if (item.layer == layeridx) {
if (forGradable) {
if (i > 0 && (item.span - i) > 0) {
show = false;
}
} else {
if ((item.span - i) > 1) {
show = false;
}
}
}
}
}
}
return show;
},
periodEdited(pi) {
const prev = this.$refs["periodeditor-" + (pi.period - 1)];
if (prev && prev[0]) {
prev[0].refresh();
}
const next = this.$refs["periodeditor-" + (pi.period + 1)];
if (next && next[0]) {
next[0].refresh();
}
},
addDay(date, days) {
if (days === undefined) {
days = 1;
}
return addDays(date, days);
},
subDay(date, days) {
if (days === undefined) {
days = 1;
}
return addDays(date, 0 - days);
},
toolboxSwitched(event) {
this.$emit('toggletoolbox', event);
},
pagecreated(page) {
this.value.pages.push(page);
},
selectedpageChanged(newTabIndex /* , prevTabIndex*/) {
const page = this.value.pages[newTabIndex];
this.$emit('pagechanged', page);
},
sumLineLayers(idx, page) {
if (idx < 0 || page.studylines.count == 0) {
return 0;
} else {
let sum = 0;
for (let i = 0; i < idx; i++) {
sum += this.countLineLayers(page.studylines[i], page) + 1;
}
return sum;
}
},
span(line, slot, layer) {
let span = 1;
for (const course of line.slots[slot].courses) {
if (course.slot == slot && course.layer == layer) {
span = course.span;
}
}
return span;
},
onDrop(event, line, slot) {
debug.info("dropping", event, line, slot);
const self = this;
if (event.type.component) { // Double check in case filter fails
debug.info("Adding new component");
if (event.type.type == "gradable") {
// Determine first available layer;
const lineslot = line.slots[slot].courses;
let nextlayer = 0;
for (const itm of lineslot) {
if (itm.layer >= nextlayer) {
nextlayer = itm.layer + 1;
}
}
call([{
methodname: 'local_treestudyplan_add_studyitem',
args: {
"line_id": line.id,
"slot": slot,
"layer": nextlayer,
"type": 'course',
"details": {
"competency_id": null,
'conditions': '',
'course_id': event.data.id,
'badge_id': null,
'continuation_id': null,
}
}
}])[0].then((response) => {
let item = response;
lineslot.push(item);
self.$emit("input", self.value);
// Call the validate period function on next tick,
// since it paints the item in the slot first
this.$nextTick(() => {
if (this.$refs.timingChecker) {
this.$refs.timingChecker.validateCoursePeriod();
}
});
ItemEventBus.$emit('coursechange');
return;
}).catch(notification.exception);
} else if (event.type.type == "filter") {
debug.info("Adding new filter compenent");
// Determine first available layer;
const lineslot = line.slots[slot].filters;
let nextlayer = 0;
for (const itm of lineslot) {
if (itm.layer >= nextlayer) {
nextlayer = itm.layer + 1;
}
}
call([{
methodname: 'local_treestudyplan_add_studyitem',
args: {
"line_id": line.id,
"slot": slot,
"type": event.data.type,
"layer": nextlayer,
"details": {
"badge_id": event.data.badge ? event.data.badge.id : undefined,
}
}
}])[0].then((response) => {
let item = response;
lineslot.push(item);
self.$emit("input", self.value);
return;
}).catch(notification.exception);
}
}
},
checkTypeCourse(type) {
if (type.type == "gradable") {
if (settings("hivizdropslots") && !type.item) {
return true;
} else {
return false;
}
} else {
return false;
}
},
checkTypeFilter(type) {
if (type.type == "filter") {
if (settings("hivizdropslots") && !type.item) {
return true;
} else {
return false;
}
} else {
return false;
}
}
},
template:
`
`
});
/*
* T-STUDYLINE-HEADER
*/
Vue.component('t-studyline-heading', {
props: {
value: {
type: Object, // Studyline
default() {
return {};
},
},
layers: {
type: Number,
'default': 1,
},
},
data() {
return {
layerHeights: {}
};
},
created() {
// Listener for the signal that a new connection was made and needs to be drawn
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
ItemEventBus.$on('lineHeightChange', this.onLineHeightChange);
},
computed: {
},
methods: {
onLineHeightChange(lineid) { // Is called with parameters (lineid, layerid, newheight)
// All layers for this line have the first slot send an update message on layer height change.
// When one of those updates is received, record the height and recalculate the total height of the
// header
if (this.$refs.main && lineid == this.value.id) {
const items = document.querySelectorAll(
`.t-studyline-slot-0[data-studyline='${this.value.id}']`);
// Determine the height of all the lines and add them up.
let heightSum = 0;
items.forEach((el) => {
// Func getBoundingClientRect() Gets the actual fractional height instead of rounded to integer pixels
const r = el.getBoundingClientRect();
const height = r.height;
heightSum += height;
});
const heightStyle = `${heightSum}px`;
this.$refs.main.style.height = heightStyle;
}
}
},
template: `
`,
});
/*
* T-STUDYLINE (Used only for study line edit mode)
*/
Vue.component('t-studyline-edit', {
props: {
value: {
type: Object, // Studyline
default() {
return {};
},
}
},
data() {
return {
};
},
computed: {
deletable() {
// Check if all the slots are empty
const slots = this.value.slots;
if (Array.isArray(slots)) {
let count = 0;
for (let i = 0; i < slots.length; i++) {
if (Array.isArray(slots[i].courses)) {
count += slots[i].courses.length;
}
if (Array.isArray(slots[i].filters)) {
count += slots[i].filters.length;
}
}
return (count == 0);
} else {
return false;
}
},
editable() {
return true;
}
},
methods: {
onEdit() {
this.$emit('edit', this.value);
},
onDelete() {
this.$emit('delete', this.value);
},
},
template: `
`,
});
/*
* During a redisign it was decided to have the studyline still get the entire array as a value,
* even though it only shows one drop slot for the layer it is in. This is to make repainting easier,
* since we modify the array for the layer we handle. FIXME: Make this less weird
*/
Vue.component('t-studyline-slot', {
props: {
type: {
type: String,
'default': 'gradable',
},
slotindex: {
type: Number,
'default': '',
},
line: {
type: Object,
default() {
return null;
},
},
layer: {
type: Number,
},
value: {
type: Array, // Dict with layer as index
default() {
return [];
},
},
plan: {
type: Object, // Studyplan data
default() {
return null;
},
},
page: {
type: Object, // Studyplan data
default() {
return null;
},
},
period: {
type: Object, // Studyplan data
default() {
return null;
},
},
},
mounted() {
const self = this;
if (self.type == "gradable" && self.slotindex == 1) {
self.resizeListener = new ResizeObserver(() => {
if (self.$refs.main) {
const size = self.$refs.main.getBoundingClientRect();
ItemEventBus.$emit('lineHeightChange', self.line.id, self.layer, size.height);
}
}).observe(self.$refs.main);
}
},
unmounted() {
if (this.resizeListener) {
this.resizeListener.disconnect();
}
},
computed: {
slotkey() {
return `${this.type}'-'${this.line.id}-${this.slotindex}-${this.layer}`;
},
itemidx() {
for (const ix in this.value) {
const itm = this.value[ix];
if (itm.layer == this.layer) {
return ix;
}
}
return null;
},
item() {
for (const ix in this.value) {
const itm = this.value[ix];
if (itm.layer == this.layer) {
return itm;
}
}
return null;
},
listtype() {
return this.type;
},
courseHoverDummy() {
return {course: this.hover.component};
},
spanCss() {
if (this.item && this.item.span > 1) {
// Calculate span like this:
// const span = (2 * this.item.span) - 1;
return `width: 100%; `;
} else {
return "";
}
}
},
data() {
return {
text: strings.courseTiming,
plantext: strings.studyplanText,
resizeListener: null,
hover: {
component: null,
type: null,
},
datechanger: {
coursespan: null,
periodspan: null,
'default': false,
defaultchoice: false,
hidewarn: false,
}
};
},
methods: {
hivizdrop() {
return settings("hivizdropslots");
},
onDrop(event) {
this.hover.component = null;
this.hover.type = null;
debug.info(event);
const self = this;
if (event.type.item) {
let item = event.data;
// To avoid weird visuals with the lines,
// we add the item to the proper place in the front-end first
item.layer = this.layer;
item.slot = this.slotindex;
self.value.push(item);
self.$emit("input", self.value);
// Then on the next tick, we inform the back end
// Since moving things around has never been unsuccessful, unless you have other problems,
// it's better to have nice visuals.
self.relocateStudyItem(item).then(() => {
if (this.$refs.timingChecker) {
this.$refs.timingChecker.validateCoursePeriod();
}
return;
}).catch(notification.exception);
} else if (event.type.component) {
debug.info("Adding new component");
if (event.type.type == "gradable") {
call([{
methodname: 'local_treestudyplan_add_studyitem',
args: {
"line_id": self.line.id,
"slot": self.slotindex,
"layer": self.layer,
"type": 'course',
"details": {
"competency_id": null,
'conditions': '',
'course_id': event.data.id,
'badge_id': null,
'continuation_id': null,
}
}
}])[0].then((response) => {
let item = response;
self.relocateStudyItem(item).then(()=>{
self.value.push(item);
self.$emit("input", self.value);
// Call the validate period function on next tick,
// since it paints the item in the slot first
this.$nextTick(() => {
if (this.$refs.timingChecker) {
this.$refs.timingChecker.validateCoursePeriod();
}
});
ItemEventBus.$emit('coursechange');
return;
}).catch(notification.exception);
return;
}).catch(notification.exception);
} else if (event.type.type == "filter") {
debug.info("Adding new filter compenent");
call([{
methodname: 'local_treestudyplan_add_studyitem',
args: {
"line_id": self.line.id,
"slot": self.slotindex,
"type": event.data.type,
"details": {
"badge_id": event.data.badge ? event.data.badge.id : undefined,
}
}
}])[0].then((response) => {
let item = response;
self.relocateStudyItem(item).then(() => {
item.layer = this.layer;
self.value.push(item);
self.$emit("input", self.value);
return;
}).catch(notification.exception);
return;
}).catch(notification.exception);
}
}
},
onCut(event) {
const self = this;
let id = event.data.id;
for (let i = 0; i < self.value.length; i++) {
if (self.value[i].id == id) {
self.value.splice(i, 1); i--;
break; // Just remove one
}
}
// Do something to signal that this item has been removed
this.$emit("input", this.value);
ItemEventBus.$emit('coursechange');
},
relocateStudyItem(item) {
const iteminfo = {'id': item.id, 'layer': this.layer, 'slot': this.slotindex, 'line_id': this.line.id};
return call([{
methodname: 'local_treestudyplan_reorder_studyitems',
args: {'items': [iteminfo]} // Function was designed to relocate multiple items at once, hence the array
}])[0].catch(notification.exception);
},
onDragEnter(event) {
this.hover.component = event.data;
this.hover.type = event.type;
},
onDragLeave() {
this.hover.component = null;
this.hover.type = null;
},
maxSpan() {
// Determine the maximum span for components in this slot
return this.page.periods - this.slotindex + 1;
},
makeType(item) {
return {
item: true,
component: false,
span: item.span,
type: this.type,
};
},
checkType(type) {
if (type.type == this.type) {
if (settings("hivizdropslots") && !type.item) {
return false;
} else {
if (type == 'filter') {
return true;
} else if (type.span <= this.maxSpan()) {
return true;
} else {
return false;
}
}
} else {
return false;
}
}
},
template: `
--{{ hover.type }}--
{{plantext.drophere}}
`,
});
Vue.component('t-item-timing-checker', {
props: {
value: {
type: Object, // T-item model
},
page: {
type: Object, // Studyplanpage
},
line: {
type: Object, // Studyline
},
period: {
type: Object, // Studyplan data
},
maxspan: {
type: Number,
},
hidden: {
type: Boolean,
'default': false,
}
},
computed: {
endperiod() {
const endperiodnr = Math.min(this.page.periods, this.period.period + (this.value.span - 1));
return this.page.perioddesc[endperiodnr - 1];
},
coursePeriodMatches() {
const self = this;
if (self.page.timeless) {
// Always return true in timeless mode.
return true;
}
if (self.value && self.value.type == 'course') {
self.datechanger.coursespan = datespaninfo(self.value.course.startdate, self.value.course.enddate);
self.datechanger.periodspan = datespaninfo(self.period.startdate, self.endperiod.enddate);
if (self.datechanger.coursespan.first.getTime() == self.datechanger.periodspan.first.getTime()
&& self.datechanger.coursespan.last.getTime() == self.datechanger.periodspan.last.getTime()) {
return true;
} else {
return false;
}
} else {
debug.warn("Timing thing not properly configured", self.value, self.period, self.maxspan);
return false;
}
},
},
data() {
return {
// Create random id to avoid opening the wrong modals
id: Math.floor(Math.random() * Date.now()).toString(16),
text: strings.courseTiming,
datechanger: {
coursespan: null,
periodspan: null,
globals: {
'default': false,
defaultchoice: false,
hidewarn: false,
},
}
};
},
methods: {
validateCoursePeriod() {
const self = this;
if (!self.page.timeless) {
debug.info("Validating course and period");
if (!(self.coursePeriodMatches)) {
debug.info("Course timing does not match period timing");
if (self.value.course.canupdatecourse) {
if (!self.hidden || !self.datechanger.globals.default) {
// Periods do not match, pop up the date change request
this.$bvModal.show('t-course-timing-matching-' + this.id);
} else if (self.datechanger.globals.defaultvalue) {
// G for it without asking
self.changeCoursePeriod();
}
} else {
// User is not able to change course timing - show a warning
if (!self.hidden || !self.datechanger.globals.hidewarn) {
this.$bvModal.show('t-course-timing-warning-' + this.id);
}
}
} else {
debug.info("Course timing matches period", self.datechanger);
}
} else {
debug.info("Skipping course timing check because of timeless mode", self.datechanger);
}
},
changeCoursePeriod() {
const self = this;
// Save the state
if (self.datechanger.globals.default) {
self.datechanger.globals.defaultvalue = true;
}
return call([{
methodname: 'local_treestudyplan_course_period_timing',
args: {
'period_id': self.period.id,
'course_id': this.value.course.id,
span: this.value.span,
}
}])[0].catch(notification.exception).then((response) => {
self.value.course.startdate = response.startdate;
self.value.course.enddate = response.enddate;
self.value.course.timing = response.timing;
self.$emit("input", self.value);
return;
});
},
checkFilterSlotBusy(slotindex) {
debug.info("checking filter", this.line.slots, slotindex, this.value.layer);
if (this.line.slots[slotindex]) {
const list = this.line.slots[slotindex].filters;
for (const ix in list) {
if (list[ix].layer == this.value.layer) {
debug.info("Busy:", list[ix]);
return list[ix];
}
}
}
return null;
},
nextFreeFilterLayer(slotindex) {
const layer = this.value.layer;
const list = this.line.slots[slotindex].filters;
const usedLayers = [];
for (const ix in list) {
usedLayers.push(list[ix].layer);
}
let nextlyr = layer + 1;
while (usedLayers.includes(nextlyr)) {
nextlyr++;
}
return nextlyr;
},
checkCourseSlotBusy(slotindex) {
debug.info("checking ", this.line.slots, slotindex, this.value.layer);
if (this.line.slots[slotindex]) {
const list = this.line.slots[slotindex].courses;
for (const ix in list) {
if (list[ix].layer == this.value.layer) {
debug.info("Busy:", list[ix]);
return list[ix];
}
}
}
return null;
},
nextFreeCourseLayer(slotindex) {
const layer = this.value.layer;
const list = this.line.slots[slotindex].courses;
const usedLayers = [];
for (const ix in list) {
usedLayers.push(list[ix].layer);
}
let nextlyr = layer + 1;
while (usedLayers.includes(nextlyr)) {
nextlyr++;
}
return nextlyr;
},
shiftCollisions(span) {
// Check all periods for collision
const items = [];
for (let i = this.value.slot; i < this.value.slot + span; i++) {
const busyFilter = this.checkFilterSlotBusy(i);
if (busyFilter) {
const nextlyr = this.nextFreeFilterLayer(i);
items.push({
id: busyFilter.id,
layer: nextlyr,
'line_id': this.line.id,
slot: busyFilter.slot,
});
busyFilter.layer = nextlyr;
}
const busyCourse = this.checkCourseSlotBusy(i);
if (busyCourse && busyCourse.id != this.value.id) {
const nextlyr = this.nextFreeCourseLayer(i);
items.push({
id: busyCourse.id,
layer: nextlyr,
'line_id': this.line.id,
slot: busyCourse.slot,
});
busyCourse.layer = nextlyr;
}
}
if (items.length > 0) {
call([{
methodname: 'local_treestudyplan_reorder_studyitems',
args: {items: items}
}])[0].catch(notification.exception);
}
},
changeSpan(span) {
const self = this;
this.shiftCollisions(span);
return call([{
methodname: 'local_treestudyplan_set_studyitem_span',
args: {
id: self.value.id,
span: span
}
}])[0].catch(notification.exception).then((response) => {
self.value.span = response.span;
self.$emit('input', self.value);
self.$nextTick(() => {
self.validateCoursePeriod();
});
return;
});
},
formatDuration(dsi) {
let s = "";
if (dsi.years == 1) {
s += `1 ${this.text.year}, `;
} else if (dsi.years > 1) {
s += `${dsi.years} ${this.text.years}, `;
}
if (dsi.weeks == 1) {
s += `1 ${this.text.week}, `;
} else if (dsi.weeks > 1) {
s += `${dsi.weeks} ${this.text.weeks}, `;
}
if (dsi.days == 1) {
s += `1 ${this.text.day}, `;
} else if (dsi.days > 1) {
s += `${dsi.days} ${this.text.days}, `;
}
return s.toLocaleLowerCase();
},
},
// To avoid the span creeping in the dom where it shouldn't, set display to none if it is hidden
// This does not affect the modals, which are rendered outside of this element when needed
template: `
{{ text.periodspan
}}
{{ n }}
{{value.span}} {{
(value.span == 1)?text.period.toLowerCase():text.periods.toLowerCase()
}}
{{ text.desc }}
{{ text.question }}
{{ text.course }}
{{ value.course.fullname }}
{{ value.course.shortname }}
{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}
{{ text.duration }}
{{ formatDuration(datechanger.coursespan)}}
{{ text.period }}
{{ period.fullname }} - {{ endperiod.fullname }}
{{ period.shortname }} - {{ endperiod.shortname }}
{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}
{{ text.duration }}
{{ formatDuration(datechanger.periodspan)}}
{{ text.rememberchoice }}
{{ text.desc }}
{{ text.warning }}
{{ text.course }}
{{ value.course.fullname }}
{{ value.course.shortname }}
{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}
{{ text.duration }}
{{ formatDuration(datechanger.coursespan)}}
"6">
{{ text.period }}
{{ period.fullname }} - {{ endperiod.fullname }}
{{ period.shortname }} - {{ endperiod.shortname }}
{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}
{{ text.duration }}
{{ formatDuration(datechanger.periodspan)}}
{{ text.hidewarning }}
`,
});
Vue.component('t-item', {
props: {
value: {
type: Object,
default() {
return null;
},
},
dummy: {
type: Boolean,
default() {
return false;
},
},
plan: {
type: Object, // Studyplan page
default() {
return null;
},
},
line: {
type: Object, // Studyplan page
default() {
return null;
},
},
page: {
type: Object, // Studyplan page
default() {
return null;
},
},
period: {
type: Object, // Studyplan page
default() {
return null;
},
},
maxspan: {
type: Number,
default() {
return 0;
},
},
},
data() {
return {
dragLine: null,
dragEventListener: null,
deleteMode: false,
conditionOptions: stringKeys.conditions,
text: strings.itemText,
showContext: false,
lines: [],
};
},
methods: {
dragStart(event) {
// Add line between start point and drag image
this.deleteMode = false;
let start = document.getElementById('studyitem-' + this.value.id);
let dragelement = document.getElementById('t-item-cdrag-' + this.value.id);
dragelement.style.position = 'fixed';
dragelement.style.left = event.position.x + 'px';
dragelement.style.top = event.position.y + 'px';
this.dragLine = new SimpleLine(start, dragelement, {
color: "#777",
gravity: {
start: LINE_GRAVITY,
end: LINE_GRAVITY,
},
});
// Add separate event listener to reposition mouse move
document.addEventListener("mousemove", this.onMouseMove);
},
dragEnd() {
if (this.dragLine !== null) {
this.dragLine.remove();
}
let dragelement = document.getElementById('t-item-cdrag-' + this.value.id);
dragelement.style.removeProperty('left');
dragelement.style.removeProperty('top');
dragelement.style.removeProperty('position');
document.removeEventListener("mousemove", this.onMouseMove);
},
onMouseMove: function(event) {
let dragelement = document.getElementById('t-item-cdrag-' + this.value.id);
dragelement.style.position = 'fixed';
dragelement.style.left = event.clientX + 'px';
dragelement.style.top = event.clientY + 'px';
// Line will follow automatically
},
onDrop(event) {
let fromid = event.data.id;
let toid = this.value.id;
this.redrawLines();
call([{
methodname: 'local_treestudyplan_connect_studyitems',
args: {'from_id': fromid, 'to_id': toid}
}])[0].then((response)=>{
let conn = {'id': response.id, 'from_id': response.from_id, 'to_id': response.to_id};
ItemEventBus.$emit("createdConnection", conn);
this.value.connections.in.push(conn);
return;
}).catch(notification.exception);
},
redrawLine(conn) {
const lineColor = "var(--success)";
const start = document.getElementById(`studyitem-${conn.from_id}`);
const end = document.getElementById(`studyitem-${conn.to_id}`);
// Delete old line
if (this.lines[conn.to_id]) {
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
}
// Create a new line if the start and finish items are visible
if (start !== null && end !== null && isVisible(start) && isVisible(end)) {
this.lines[conn.to_id] = new SimpleLine(start, end, {
color: lineColor,
gravity: {
start: LINE_GRAVITY,
end: LINE_GRAVITY,
},
});
}
},
deleteLine(conn) {
const self = this;
call([{
methodname: 'local_treestudyplan_disconnect_studyitems',
args: {'from_id': conn.from_id, 'to_id': conn.to_id}
}])[0].then((response)=>{
if (response.success) {
this.removeLine(conn);
// Send disconnect event on message bus, so the connection on the other end can delete it too
ItemEventBus.$emit("connectionDisconnected", conn);
// Remove connection from our outgoing list
let index = self.value.connections.out.indexOf(conn);
self.value.connections.out.splice(index, 1);
}
return;
}).catch(notification.exception);
},
highlight(conn) {
if (this.lines[conn.to_id]) {
this.lines[conn.to_id].setConfig({color: "var(--danger)"});
}
},
normalize(conn) {
if (this.lines[conn.to_id]) {
this.lines[conn.to_id].setConfig({color: "var(--success)"});
}
},
updateItem() {
call([{
methodname: 'local_treestudyplanEdit_studyitem',
args: {
'id': this.value.id,
'conditions': this.value.conditions,
'continuation_id': this.value.continuation_id,
}
}])[0].catch(notification.exception);
},
doShowContext(event) {
if (this.hasContext) {
this.showContext = true;
event.preventDefault();
}
},
redrawLines() {
if (this.value.connections && this.value.connections.out) {
for (let i in this.value.connections.out) {
let conn = this.value.connections.out[i];
this.redrawLine(conn);
}
}
},
// EVENT LISTENERS
onCreatedConnection(conn) {
if (conn.from_id == this.value.id) {
this.value.connections.out.push(conn);
this.redrawLine(conn);
}
},
// Listener for the signal that a connection was removed by the outgoing item
onRemovedConnection(conn) {
if (this.value.connections && this.value.connections.out) {
for (let i in this.value.connections.in) {
let cin = this.value.connections.in[i];
if (conn.id == cin.id) {
self.value.connections.out.splice(i, 1);
}
}
}
},
// Listener for reposition events
// When an item in the list is repositioned, all lines need to be redrawn
onRePositioned() {
this.redrawLines();
},
// When an item is disPositioned - (temporarily) removed from the list,
// all connections need to be deleted.
onDisPositioned(reid) {
if (this.value.connections && this.value.connections.out) {
for (let i in this.value.connections.out) {
let conn = this.value.connections.out[i];
if (conn.to_id == reid) {
this.removeLine(conn);
}
}
}
},
// When an item is deleted
// all connections to/from that item need to be cleaned up
onItemDeleted(itemid) {
const self = this;
if (this.value.connections && this.value.connections.out) {
for (const i in this.value.connections.out) {
let conn = this.value.connections.out[i];
if (conn.to_id == itemid) {
self.removeLine(conn);
self.value.connections.out.splice(i, 1);
}
}
}
if (this.value.connections && this.value.connections.in) {
for (const i in this.value.connections.in) {
let conn = this.value.connections.in[i];
if (conn.from_id == itemid) {
self.value.connections.out.splice(i, 1);
}
}
}
},
onRedrawLines() {
this.redrawLines();
},
removeLine(conn) {
if (this.lines[conn.to_id]) {
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
}
},
deleteItem() {
const self = this;
const msgparams = {
item: this.text['type_' + this.value.type].toLocaleLowerCase(),
name: (this.value.type == 'course') ? this.value.course.displayname : "",
line: (this.line) ? this.line.name : "",
period: (this.period) ? this.period.fullname : this.plan.name,
};
this.$bvModal.msgBoxConfirm(strformat(this.text.item_delete_message, msgparams), {
okVariant: 'danger',
okTitle: this.text.ok,
cancelTitle: this.text.cancel,
}).then(value => {
if (value) {
// Confirmed to delete.
call([{
methodname: 'local_treestudyplan_delete_studyitem',
args: {
'id': self.value.id,
}
}])[0].then((response) => {
if (response.success == true) {
self.$emit("deleted", {data: self.value});
}
return;
}).catch(notification.exception);
}
return;
}).catch(err => {
debug.console.error(err);
});
}
},
computed: {
hasConnectionsOut() {
return !(["finish"].includes(this.value.type));
},
hasConnectionsIn() {
return !(["start"].includes(this.value.type));
},
hasContext() {
return ['start', 'junction', 'finish'].includes(this.value.type);
}
},
created() {
// Add event listeners on the message bus
// But only if not in "dummy" mode - mode which is used for droplist placeholders
// Since an item is "fully made" with all references, not specifying dummy mode really messes things up
if (!this.dummy) {
// Listener for the signal that a new connection was made and needs to be drawn
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
ItemEventBus.$on('createdConnection', this.onCreatedConnection);
// Listener for the signal that a connection was removed by the outgoing item
ItemEventBus.$on('removedConnection', this.onRemovedConnection);
// Listener for reposition events
// When an item in the list is repositioned, all lines need to be redrawn
ItemEventBus.$on('rePositioned', this.onRePositioned);
// When an item is disPositioned - (temporarily) removed from the list,
// all connections need to be deleted.
ItemEventBus.$on('disPositioned', this.onDisPositioned);
// When an item is deleted
// all connections to/from that item need to be cleaned up
ItemEventBus.$on('itemDeleted', this.onItemDeleted);
ItemEventBus.$on('redrawLines', this.onRedrawLines);
}
},
mounted() {
// Initialize connection lines when mounting
// But only if not in "dummy" mode - mode which is used for droplist placeholders
// Since an item is "fully made" with all references, not specifying dummy mode really messes things up
if (!this.dummy) {
this.redrawLines();
setTimeout(()=>{
ItemEventBus.$emit("rePositioned", this.value.id);
}, 10);
}
},
beforeDestroy() {
if (!this.dummy) {
for (let i in this.value.connections.out) {
let conn = this.value.connections.out[i];
this.removeLine(conn);
}
ItemEventBus.$emit("disPositioned", this.value.id);
// Remove event listeners
ItemEventBus.$off('createdConnection', this.onCreatedConnection);
ItemEventBus.$off('removedConnection', this.onRemovedConnection);
ItemEventBus.$off('rePositioned', this.onRePositioned);
ItemEventBus.$off('disPositioned', this.onDisPositioned);
ItemEventBus.$off('itemDeleted', this.onItemDeleted);
ItemEventBus.$off('redrawLines', this.onRedrawLines);
}
},
updated() {
if (!this.dummy) {
this.redrawLines();
}
},
template: `
`,
});
Vue.component('t-item-invalid', {
props: {
'value': {
type: Object,
default() {
return null;
},
},
},
data() {
return {
text: strings.invalid,
};
},
methods: {
},
template: `
`,
});
// TAG: Course item
Vue.component('t-item-course', {
props: {
value: {
type: Object,
default() {
return null;
},
},
plan: {
type: Object,
default() {
return null;
},
},
line: {
type: Object,
default() {
return null;
},
},
page: {
type: Object, // PAge data
default() {
return null;
}
},
period: {
type: Object, // Period data
default() {
return null;
}
},
maxspan: {
type: Number,
default() {
return 0;
}
},
},
data() {
return {
conditionOptions: stringKeys.conditions,
text: strings.itemCourseText,
};
},
computed: {
useItemConditions() {
if (this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useItemConditions !== undefined) {
return this.plan.aggregation_info.useItemConditions;
} else {
return false;
}
},
configurationState() {
if (this.hasGrades() || this.hasCompletions() || this.hasCompetencies()) {
return "t-configured-ok";
} else {
return "t-configured-alert";
}
},
configurationIcon() {
if (this.hasGrades() || this.hasCompletions() || this.hasCompetencies()) {
return "check";
} else {
return "exclamation-circle";
}
},
startdate() {
return formatDate(this.value.course.startdate);
},
enddate() {
if (this.value.course.enddate > 0) {
return formatDate(this.value.course.enddate);
} else {
return this.text.noenddate;
}
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
hasGrades() {
if (this.value.course.grades && this.value.course.grades.length > 0) {
for (const g of this.value.course.grades) {
if (g.selected) {
return true;
}
}
}
return false;
},
hasCompletions() {
if (this.value.course.completion && this.value.course.completion.conditions) {
for (const cgroup of this.value.course.completion.conditions) {
if (cgroup.items && cgroup.items.length > 0) {
return true;
}
}
}
return false;
},
hasCompetencies() {
if (this.value.course.competency && this.value.course.competency.competencies) {
return (this.value.course.competency.competencies.length > 0);
}
return false;
},
includeChanged(newValue, g) {
call([{
methodname: 'local_treestudyplan_include_grade',
args: {
'grade_id': g.id,
'item_id': this.value.id,
'include': newValue,
'required': g.required,
}
}])[0].catch(notification.exception);
},
requiredChanged(newValue, g) {
call([{
methodname: 'local_treestudyplan_include_grade',
args: {
'grade_id': g.id,
'item_id': this.value.id,
'include': g.selected,
'required': newValue,
}
}])[0].catch(notification.exception);
},
updateConditions() {
call([{
methodname: 'local_treestudyplanEdit_studyitem',
args: {
'id': this.value.id,
'conditions': this.value.conditions,
}
}])[0].catch(notification.exception);
},
},
template: `
{{ value.course.context.path.join(" / ")}} / {{value.course.shortname}}
{{ text.delete }}
{{ text.ok }}
`,
});
Vue.component('t-item-course-grades', {
props: {
'value': {
type: Object,
default() {
return null;
},
},
'plan': {
type: Object,
default() {
return null;
},
},
},
data() {
return {
conditionOptions: stringKeys.conditions,
text: strings.itemCourseText,
};
},
computed: {
useRequiredGrades() {
if (this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined) {
return this.plan.aggregation_info.useRequiredGrades;
} else {
return false;
}
},
selectedgrades() {
let list = [];
for (let ix in this.value.course.grades) {
let g = this.value.course.grades[ix];
if (g.selected) {
list.push(g);
}
}
return list;
},
},
methods: {
includeChanged(newValue, g) {
call([{
methodname: 'local_treestudyplan_include_grade',
args: {
'grade_id': g.id,
'item_id': this.value.id,
'include': newValue,
'required': g.required,
}
}])[0].catch(notification.exception);
},
requiredChanged(newValue, g) {
call([{
methodname: 'local_treestudyplan_include_grade',
args: {
'grade_id': g.id,
'item_id': this.value.id,
'include': g.selected,
'required': newValue,
}
}])[0].catch(notification.exception);
},
},
template: `
-
{{text.grade_include}}{{text.grade_require}}
-
g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.course.ctxid"
genericonly>
`,
});
Vue.component('t-item-course-completion', {
props: {
value: {
type: Object,
default() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
course: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
text: strings.completion,
};
},
computed: {
hasCompletions() {
if (this.value.conditions) {
for (const cgroup of this.value.conditions) {
if (cgroup.items && cgroup.items.length > 0) {
return true;
}
}
}
return false;
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
completionIcon(completion) {
switch (completion) {
case "progress":
return "exclamation-circle";
case "complete":
return "check-circle";
case "complete-pass":
return "check-circle";
case "complete-fail":
return "times-circle";
default: // Case "incomplete"
return "circle-o";
}
},
completionTag(cgroup) {
return cgroup.completion ? 'completed' : 'incomplete';
}
},
template: `
{{ text.aggregation_overall_all}}{{ text.aggregation_overall_any}} |
{{text.completion_not_configured}}!
{{text.configure_completion}}
|
{{ text.aggregation_all}}{{ text.aggregation_any}}
{{cgroup.title}} |
|
{{ci.details.requirement}}
|
`,
});
// TAG: Course competency
Vue.component('t-item-course-competency', {
props: {
value: {
type: Object,
default() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
item: {
type: Object,
default() {
return {id: null};
},
}
},
data() {
return {
text: strings.competency,
};
},
computed: {
hasCompletions() {
if (this.value.conditions) {
for (const cgroup of this.value.conditions) {
if (cgroup.items && cgroup.items.length > 0) {
return true;
}
}
}
return false;
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
pathtags(competency) {
const path = competency.path;
let s = "";
for (const ix in path) {
const p = path[ix];
if (ix > 0) {
s += " / ";
}
let url;
if (p.type == 'competency') {
url = `/admin/tool/lp/competencies.php?competencyid=${p.id}`;
} else {
url = `/admin/tool/lp/competencies.php?competencyframeworkid=${p.id}&pagecontextid=${p.contextid}`;
}
s += `${p.title}`;
}
return s;
},
requiredChanged(newValue, c) {
call([{
methodname: 'local_treestudyplan_require_competency',
args: {
'competency_id': c.id,
'item_id': this.item.id,
'required': newValue,
}
}])[0].catch(notification.exception);
},
},
template: `
{{text.competency_not_configured}}
{{text.configure_competency}}
|
|
|
{{ text.required }}
|
{{ c.ruleoutcome }} {{ text.when}}
|
|
{{ cc.points }} {{ text.points }} |
{{ text.required }} |
`,
});
/* **********************************
* *
* Toolbox list components *
* *
************************************/
Vue.component('t-item-junction', {
props: {
value: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
conditionOptions: stringKeys.conditions,
};
},
methods: {
},
template: `
`,
});
Vue.component('t-item-finish', {
props: {
value: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
};
},
methods: {
},
template: `
`,
});
Vue.component('t-item-start', {
props: {
value: {
type: Object,
default() {
return {};
},
},
},
data() {
return {};
},
methods: {
},
template: `
`,
});
Vue.component('t-item-badge', {
props: {
value: {
type: Object,
default() {
return {
badge: {}
};
},
},
},
data() {
return {
txt: strings,
text: strings.itemText,
};
},
methods: {
},
template: `
`,
});
Vue.component('t-coursecat-list', {
props: {
value: {
type: Array,
default() {
return {};
},
},
},
data() {
return {
};
},
methods: {
},
template: `
`,
});
Vue.component('t-coursecat-list-item', {
props: {
value: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
loading: false,
};
},
computed: {
showSpinner() {
return this.canLoadMore();
},
hasDetails() {
return (this.value.haschildren || this.value.hascourses);
}
},
methods: {
canLoadMore() {
return (this.value.haschildren && (!this.value.children)) ||
(this.value.hascourses && (!this.value.courses));
},
onShowDetails() {
const self = this;
if (this.canLoadMore()) {
call([{
methodname: 'local_treestudyplan_get_category',
args: {
"id": this.value.id,
}
}])[0].then((response) => {
self.$emit('input', response);
return;
}).catch(notification.exception);
}
}
},
template: `
{{ value.category.name }}
{{ value.category.name }}
`,
});
Vue.component('t-course-list', {
props: {
value: {
type: Array,
default() {
return {};
},
},
},
data() {
return {
};
},
methods: {
makeType() {
return {
item: false,
component: true,
span: 1, // TODO: Detect longer courses and give them an appropriate span
type: 'gradable',
};
},
},
template: `
-
{{ course.shortname }} - {{ course.fullname }}
`,
});
Vue.component('t-toolbox', {
props: {
value: {
type: Boolean,
'default': true,
},
activepage: {
type: Object,
default() {
return null;
}
},
coaching: {
type: Boolean,
'default': false,
},
studyplanid: {
type: Number,
'default': 0,
}
},
data() {
return {
toolboxright: !(settings("toolboxleft")),
text: strings.toolbox,
relatedbadges: [],
systembadges: [],
courses: [],
filters: {
courses: "",
systembadges: "",
relatedbadges: "",
},
loadingcourses: false,
loadingcategories: [],
badgelistshown: {
relatedbadges: true,
systembadges: false,
}
};
},
watch: {
// Whenever activepage changes, this function will run
activepage(/* Params newVal, oldVal */) {
this.filterRelatedbadges();
}
},
mounted() {
const self = this;
this.initialize();
this.$root.$on('bv::collapse::state', (collapseId, isJustShown) => {
self.badgelistshown[collapseId] = !!isJustShown;
});
},
computed: {
filterComponentType() {
return {
item: false,
component: true,
span: 1,
type: 'filter',
};
},
filteredCourses() {
const self = this;
if (self.filters.courses) {
return self.filterCategories(self.courses);
} else {
return self.courses;
}
}
},
methods: {
hivizdrop() {
return settings("hivizdropslots");
},
filterCategories(catlist) {
const self = this;
const list = [];
const search = new RegExp(`.*?${self.filters.courses}.*?`, "ig");
for (const cat of catlist) {
const clone = Object.assign({}, cat);
clone.courses = [];
if (cat.courses) {
for (const course of cat.courses) {
if (search.test(course.shortname) || search.test(course.fullname)) {
clone.courses.push(course);
}
}
} else if (cat.hascourses && !(self.loadingcategories.includes(cat.id))) {
self.loadingcategories.push(cat.id);
debug.info(`Loading from category ${cat.category.name}`, cat);
call([{
methodname: 'local_treestudyplan_get_category',
args: {
"id": cat.id
}
}])[0].then((response) => {
// Add reactive array 'children' to cat
self.$set(cat, "children", response.children);
self.$set(cat, "courses", response.courses);
self.loadingcategories.splice(self.loadingcategories.indexOf(cat.id), 1);
return;
}).catch(notification.exception);
}
if (cat.children) {
clone.children = self.filterCategories(cat.children);
} else if (cat.haschildren && !(self.loadingcategories.includes(cat.id))) {
self.loadingcategories.push(cat.id);
debug.info(`Loading from category ${cat.category.name}`, cat);
call([{
methodname: 'local_treestudyplan_get_category',
args: {
"id": cat.id,
}
}])[0].then((response) => {
// Add reactive array 'children' to cat
self.$set(cat, "children", response.children);
self.loadingcategories.splice(self.loadingcategories.indexOf(cat.id), 1);
return;
}).catch(notification.exception);
}
if ((clone.children && clone.children.length) || clone.courses.length) {
list.push(clone);
}
}
return list;
},
initialize() {
const self = this;
self.loadingcourses = true;
call([{
methodname: 'local_treestudyplan_map_categories',
args: {
'studyplan_id': self.studyplanid,
}
}])[0].then((response) => {
self.courses = response;
self.loadingcourses = false;
return;
}).catch(notification.exception);
this.filterSystembadges();
this.filterRelatedbadges();
},
filterSystembadges() {
const self = this;
call([{
methodname: 'local_treestudyplan_search_badges',
args: {
search: this.filters.systembadges || "",
}
}])[0].then((response) => {
self.systembadges = response;
return;
}).catch(notification.exception);
},
filterRelatedbadges() {
const self = this;
if (this.activepage) {
call([{
methodname: 'local_treestudyplan_search_related_badges',
args: {
'page_id': this.activepage.id,
search: this.filters.relatedbadges || ""
}
}])[0].then((response) => {
self.relatedbadges = response;
return;
}).catch(notification.exception);
}
},
resetSystembadges() {
this.filters.systembadges = "";
this.filterSystembadges();
},
resetRelatedbadges() {
this.filters.relatedbadges = "";
this.filterRelatedbadges();
},
},
template: `
`,
});
},
};