4709 lines
214 KiB
JavaScript
4709 lines
214 KiB
JavaScript
/* eslint no-var: "error"*/
|
|
/* eslint no-console: "off"*/
|
|
/* eslint no-unused-vars: warn */
|
|
/* eslint max-len: ["error", { "code": 160 }] */
|
|
/* eslint promise/no-nesting: "off" */
|
|
/* 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 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: 'studyplan_edit',
|
|
'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',
|
|
relatedbadges: 'relatedbages@badges', /* [sic] as in badges translation file */
|
|
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:
|
|
`
|
|
<span>
|
|
<a v-if="value.advanced"
|
|
href='#'
|
|
@click.prevent=''
|
|
class='text-danger'
|
|
v-b-modal="'t-studyplan-'+value.id+'-advanced'"
|
|
><i class='fa fa-wrench'></i> {{text.advanced_tools}}</a>
|
|
<b-modal v-if="value.advanced"
|
|
:id="'t-studyplan-'+value.id+'-advanced'"
|
|
size="lg"
|
|
:title="text.advanced_tools_heading"
|
|
ok-only
|
|
@hide="modalClose"
|
|
body-class="p-0"
|
|
>
|
|
<b-tabs card>
|
|
<b-tab :title="text.advanced_warning_title" active>
|
|
{{ text.advanced_warning}}
|
|
</b-tab>
|
|
<b-tab :title="text.advanced_course_manipulation_title" >
|
|
<h3>{{ text.advanced_cascade_cohortsync_title}}</h3>
|
|
<p>{{ text.advanced_cascade_cohortsync_desc}}</p>
|
|
<p class="mt-2"><b-button
|
|
variant="info"
|
|
@click.prevent="cascadeCohortsync"
|
|
>{{ text.advanced_cascade_cohortsync}}</b-button></p>
|
|
<h3>{{ text.advanced_bulk_course_timing}}</h3>
|
|
<p>{{ text.advanced_bulk_course_timing_desc}}</p>
|
|
<p>{{text.currentpage}} <i>{{selectedpage.fullname}}</i></p>
|
|
<p class="mt-2"><b-button
|
|
variant="info"
|
|
@click.prevent="bulkCourseTiming"
|
|
>{{ text.advanced_bulk_course_timing}}</b-button></p>
|
|
<template v-if="['bistate','tristate'].includes(value.aggregation)">
|
|
<h3>{{ text.advanced_force_scale_title}}</h3>
|
|
{{ text.advanced_force_scale_desc}}
|
|
<p class="mt-2"><b-form-select v-model="forceScales.selectedScale"
|
|
:options="scales" text-field="name" value-field="id"
|
|
></b-form-select>
|
|
<b-button
|
|
variant="danger"
|
|
:disabled="forceScales.selectedScale == null"
|
|
@click.prevent="forceScalesStart"
|
|
>{{ text.advanced_force_scale_button}}</b-button>
|
|
</p>
|
|
<p class="mt-2">
|
|
<ul class='t-advanced-scrollable' v-if="forceScales.result.length > 0">
|
|
<li v-for="c in forceScales.result">
|
|
<span class='t-advanced-coursename'>{{c.course.fullname}}</span>
|
|
<ul v-if="c.grades.length > 0">
|
|
<li v-for='g in c.grades'
|
|
><span class='t-advanced-gradename'><span v-html="g.name"></span></span>
|
|
<span v-if="g.changed == 'converted'" class='t-advanced-status changed'
|
|
>{{text.advanced_converted}}</span
|
|
><span v-else-if="g.changed == 'skipped'" class='t-advanced-status skipped'
|
|
>{{text.advanced_skipped}}</span
|
|
><span v-else class='t-advanced-status skipped'
|
|
>{{text.advanced_error}}</span
|
|
></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</p>
|
|
</template>
|
|
</b-tab>
|
|
<b-tab :title='text.advanced_backup_restore'>
|
|
<h3>{{ text.advanced_backup }}</h3>
|
|
<p><b-button
|
|
variant="primary"
|
|
@click.prevent="exportPage('json')"
|
|
>{{ text.advanced_backup_page }}</b-button>
|
|
{{text.currentpage}} <i>{{selectedpage.fullname}}</i></p>
|
|
<p><b-button
|
|
variant="primary"
|
|
@click.prevent="exportPlan('json')"
|
|
>{{ text.advanced_backup_plan }}</b-button></p>
|
|
<h3>{{ text.advanced_restore }}</h3>
|
|
<p><b-button
|
|
variant="danger"
|
|
@click.prevent="importStudylines"
|
|
>{{ text.advanced_restore_lines}}</b-button></p>
|
|
<p><b-button
|
|
variant="danger"
|
|
@click.prevent="importPages"
|
|
>{{ text.advanced_restore_pages }}</b-button></p>
|
|
<h3>{{ text.advanced_export }}</h3>
|
|
<p><b-button
|
|
variant="primary"
|
|
@click.prevent="exportPage('csv')"
|
|
>{{ text.advanced_export_csv_page }}</b-button>
|
|
{{text.currentpage}} <i>{{selectedpage.fullname}}</i></p>
|
|
</b-tab>
|
|
<b-tab :title='text.advanced_purge'>
|
|
<p>{{text.advanced_purge_page_expl}}</p>
|
|
<p>{{text.currentpage}} <i>{{selectedpage.fullname}}</i></p>
|
|
<p><b-button
|
|
variant="danger"
|
|
@click.prevent="purgeStudyplanpage"
|
|
>{{ text.advanced_purge_page}}</b-button></p>
|
|
<p>{{text.advanced_purge_plan_expl}}</p>
|
|
<p><b-button
|
|
variant="danger"
|
|
@click.prevent="purgeStudyplan"
|
|
>{{ text.advanced_purge_plan}}</b-button></p>
|
|
</b-tab>
|
|
</b-tabs>
|
|
</b-modal>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
|
|
/*
|
|
* 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:
|
|
`
|
|
<span class='s-studyplan-edit'>
|
|
<mform
|
|
name="studyplan_editform"
|
|
:params="{studyplan_id: value.id, mode: mode, contextid: contextid }"
|
|
@saved="planSaved"
|
|
:variant="variant"
|
|
:type="type"
|
|
:title="(mode == 'create')?text.studyplan_add:text.studyplanEdit"
|
|
><slot><i class='fa fa-gear'></i></slot></mform>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
/*
|
|
* 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:
|
|
`
|
|
<span class='s-studyplan-page-edit'>
|
|
<mform
|
|
name="studyplanpage_editform"
|
|
:params="{page_id: value.id, studyplan_id: studyplan.id, mode: mode }"
|
|
@saved="planSaved"
|
|
:variant="variant"
|
|
:type="type"
|
|
:title="(mode == 'create')?text.studyplanpage_add:text.studyplanpage_edit"
|
|
><slot><i class='fa fa-gear'></i></slot></mform>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
|
|
/*
|
|
* 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: {
|
|
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);
|
|
|
|
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) {
|
|
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() {
|
|
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() {
|
|
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:
|
|
`
|
|
<span class='s-studyplan-associate'
|
|
><a href='#' @click.prevent="showModal" ><slot><i class='fa fa-users'></i></slot></a>
|
|
<b-modal
|
|
v-model="show"
|
|
size="lg"
|
|
ok-variant="primary"
|
|
:title="text.associations + ' - ' + value.name"
|
|
ok-only>
|
|
<b-tabs class='s-studyplan-associate-window'>
|
|
<b-tab :title="text.cohorts">
|
|
<b-container>
|
|
<b-row class='mb-2 mt-2'>
|
|
<b-col>{{text.associated_cohorts}}</b-col>
|
|
<b-col>{{text.associate_cohorts}}</b-col>
|
|
</b-row>
|
|
<b-row class='mb-2'>
|
|
<b-col>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
type="text" @input="searchCohorts($event)"
|
|
:placeholder="text.search"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.associated.cohorts"
|
|
:options="association.cohorts"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.search.cohorts"
|
|
:options="search.cohorts"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row class='mt-2'>
|
|
<b-col>
|
|
<b-button variant='danger' @click.prevent="cohortDisassociate()"
|
|
><i class='fa fa-chain-broken'></i> {{text.delete_association}}</b-button>
|
|
</b-col>
|
|
<b-col>
|
|
<b-button variant='success' @click.prevent="cohortAssociate()"
|
|
><i class='fa fa-link'></i> {{text.add_association}}</b-button>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-tab>
|
|
<b-tab :title="text.users">
|
|
<b-container>
|
|
<b-row class='mb-2 mt-2'>
|
|
<b-col>{{text.associated_users}}</b-col>
|
|
<b-col>{{text.associate_users}}</b-col>
|
|
</b-row>
|
|
<b-row class='mb-2'>
|
|
<b-col>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
type="text"
|
|
@input="searchUsers($event)"
|
|
:placeholder="text.search + ' ' + text.users"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.associated.users"
|
|
:options="association.users"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.search.users"
|
|
:options="search.users"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row class='mt-2'>
|
|
<b-col>
|
|
<b-button variant='danger' @click.prevent="userDisassociate()"
|
|
><i class='fa fa-chain-broken'></i> {{text.delete_association}}</b-button>
|
|
</b-col>
|
|
<b-col>
|
|
<b-button variant='success' @click.prevent="userAssociate()"
|
|
><i class='fa fa-link'></i> {{text.add_association}}</b-button>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-tab>
|
|
<b-tab :title="text.coaches">
|
|
<b-container>
|
|
<b-row class='mb-2 mt-2'>
|
|
<b-col>{{text.associated_coaches}}</b-col>
|
|
<b-col>{{text.associate_coaches}}</b-col>
|
|
</b-row>
|
|
<b-row class='mb-2'>
|
|
<b-col>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
type="text"
|
|
@input="searchCoaches($event)"
|
|
:placeholder="text.search + ' ' + text.coaches"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.associated.coaches"
|
|
:options="association.coaches"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.search.coaches"
|
|
:options="search.coaches"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row class='mt-2'>
|
|
<b-col>
|
|
<b-button variant='danger' @click.prevent="coachDisassociate()"
|
|
><i class='fa fa-chain-broken'></i> {{text.delete_association}}</b-button>
|
|
</b-col>
|
|
<b-col>
|
|
<b-button variant='success' @click.prevent="coachAssociate()"
|
|
><i class='fa fa-link'></i> {{text.add_association}}</b-button>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-tab>
|
|
</b-tabs>
|
|
</b-modal>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
/* * ****************
|
|
*
|
|
* 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_treestudyplan_edit_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:
|
|
`
|
|
<span class='t-period-edit'>
|
|
<b-button :variant="variant" v-if='type == "button"' @click.prevent='editStart()'
|
|
><slot><i class='fa fa-gear'></i></slot></b-button>
|
|
<a variant="variant" v-else href='#' @click.prevent='editStart()'
|
|
><slot><i class='fa fa-gear'></i></slot></a>
|
|
<b-modal
|
|
v-model="show"
|
|
size="lg"
|
|
ok-variant="primary"
|
|
:title="text.edit"
|
|
@ok="editFinish()"
|
|
:ok-disabled="Math.min(editdata.fullname.length,editdata.shortname.length) == 0"
|
|
>
|
|
<b-container>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.fullname}}</b-col>
|
|
<b-col cols="8">
|
|
<b-form-input v-model="editdata.fullname"
|
|
:state='editdata.fullname.length>0'
|
|
></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.shortname}}</b-col>
|
|
<b-col cols="8">
|
|
<b-form-input v-model="editdata.shortname"
|
|
:state='editdata.shortname.length>0'
|
|
></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row v-if="!value.timeless">
|
|
<b-col cols="4">{{ text.studyplan_startdate}}</b-col>
|
|
<b-col cols="8">
|
|
<b-form-datepicker
|
|
start-weekday="1"
|
|
v-model="editdata.startdate"
|
|
:min="(minstart ? minstart : '')"
|
|
:max="subDay(value.enddate)"
|
|
></b-form-datepicker>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row v-if="!value.timeless">
|
|
<b-col cols="4">{{ text.studyplan_enddate}}</b-col>
|
|
<b-col cols="8">
|
|
<b-form-datepicker
|
|
start-weekday="1"
|
|
v-model="editdata.enddate"
|
|
:min="addDay(value.startdate)"
|
|
:max="(maxend ? maxend : '')"
|
|
></b-form-datepicker>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-modal>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
// 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: {
|
|
toolboxShown: 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: '<No study lines defined>',
|
|
shortname: '<No study lines>',
|
|
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);
|
|
} else {
|
|
self.edit.toolboxShown = true; // Defaults to on in coching view.
|
|
}
|
|
this.$root.$emit('redrawLines');
|
|
this.$emit('pagechanged', this.selectedpage);
|
|
},
|
|
beforeUnmount() {
|
|
this.edit.toolboxShown = false;
|
|
debug.info("Hiding toolbar because of destroy");
|
|
},
|
|
deactivated() {
|
|
this.edit.toolboxShown = false;
|
|
debug.info("Hiding toolbar because of deactivation");
|
|
},
|
|
activated() {
|
|
if (this.coaching) {
|
|
self.edit.toolboxShown = true; // Defaults to on in coching view.
|
|
}
|
|
|
|
},
|
|
updated() {
|
|
this.$root.$emit('redrawLines');
|
|
ItemEventBus.$emit('redrawLines');
|
|
},
|
|
computed: {
|
|
selectedpage() {
|
|
return this.value.pages[this.selectedpageindex];
|
|
},
|
|
hivizdrop() {
|
|
return settings("hivizdropslots");
|
|
},
|
|
},
|
|
methods: {
|
|
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_treestudyplan_edit_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:
|
|
`
|
|
<div>
|
|
<t-toolbox v-model="edit.toolboxShown"
|
|
:activepage="selectedpage"
|
|
:coaching="coaching"
|
|
:studyplanid="value.id"></t-toolbox>
|
|
<div class='controlbox t-studyplan-controlbox'>
|
|
<div class="controlbox-group">
|
|
<b-form-checkbox v-if="!coaching"
|
|
v-model="edit.studyline.editmode" class="sw-studyplan-toolbar" switch
|
|
@change="toolboxSwitched(edit.toolboxShown && !edit.studyline.editmode); "
|
|
>{{ text.studyline_editmode }}</b-form-checkbox>
|
|
<b-form-checkbox
|
|
v-if="!edit.studyline.editmode" v-model="edit.toolboxShown" class="sw-studyplan-toolbar" switch
|
|
@change="toolboxSwitched"
|
|
>{{ text.toolbox_toggle}}</b-form-checkbox>
|
|
<drop
|
|
mode='copy'
|
|
class='t-item-deletebox text-danger border-danger'
|
|
@drop='deleteStudyItem'
|
|
:accepts-type="trashbinAccepts"
|
|
><i class='fa fa-trash'></i>
|
|
</drop>
|
|
</div>
|
|
<div class="controlbox-group" v-if="!coaching">
|
|
<span class='control editable'>
|
|
<t-studyplan-advanced v-model="value" :selectedpage="selectedpage"></t-studyplan-advanced>
|
|
</span>
|
|
<span class='control editable'>
|
|
<t-studyplan-associate
|
|
v-model="value"><i class='fa fa-users'></i> {{text.associations}}</t-studyplan-associate>
|
|
</span>
|
|
<span class='control editable'>
|
|
<t-studyplan-edit v-model="value" @moved="movedStudyplan"
|
|
><i class='fa fa-gear'></i> {{text.edit}}</t-studyplan-edit>
|
|
</span>
|
|
<span class='control deletable'>
|
|
<a v-if='value.pages.length == 0' href='#' @click.prevent='deletePlan(value)'
|
|
><i class='text-danger fa fa-trash'></i></a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<b-card no-body>
|
|
<b-tabs
|
|
v-model='selectedpageindex'
|
|
@activate-tab='selectedpageChanged'
|
|
content-class="mt-1">
|
|
<!-- New Tab Button (Using tabs-end slot) -->
|
|
<template #tabs-end>
|
|
<t-studyplan-page-edit
|
|
v-if="!coaching"
|
|
:studyplan="value"
|
|
v-model="create.page"
|
|
type="link"
|
|
mode="create"
|
|
@created="pagecreated"
|
|
><i class='fa fa-plus'></i></t-studyplan-page-edit>
|
|
</template>
|
|
<b-tab
|
|
v-for="(page,pageindex) in value.pages"
|
|
:key="page.id"
|
|
>
|
|
<template #title>
|
|
{{page.shortname}}
|
|
<t-studyplan-page-edit
|
|
v-if="!coaching && (pageindex == selectedpageindex)"
|
|
v-model="value.pages[pageindex]"
|
|
:studyplan="value"
|
|
type="link"
|
|
></t-studyplan-page-edit>
|
|
</template>
|
|
<div class='t-studyplan-content-edit'
|
|
v-if="edit.studyline.editmode">
|
|
<drop-list
|
|
:items="page.studylines"
|
|
class="t-slot-droplist"
|
|
:accepts-type="'studyline-'+page.id"
|
|
@reorder="reorderLines($event,page.studylines)"
|
|
mode="copy"
|
|
row
|
|
>
|
|
<template v-slot:item="{item}">
|
|
<drag
|
|
:key="item.id"
|
|
class='t-studyline-drag'
|
|
:data="item"
|
|
:type="'studyline-'+page.id"
|
|
>
|
|
<template v-slot:drag-image>
|
|
<i class="fa fa-arrows text-primary"></i>
|
|
</template>
|
|
<t-studyline-edit
|
|
v-if="!coaching"
|
|
v-model="item"
|
|
@edit='editLineStart(item)'
|
|
@delete='deleteLine(page,item)'
|
|
>
|
|
<div v-if="!slotsempty(item.slots)"> {{ text.editmode_modules_hidden}} </div>
|
|
</t-studyline-edit>
|
|
</drag>
|
|
</template>
|
|
</drop-list>
|
|
</div>
|
|
<div class='t-studyplan-content' v-else>
|
|
<!-- Now paint the headings column -->
|
|
<div class='t-studyplan-headings'>
|
|
<s-studyline-header-heading :identifier='Number(page.id)'></s-studyline-header-heading>
|
|
<template v-if="page.studylines.length > 0">
|
|
<t-studyline-heading v-for="(line,lineindex) in page.studylines"
|
|
:key="line.id"
|
|
@resize="headingresized(lineindex,$event)"
|
|
v-model="page.studylines[lineindex]"
|
|
:layers='countLineLayers(line,page)+1'
|
|
:class=" 't-studyline' + ((lineindex%2==0)?' odd ' :' even ' )
|
|
+ ((lineindex==0)?' first ':' ')
|
|
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
|
|
></t-studyline-heading>
|
|
</template>
|
|
<t-studyline-heading v-else
|
|
@resize="headingresized(0,$event)"
|
|
|
|
:layers="1"
|
|
:class="'odd first last'"
|
|
></t-studyline-heading>
|
|
</div>
|
|
<!-- Next, paint all the cells in the scrollable -->
|
|
<div class="t-studyplan-scrollable" >
|
|
<div class="t-studyplan-timeline" :style="columnsStylerule(page)">
|
|
<!-- add period information -->
|
|
<template v-for="(n,index) in (page.periods+1)">
|
|
<s-studyline-header-period
|
|
mode="edit"
|
|
:x-index="index"
|
|
:style="'grid-area: 1 / '+ ((2*index)) +';'"
|
|
:identifier='Number(page.id)'
|
|
v-if="index > 0"
|
|
v-model="page.perioddesc[index-1]"
|
|
><t-period-edit
|
|
v-if="!coaching"
|
|
:ref="'periodeditor-'+index"
|
|
@edited="periodEdited"
|
|
v-model="page.perioddesc[index-1]"
|
|
:minstart="(index > 1) ? addDay(page.perioddesc[index-2].startdate,2) : null"
|
|
:maxend="(index < page.periods) ? subDay(page.perioddesc[index].enddate,2) : null"
|
|
></t-period-edit
|
|
></s-studyline-header-period>
|
|
<div class="s-studyline-header-filter"
|
|
:x-index="index"
|
|
:style="'grid-area: 1 / '+ ((2*index)+1) +';'"
|
|
></div>
|
|
</template>
|
|
|
|
<!-- Line by line add the items -->
|
|
<!-- The grid layout handles putting it in rows and columns -->
|
|
<template v-for="(line,lineindex) in page.studylines"
|
|
><template v-for="(layernr,layeridx) in countLineLayers(line,page)+1"
|
|
><template v-for="(n,index) in (page.periods+1)"
|
|
><t-studyline-slot
|
|
v-if="index > 0 && showslot(page,line, index, layeridx, 'gradable')"
|
|
:style="'grid-area: '+ (1+sumLineLayers(lineindex,page)+layernr)
|
|
+ ' / ' + (2 * index)
|
|
+ ' / ' + (1 + sumLineLayers(lineindex,page)+layernr)
|
|
+ ' / ' + ( (2 * index) + (2*span(line,index,layeridx) - 1)) + ';'"
|
|
type='gradable'
|
|
v-model="line.slots[index].courses"
|
|
:key="'c-'+lineindex+'-'+index+'-'+layernr"
|
|
:slotindex="index"
|
|
:line="line"
|
|
:plan="value"
|
|
:page="page"
|
|
:period="page.perioddesc[index-1]"
|
|
:layer="layeridx"
|
|
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
|
+ ((lineindex==0 && layernr==1)?' first ':' ')
|
|
+ ((lineindex==page.studylines.length-1)?' last ':' ')
|
|
+ ((layernr == countLineLayers(line,page))?' lastlyr ':' ')
|
|
+ ((layernr == countLineLayers(line,page)+1)?' newlyr ':' ')"
|
|
></t-studyline-slot
|
|
><t-studyline-slot
|
|
type='filter'
|
|
:style="'grid-area: '+ (1+sumLineLayers(lineindex,page)+layernr) + ' / ' + (2*index+1) +';'"
|
|
v-if="showslot(page,line, index, layeridx, 'filter')"
|
|
v-model="line.slots[index].filters"
|
|
:key="'f-'+lineindex+'-'+index+'-'+layernr"
|
|
:slotindex="index"
|
|
:line="line"
|
|
:plan="value"
|
|
:page="page"
|
|
:layer="layeridx"
|
|
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
|
+ ((lineindex==0 && layernr==1)?' first ':'')
|
|
+ ((lineindex==page.studylines.length-1)?' last ':' ')
|
|
+ ((index==page.periods)?' rightmost':'')
|
|
+ ((layernr == countLineLayers(line,page))?' lastlyr ':' ')
|
|
+ ((layernr == countLineLayers(line,page)+1)?' newlyr ':' ')"
|
|
></t-studyline-slot
|
|
></template
|
|
></template
|
|
></template
|
|
><template v-if="hivizdrop"
|
|
><template v-for="(line,lineindex) in page.studylines"
|
|
><template v-for="(n,index) in (page.periods+1)"
|
|
><drop v-if="index > 0"
|
|
:style="'grid-area: '+ (2 + sumLineLayers(lineindex,page))
|
|
+ ' / ' + (2 * index)
|
|
+ ' / ' + (1 + sumLineLayers(lineindex + 1,page))
|
|
+ ' / ' + (2 * index) + '; overflow: hidden;'"
|
|
:class="'t-slot-drop t-slot-linedrop course hiviz'"
|
|
:accepts-type="checkTypeCourse"
|
|
@drop="onDrop($event,line,index)"
|
|
mode="cut"
|
|
><span>{{text.drophere}}</span></drop
|
|
><drop
|
|
:style="'grid-area: '+ (2 + sumLineLayers(lineindex,page))
|
|
+ ' / ' + ((2 * index) + 1)
|
|
+ ' / ' + (1 + sumLineLayers(lineindex + 1,page))
|
|
+ ' / ' + ( (2 * index) + 1 ) + '; overflow: hidden;'"
|
|
:class="'t-slot-drop t-slot-linedrop filter hiviz'"
|
|
:accepts-type="checkTypeFilter"
|
|
@drop="onDrop($event,line,index)"
|
|
mode="cut"
|
|
><span>{{text.drophere}}</span></drop
|
|
></template
|
|
></template
|
|
></template
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<div v-if="edit.studyline.editmode" class='t-studyline-add ml-2 mt-1'>
|
|
<a href="#" v-b-modal="'modal-add-studyline-'+page.id" @click.prevent="false;"
|
|
><i class='fa fa-plus'></i>{{ text.studyline_add }}</a>
|
|
</div>
|
|
<b-modal
|
|
:id="'modal-add-studyline-'+page.id"
|
|
size="lg"
|
|
:ok-title="text.add"
|
|
ok-variant="primary"
|
|
:title="text.studyline_add"
|
|
@ok="addStudyLine(page,create.studyline)"
|
|
:ok-disabled="Math.min(create.studyline.name.length,create.studyline.shortname.length) == 0"
|
|
>
|
|
<b-container>
|
|
<b-row>
|
|
<b-col cols="3">{{text.studyline_name}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="create.studyline.name" :placeholder="text.studyline_name_ph"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="3">{{text.studyline_shortname}}</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
v-model="create.studyline.shortname"
|
|
:placeholder="text.studyline_shortname_ph"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="3">{{text.studyline_color}}</b-col>
|
|
<b-col>
|
|
<input type="color" v-model="create.studyline.color" />
|
|
<!-- hsluv-picker v-model="create.studyline.color" horizontal displaysize="175" ></hsluv-picker -->
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="3">{{ text.studyline_enrollable}}</b-col>
|
|
<b-col>
|
|
<b-form-select v-model="create.studyline.enrol.enrollable">
|
|
<b-form-select-option
|
|
v-for="(nr,n) in 4"
|
|
:value="n"
|
|
:key="n"
|
|
>{{text['line_enrollable_'+n]}}</b-form-select-option>
|
|
</b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row v-if='[2,3].includes(create.studyline.enrol.enrollable)'>
|
|
<b-col cols="3">{{ text.studyline_enrolroles}}</b-col>
|
|
<b-col>
|
|
<b-form-select
|
|
v-model="create.studyline.enrol.enrolroles"
|
|
:options="availableroles"
|
|
multiple
|
|
value-field="id"
|
|
text-field="name"
|
|
:select-size="6"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-modal>
|
|
<b-modal
|
|
:id="'modal-edit-studyline-'+page.id"
|
|
size="lg"
|
|
ok-variant="primary"
|
|
:title="text.studyline_edit"
|
|
@ok="editLineFinish()"
|
|
:ok-disabled="Math.min(edit.studyline.data.name.length,edit.studyline.data.shortname.length) == 0"
|
|
>
|
|
<b-container>
|
|
<b-row>
|
|
<b-col cols="3">{{ text.studyline_name}}</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
v-model="edit.studyline.data.name"
|
|
:placeholder="text.studyline_name_ph"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="3">{{ text.studyline_shortname}}</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
v-model="edit.studyline.data.shortname"
|
|
:placeholder="text.studyline_shortname_ph"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="3">{{ text.studyline_color}}</b-col>
|
|
<b-col>
|
|
<input type="color" v-model="edit.studyline.data.color" />
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="3">{{ text.studyline_enrollable}}</b-col>
|
|
<b-col>
|
|
<b-form-select v-model="edit.studyline.data.enrol.enrollable">
|
|
<b-form-select-option
|
|
v-for="(nr,n) in 4"
|
|
:value="n"
|
|
>{{text['line_enrollable_'+n]}}</b-form-select-option>
|
|
</b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row v-if='[2,3].includes(edit.studyline.data.enrol.enrollable)'>
|
|
<b-col cols="3">{{ text.studyline_enrolroles}}</b-col>
|
|
<b-col>
|
|
<b-form-select
|
|
v-model="edit.studyline.data.enrol.enrolroles"
|
|
:options="availableroles"
|
|
multiple
|
|
value-field="id"
|
|
text-field="name"
|
|
:select-size="6"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-modal>
|
|
</b-tab>
|
|
</b-tabs>
|
|
</b-card>
|
|
</div>
|
|
`
|
|
});
|
|
|
|
/*
|
|
* 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: `
|
|
<div class="t-studyline t-studyline-heading "
|
|
:data-studyline="value.id" ref="main"
|
|
><div class="t-studyline-handle" :style="'background-color: ' + value.color"></div>
|
|
<div class="t-studyline-title">
|
|
<abbr v-b-tooltip.hover.right :title="value.name">{{ value.shortname }}</abbr>
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
/*
|
|
* 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: `
|
|
<div :class="'t-studyline '" >
|
|
<div class="t-studyline-handle" :style="'background-color: ' + value.color"></div>
|
|
<div class="t-studyline-title">
|
|
<div>
|
|
<i class='fa fa-arrows text-primary'></i>
|
|
<abbr v-b-tooltip.hover :title="value.name">{{ value.shortname }}</abbr>
|
|
</div>
|
|
</div>
|
|
<div class='t-studyline-editmode-content'>
|
|
<slot></slot>
|
|
</div>
|
|
<div class='controlbox'>
|
|
<template v-if='editable || deletable'>
|
|
<span class='control editable' v-if='editable'>
|
|
<a href='#' @click.prevent='onEdit'><i class='fa fa-pencil'></i></a>
|
|
</span>
|
|
<span class='control deletable' v-if='deletable'>
|
|
<a v-if='deletable' href='#' @click.prevent='onDelete'><i class='text-danger fa fa-trash'></i></a>
|
|
</span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
/*
|
|
* 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: `
|
|
<div :class=" 't-studyline-slot '+type
|
|
+ ' t-studyline-slot-'+slotindex + ' '
|
|
+ ((slotindex==0)?' t-studyline-firstcolumn ':' ')
|
|
+ 'periodcount-' + page.periods + ' '"
|
|
:data-studyline="line.id" ref="main"
|
|
:style='spanCss'
|
|
><drag v-if="item"
|
|
:key="item.id"
|
|
class="t-slot-item"
|
|
:data="item"
|
|
:type="makeType(item)"
|
|
@cut="onCut"
|
|
><t-item
|
|
@deleted="onCut"
|
|
v-model="value[itemidx]"
|
|
:plan="plan"
|
|
:line='line'
|
|
:page='page'
|
|
:period='period'
|
|
:maxspan='maxSpan()'
|
|
></t-item
|
|
></drag
|
|
><drop v-else
|
|
:class="'t-slot-drop '+type + (layer > 0?' secondary':' primary') + (hivizdrop()?' hiviz':'')"
|
|
:accepts-type="checkType"
|
|
@drop="onDrop"
|
|
mode="cut"
|
|
@dragenter="onDragEnter"
|
|
@dragleave="onDragLeave"
|
|
><template v-if="hover.component">
|
|
<div v-if="hover.type.item"
|
|
class="t-slot-item feedback"
|
|
:key="hover.component.id"
|
|
><t-item v-model="hover.component" dummy></t-item
|
|
></div
|
|
><div v-else-if="hover.type.type == 'gradable'"
|
|
class="t-slot-item feedback"
|
|
:key="'course-'+hover.component.id"
|
|
><t-item-course v-model="courseHoverDummy"></t-item-course></div
|
|
><div v-else-if="hover.type.type == 'filter'"
|
|
class="t-slot-item feedback"
|
|
key="tooldrop"
|
|
><t-item-junction v-if="hover.component.type == 'junction'" ></t-item-junction
|
|
><t-item-start v-else-if="hover.component.type == 'start'" ></t-item-start
|
|
><t-item-finish v-else-if="hover.component.type == 'finish'" ></t-item-finish
|
|
><t-item-badge v-else-if="hover.component.type == 'badge'" ></t-item-badge
|
|
></div
|
|
><div v-else
|
|
class="t-slot-item feedback"
|
|
:key="hover.type">--{{ hover.type }}--</div
|
|
></template
|
|
><span v-else-if="hivizdrop()">{{plantext.drophere}}</span></drop>
|
|
<t-item-timing-checker hidden
|
|
v-if="value && value[itemidx] && value[itemidx].course"
|
|
ref="timingChecker"
|
|
:maxspan="maxSpan()"
|
|
:page="page"
|
|
:line="line"
|
|
:period="period"
|
|
v-model="value[itemidx]"
|
|
></t-item-timing-checker>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
|
|
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: `
|
|
<div :class="'t-item-timing-checker'" :style="hidden?'display: none ':''">
|
|
<template v-if="!hidden" >
|
|
<span class="mr-1" v-if="coursePeriodMatches">
|
|
<i class="text-success fa fa-calendar-check-o"
|
|
v-b-tooltip.hover.topright :title="text.timing_ok"
|
|
></i>
|
|
</span>
|
|
<span class="mr-1" v-else>
|
|
<a href='#' @click.prevent="validateCoursePeriod()" class="text-warning"
|
|
v-b-tooltip.hover.bottomleft :title="text.timing_off"
|
|
><i class="fa fa-calendar-times-o"
|
|
></i
|
|
><i class="fa fa-question-circle text-black-50"
|
|
style="font-size: 0.8em; top: -0.3em; position: relative;"
|
|
|
|
></i
|
|
></a>
|
|
</span>
|
|
<span class="ml-1" v-b-tooltip.hover.bottomleft :title="text.periodspan_desc"
|
|
>{{ text.periodspan
|
|
}} <b-form-select v-if="maxspan > 1"
|
|
class=""
|
|
size="sm" @change="changeSpan" v-model="value.span">
|
|
<b-form-select-option v-for="(n,i) in maxspan" :value='n' :key='i'
|
|
>{{ n }}</b-form-select-option>
|
|
</b-form-select
|
|
><span v-else>{{value.span}}</span> {{
|
|
(value.span == 1)?text.period.toLowerCase():text.periods.toLowerCase()
|
|
}}<i
|
|
class="fa fa-question-circle text-black-50"
|
|
style="font-size: 0.8em; top: -0.3em; position: relative;"
|
|
></i>
|
|
</span>
|
|
</template>
|
|
<b-modal
|
|
:id="'t-course-timing-matching-'+this.id"
|
|
size="lg"
|
|
:title="text.title"
|
|
@ok="changeCoursePeriod"
|
|
:ok-title="text.yes"
|
|
ok-variant="danger"
|
|
:cancel-title="text.no"
|
|
cancel-variant="primary"
|
|
>
|
|
<b-container v-if="datechanger.coursespan && datechanger.periodspan && value && value.course">
|
|
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row>
|
|
<b-row><b-col cols="12"><div class="generalbox alert alert-warning">{{ text.question }}</div></b-col></b-row>
|
|
<b-row>
|
|
<b-col cols="6">
|
|
<h3> {{ text.course }} </h3>
|
|
<p class="mb-0"><b>{{ value.course.fullname }}</b></p>
|
|
<p class="mb-1"><b>{{ value.course.shortname }}</b></p>
|
|
<p class="mb-1">{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
|
|
<p class="mb-0"><b>{{ text.duration }}</b><br>
|
|
{{ formatDuration(datechanger.coursespan)}}</p>
|
|
</b-col>
|
|
<b-col cols="6">
|
|
<h3> {{ text.period }} </h3>
|
|
<p class="mb-0"><b>{{ period.fullname }}</b><b v-if="value.span > 1"> - {{ endperiod.fullname }}</b></p>
|
|
<p class="mb-1"><b>{{ period.shortname }}</b><b v-if="value.span > 1"> - {{ endperiod.shortname }}</b></p>
|
|
<p class="mb-1">{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
|
|
<p class="mb-0"><b>{{ text.duration }}</b><br>
|
|
{{ formatDuration(datechanger.periodspan)}}</p>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row v-if='hidden' class="pt-2"><b-col cols="12">
|
|
<b-form-checkbox type="checkbox" v-model="datechanger.globals.default">{{ text.rememberchoice }}</b-form-checkbox>
|
|
</b-col></b-row>
|
|
</b-container>
|
|
</b-modal>
|
|
<b-modal
|
|
:id="'t-course-timing-warning-'+this.id"
|
|
size="lg"
|
|
ok-variant="primary"
|
|
:title="text.title"
|
|
:ok-title="text.yes"
|
|
ok-only
|
|
>
|
|
<b-container v-if="datechanger.coursespan && datechanger.periodspan && value && value.course">
|
|
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row>
|
|
<b-row><b-col cols="12"><div class="generalbox alert alert-warning">{{ text.warning }}</div></b-col></b-row>
|
|
<b-row>
|
|
<b-col cols="6">
|
|
<h3> {{ text.course }} </h3>
|
|
<p class="mb-0"><b>{{ value.course.fullname }}</b></p>
|
|
<p class="mb-1"><b>{{ value.course.shortname }}</b></p>
|
|
<p class="mb-1">{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
|
|
<p class="mb-0"><b>{{ text.duration }}</b><br>
|
|
{{ formatDuration(datechanger.coursespan)}}</p>
|
|
</b-col>
|
|
<b-col cols=>"6">
|
|
<h3> {{ text.period }} </h3>
|
|
<p class="mb-0"><b>{{ period.fullname }}</b><b v-if="value.span > 1"> - {{ endperiod.fullname }}</b></p>
|
|
<p class="mb-1"><b>{{ period.shortname }}</b><b v-if="value.span > 1"> - {{ endperiod.shortname }}</b></p>
|
|
<p class="mb-1">{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
|
|
<p class="mb-0"><b>{{ text.duration }}</b><br>
|
|
{{ formatDuration(datechanger.periodspan)}}</p>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row v-if='hidden' class="pt-2"><b-col cols="12">
|
|
<b-form-checkbox type="checkbox" v-model="datechanger.globals.hidewarn">{{ text.hidewarning }}</b-form-checkbox>
|
|
</b-col></b-row>
|
|
</b-container>
|
|
</b-modal>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
|
|
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_treestudyplan_edit_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: `
|
|
<div class="t-item-base" :id="'studyitem-'+value.id">
|
|
<t-item-course
|
|
v-if="value.type == 'course'"
|
|
@deleterq="deleteItem"
|
|
v-model="value"
|
|
:plan='plan'
|
|
:line='line'
|
|
:page='page'
|
|
:period='period'
|
|
:maxspan='maxspan'
|
|
></t-item-course>
|
|
<t-item-junction
|
|
v-if="value.type == 'junction'"
|
|
@deleterq="deleteItem"
|
|
v-model="value"
|
|
></t-item-junction>
|
|
<t-item-start
|
|
v-if="value.type == 'start'"
|
|
@deleterq="deleteItem"
|
|
v-model="value"
|
|
></t-item-start>
|
|
<t-item-finish
|
|
v-if="value.type == 'finish'"
|
|
@deleterq="deleteItem"
|
|
v-model="value"
|
|
></t-item-finish>
|
|
<t-item-badge
|
|
v-if="value.type == 'badge'"
|
|
@deleterq="deleteItem"
|
|
v-model="value"
|
|
></t-item-badge>
|
|
<t-item-invalid
|
|
v-if="value.type == 'invalid'"
|
|
@deleterq="deleteItem"
|
|
v-model="value"
|
|
></t-item-invalid>
|
|
<drop v-if='!dummy && hasConnectionsIn' accepts-type="linestart"
|
|
:id="'t-item-cend-'+value.id"
|
|
class="t-item-connector-end"
|
|
mode="copy"
|
|
@drop="onDrop"
|
|
><svg width='5px' height='10px'><rect ry="1px" rx="1px" y="0px" x="0px" height="10px" width="5px"/></svg></drop>
|
|
<drag v-if='!dummy && hasConnectionsOut' type="linestart"
|
|
:id="'t-item-cstart-'+value.id"
|
|
:class="'t-item-connector-start ' + ((deleteMode&&value.connections.out.length)?'deleteMode':'')"
|
|
:data="value"
|
|
@dragstart="dragStart"
|
|
@dragend="dragEnd"
|
|
@click.prevent="deleteMode = (value.connections.out.length)?(!deleteMode):false"
|
|
>
|
|
<svg width='5px' height='10px'><rect ry="1px" rx="1px" y="0px" x="0px" height="10px" width="5px"/></svg>
|
|
<template v-slot:drag-image="{data}"> <i :id="'t-item-cdrag-'+value.id" class="fa"></i>
|
|
</template>
|
|
</drag>
|
|
<div class="deletebox" v-if="deleteMode && value.connections.out.length > 0"
|
|
>
|
|
<a v-for="conn in value.connections.out"
|
|
@click.prevent="deleteLine(conn)"
|
|
@mouseenter="highlight(conn)"
|
|
@mouseleave="normalize(conn)"
|
|
class="t-connection-delete text-danger"
|
|
:title="conn.id">
|
|
<i class="fa fa-trash"></i>
|
|
</a>
|
|
</div>
|
|
<a v-if="hasContext" class="t-item-config"
|
|
v-b-modal="'t-item-config-'+value.id" href="#" @click.prevent=""><i class="fa fa-gear"></i></a>
|
|
<b-modal no-body class=""
|
|
:id="'t-item-config-'+value.id"
|
|
:title="text['item_configuration']"
|
|
scrollable
|
|
ok-only
|
|
class="t-item-contextview b-modal-justify-footer-between"
|
|
>
|
|
<b-form-group
|
|
v-if="value.type != 'start'"
|
|
:label="text.select_conditions"
|
|
>
|
|
<b-form-select size="sm"
|
|
@input="updateItem"
|
|
v-model="value.conditions"
|
|
:options="conditionOptions"
|
|
></b-form-select>
|
|
</b-form-group>
|
|
|
|
<template #modal-footer="{ ok, cancel, hide }" >
|
|
<a href='#' @click.prevent='deleteItem()' class="text-danger"
|
|
><i class="fa fa-trash"></i>
|
|
{{ text.delete }}
|
|
</a>
|
|
<b-button size="sm" variant="primary" @click.prevent="ok()">
|
|
{{ text.ok }}
|
|
</b-button>
|
|
</template>
|
|
|
|
</b-modal>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-invalid', {
|
|
props: {
|
|
'value': {
|
|
type: Object,
|
|
default() {
|
|
return null;
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
text: strings.invalid,
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<div class="t-item-invalid">
|
|
<b-card no-body >
|
|
<b-row no-gutters>
|
|
<b-col md="1">
|
|
<span class="t-timing-indicator timing-invalid"></span>
|
|
</b-col>
|
|
<b-col md="11">
|
|
<b-card-body class="align-items-center">
|
|
<i class="fa fa-exclamation"></i> {{text.error}}
|
|
<a href='#' @click.prevent='$emit("deleterq")' class="text-danger"
|
|
><i class="fa fa-trash"></i></a>
|
|
</b-card-body>
|
|
</b-col>
|
|
</b-row>
|
|
</b-card>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
// 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_treestudyplan_edit_studyitem',
|
|
args: {
|
|
'id': this.value.id,
|
|
'conditions': this.value.conditions,
|
|
}
|
|
}])[0].catch(notification.exception);
|
|
},
|
|
},
|
|
template: `
|
|
<div class="t-item-course card">
|
|
<div class='t-item-course-cardwrapper mr-0 ml-0 h-100 '>
|
|
<div
|
|
:title="text['coursetiming_'+value.course.timing]"
|
|
v-b-popover.hover.top="startdate+' - '+enddate"
|
|
:class="'h-100 t-timing-indicator timing-'+value.course.timing"
|
|
></div>
|
|
<div class="t-item-course-title card-body h-100">
|
|
<fittext maxsize="12pt" minsize="9pt">
|
|
<a v-b-modal="'t-item-course-config-'+value.id"
|
|
:id="'t-item-course-details-'+value.id"
|
|
:href="wwwroot+'/course/view.php?id='+value.course.id"
|
|
@click.prevent.stop="">{{ value.course.displayname }}</a>
|
|
</fittext>
|
|
</div>
|
|
<div class="h-100 t-item-course-indicator ">
|
|
<a class="t-item-course-config"
|
|
v-b-modal="'t-item-course-config-'+value.id"
|
|
href="#" @click.prevent=""
|
|
><i :class="'fa fa-'+configurationIcon+' ' + configurationState"></i></a>
|
|
</div>
|
|
</div>
|
|
<b-modal class=""
|
|
:id="'t-item-course-config-'+value.id"
|
|
:title="value.course.displayname + ' - ' + value.course.fullname"
|
|
ok-only
|
|
size="lg"
|
|
scrollable
|
|
class="b-modal-justify-footer-between"
|
|
>
|
|
<template #modal-header>
|
|
<div>
|
|
<h1><a :href="wwwroot+'/course/view.php?id='+value.course.id" target="_blank"
|
|
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a>
|
|
<a v-if='!!value.course.completion'
|
|
:href="wwwroot+'/course/completion.php?id='+value.course.id" target="_blank"
|
|
:title="text.configure_completion"><i class="fa fa-gear"></i></a>
|
|
</h1>
|
|
{{ value.course.context.path.join(" / ")}} / {{value.course.shortname}}
|
|
</div>
|
|
<div class="r-course-detail-header-right">
|
|
<div :class="'r-timing-'+value.course.timing">
|
|
{{text['coursetiming_'+value.course.timing]}}<br>
|
|
{{ startdate }} - {{ enddate }}
|
|
</div>
|
|
<t-item-timing-checker
|
|
class="mt-1"
|
|
:maxspan="maxspan"
|
|
:page="page"
|
|
:line="line"
|
|
:period="period"
|
|
v-model="value"
|
|
></t-item-timing-checker>
|
|
</div>
|
|
</template>
|
|
|
|
<s-course-extrafields
|
|
v-if="value.course.extrafields"
|
|
v-model="value.course.extrafields"
|
|
position="above"
|
|
></s-course-extrafields>
|
|
<t-item-course-grades
|
|
v-if='!!value.course.grades && value.course.grades.length > 0'
|
|
v-model='value' :plan="plan"
|
|
></t-item-course-grades>
|
|
<t-item-course-completion
|
|
v-if='!!value.course.completion'
|
|
v-model='value.course.completion'
|
|
:course='value.course'
|
|
></t-item-course-completion>
|
|
<t-item-course-competency
|
|
v-if='!!value.course.competency'
|
|
v-model='value.course.competency'
|
|
:item='value'
|
|
></t-item-course-competency>
|
|
<s-course-extrafields
|
|
v-if="value.course.extrafields"
|
|
v-model="value.course.extrafields"
|
|
position="below"
|
|
></s-course-extrafields>
|
|
<template #modal-footer="{ ok, cancel, hide }" >
|
|
<a href='#' @click.prevent='$emit("deleterq")' class="text-danger"
|
|
><i class="fa fa-trash"></i>
|
|
{{ text.delete }}
|
|
</a>
|
|
<b-button size="sm" variant="primary" @click.prevent="ok()">
|
|
{{ text.ok }}
|
|
</b-button>
|
|
</template>
|
|
</b-modal>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
|
|
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: `
|
|
<div>
|
|
<b-form-group
|
|
:label="text.select_grades"
|
|
><ul class="t-item-module-children">
|
|
<li class="t-item-course-gradeinfo">
|
|
<span class='t-item-course-chk-lbl'>{{text.grade_include}}</span
|
|
><span v-if="useRequiredGrades" class='t-item-course-chk-lbl'>{{text.grade_require}}</span>
|
|
</li>
|
|
<li class="t-item-course-gradeinfo" v-for="g in value.course.grades">
|
|
<b-form-checkbox inline
|
|
@change="includeChanged($event,g)" v-model="g.selected"
|
|
></b-form-checkbox>
|
|
<b-form-checkbox v-if="useRequiredGrades" inline :disabled="!g.selected"
|
|
@change="requiredChanged($event,g)" v-model="g.required"
|
|
></b-form-checkbox>
|
|
<span :title="g.typename" v-html="g.icon"></span>
|
|
<s-edit-mod
|
|
:title="value.course.fullname"
|
|
@saved="(fd) => g.name = fd.get('name')"
|
|
v-if="g.cmid > 0"
|
|
:cmid="g.cmid"
|
|
:coursectxid="value.course.ctxid"
|
|
genericonly><span v-html="g.name"></span></s-edit-mod>
|
|
</li>
|
|
</ul>
|
|
</b-form-group>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
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: `
|
|
<table class="r-item-course-grade-details">
|
|
<tr v-if="hasCompletions">
|
|
<td colspan='2'><span v-if="value.aggregation == 'all'">{{ text.aggregation_overall_all}}</span
|
|
><span v-else>{{ text.aggregation_overall_any}}</span></td>
|
|
</tr>
|
|
<tr v-else>
|
|
<td colspan='2'>{{text.completion_not_configured}}!
|
|
<br/><a :href="wwwroot+'/course/completion.php?id='+course.id" target='_blank'>{{text.configure_completion}}</a>
|
|
</td>
|
|
</tr>
|
|
<template v-for='cgroup in value.conditions'>
|
|
<tr>
|
|
<th colspan='2'><span v-if="cgroup.items.length > 1"
|
|
><span v-if="cgroup.aggregation == 'all'">{{ text.aggregation_all}}</span
|
|
><span v-else>{{ text.aggregation_any}}</span></span>
|
|
{{cgroup.title}}</th>
|
|
</tr>
|
|
<tr v-for='ci in cgroup.items'>
|
|
<td><span v-html='ci.details.criteria'></span>
|
|
</td>
|
|
<td v-if="ci.details.requirement" class="font-italic">
|
|
{{ci.details.requirement}}
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</table>
|
|
`,
|
|
});
|
|
|
|
// 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 += `<a href="${url}">${p.title}</a>`;
|
|
}
|
|
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: `
|
|
<table class="t-item-course-competency-list">
|
|
<tr v-if="value.competencies.length == 0">
|
|
<td colspan='2'>{{text.competency_not_configured}}
|
|
<br><a :href="wwwroot+'/admin/tool/lp/coursecompetencies.php?courseid='+item.course.id" target='_blank'>{{text.configure_competency}}</a>
|
|
</td>
|
|
</tr>
|
|
<template v-else>
|
|
<tr class='t-item-course-competency-headers'>
|
|
<th>{{text.heading}}</th>
|
|
<th></th>
|
|
<th>{{text.required}}</th>
|
|
</tr>
|
|
<tr v-for='c in value.competencies'>
|
|
<td :colspan="(c.details)?1:2"><a href='#' v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a></td>
|
|
<td class='details' v-if="c.details">
|
|
<a href='#' v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.details'></span></a>
|
|
</td>
|
|
<td>
|
|
<b-form-checkbox inline
|
|
@change="requiredChanged($event,c)"
|
|
v-model="c.required"
|
|
>{{ text.required }}</b-form-checkbox>
|
|
</td>
|
|
<b-modal :id="'modal-competency-id-'+c.id"
|
|
size="lg"
|
|
ok-only
|
|
centered
|
|
scrollable
|
|
>
|
|
<template #modal-header>
|
|
<div>
|
|
<h1><i class="fa fa-puzzle-piece"></i>
|
|
<a :href="wwwroot+'/admin/tool/lp/competencies.php?competencyid='+c.id" target="_blank"
|
|
>{{c.title}} {{c.details}} </a
|
|
></h1>
|
|
<div><span v-html="pathtags(c)"></span></div>
|
|
</div>
|
|
</template>
|
|
<div class="mb-2" v-if="c.description"><span v-html='c.description'></span></div>
|
|
|
|
<template v-if="c.rule && c.children">
|
|
<div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div>
|
|
<table v-if="c.children" class='t-item-course-competency-list'>
|
|
<tr class='t-item-course-competency-headers'>
|
|
<th>{{text.heading}}</th>
|
|
<th></th>
|
|
<th>{{text.required}}</th>
|
|
</tr>
|
|
<tr v-for="cc in c.children">
|
|
<td :colspan="(c.details)?1:2" ><span v-html='cc.title'></span></td>
|
|
<td class='details' v-if="cc.details"><span v-html='cc.details'></span></td>
|
|
<td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td>
|
|
<td><span class="text-danger" v-if='cc.required'>{{ text.required }}</span></td>
|
|
</tr>
|
|
</table>
|
|
</template>
|
|
</b-modal>
|
|
</tr>
|
|
</template>
|
|
</table>
|
|
`,
|
|
});
|
|
/* **********************************
|
|
* *
|
|
* Toolbox list components *
|
|
* *
|
|
************************************/
|
|
Vue.component('t-item-junction', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default() {
|
|
return {};
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
conditionOptions: stringKeys.conditions,
|
|
};
|
|
},
|
|
methods: {
|
|
|
|
},
|
|
template: `
|
|
<div class='t-item-junction t-item-filter'>
|
|
<i class="fa fa-check-circle"></i>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-finish', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default() {
|
|
return {};
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<div class='t-item-finish t-item-filter'>
|
|
<i class="fa fa-stop-circle"></i>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-start', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default() {
|
|
return {};
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<div class='t-item-start t-item-filter'>
|
|
<i class="fa fa-play-circle"></i>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-badge', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default() {
|
|
return {
|
|
badge: {}
|
|
};
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
txt: strings,
|
|
text: strings.itemText,
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<div class='t-item-badge t-item-filter' v-b-tooltip.hover :title="value.badge.name">
|
|
<svg class="t-badge-backdrop " width='50px' height='50px' viewBox="0 0 100 100">
|
|
<title>{{value.badge.name}}</title>
|
|
<circle cx="50" cy="50" r="46"
|
|
style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/>
|
|
<image class="badge-image" clip-path="circle() fill-box"
|
|
:href="value.badge.imageurl" x="12" y="12" width="76" height="76"/>
|
|
</svg>
|
|
<a class="t-item-config badge"
|
|
v-b-modal="'t-item-badge-details-'+value.id" href="#" @click.prevent=""><i class="fa fa-gear"></i></a>
|
|
<b-modal class=""
|
|
:id="'t-item-badge-details-'+value.id"
|
|
:title="value.badge.name"
|
|
size="lg"
|
|
ok-only
|
|
centered
|
|
scrollable
|
|
class="b-modal-justify-footer-between"
|
|
>
|
|
<template #modal-header>
|
|
<div>
|
|
<h1><i class="fa fa-certificate"></i>
|
|
<a :href="value.badge.infolink" target="_blank"
|
|
>{{ value.badge.name }}</a
|
|
></h1>
|
|
</div>
|
|
</template>
|
|
<b-container fluid>
|
|
<b-row><b-col cols="3">
|
|
<img :src="value.badge.imageurl"/>
|
|
</b-col><b-col cols="9">
|
|
<p>{{value.badge.description}}</p>
|
|
<ul class="list-unstyled w-100 border-grey border-top border-bottom pt-1 pb-1 mb-1"
|
|
v-if="value.badge.criteria"><li v-for="crit in value.badge.criteria"
|
|
><span v-html='crit'></span></li></ul>
|
|
<p><strong><i class="fa fa-link"></i>
|
|
<a :href="value.badge.infolink">{{ txt.badge.badgeinfo }}</a></strong></p>
|
|
</b-col></b-row>
|
|
</b-container>
|
|
<template #modal-footer="{ ok, cancel, hide }" >
|
|
<a href='#' @click.prevent='$emit("deleterq")' class="text-danger"
|
|
><i class="fa fa-trash"></i>
|
|
{{ text.delete }}
|
|
</a>
|
|
<b-button size="sm" variant="primary" @click.prevent="ok()">
|
|
{{ text.ok }}
|
|
</b-button>
|
|
</template>
|
|
</b-modal>
|
|
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-coursecat-list', {
|
|
props: {
|
|
value: {
|
|
type: Array,
|
|
default() {
|
|
return {};
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<ul class="t-coursecat-list">
|
|
<t-coursecat-list-item
|
|
v-for="coursecat,idx in value"
|
|
v-model="value[idx]"
|
|
:key="coursecat.id"></t-coursecat-list-item>
|
|
</ul>
|
|
`,
|
|
});
|
|
|
|
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: `
|
|
<li class="t-coursecat-list-item">
|
|
<span v-if="hasDetails" v-b-toggle="'coursecat-'+value.id">
|
|
<i class="when-closed fa fa-caret-right t-caret"></i>
|
|
<i class="when-open fa fa-caret-down t-caret"></i>
|
|
<span class="t-coursecat-heading">
|
|
<i class="t-coursecat-list-item fa fa-tasks"></i>
|
|
{{ value.category.name }}
|
|
</span>
|
|
</span>
|
|
<span v-else>
|
|
<i class="when-closed fa t-caret" style="visibility: hidden"></i>
|
|
<span class="t-coursecat-heading">
|
|
<i class="t-coursecat-list-item fa fa-tasks"></i>
|
|
{{ value.category.name }}
|
|
</span>
|
|
</span>
|
|
<b-collapse v-if="hasDetails" :id="'coursecat-'+value.id"
|
|
@show="onShowDetails" :visible="!!(value.children) || !!(value.courses)">
|
|
<b-spinner class="ml-4" v-if="showSpinner" small variant="primary"></b-spinner>
|
|
<t-coursecat-list v-if="value.children" v-model="value.children"></t-coursecat-list>
|
|
<t-course-list v-if="value.courses" v-model="value.courses"></t-course-list>
|
|
</b-collapse>
|
|
</li>
|
|
`,
|
|
});
|
|
|
|
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: `
|
|
<ul class="t-course-list">
|
|
<li class="t-course-list-item" v-for="course in value" :key="course.id">
|
|
<span class='t-course-heading'>
|
|
<drag
|
|
class="draggable-course"
|
|
:data="course"
|
|
:type="makeType()"
|
|
@cut=""
|
|
>
|
|
<i class="t-course-list-item fa fa-book"></i> {{ course.shortname }} - {{ course.fullname }}
|
|
</drag>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
`,
|
|
});
|
|
|
|
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")),
|
|
toolboxcoursesonly: settings("toolboxcoursesonly"),
|
|
enablebadges: settings("enablebadges"),
|
|
allowcoursebadges: settings("badges_allowcoursebadges"),
|
|
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;
|
|
debug.info("Toolbox Loading courses and categories");
|
|
call([{
|
|
methodname: 'local_treestudyplan_map_categories',
|
|
args: {
|
|
'studyplan_id': self.studyplanid,
|
|
}
|
|
}])[0].then((response) => {
|
|
debug.info("Toolbox got courses and categories", response);
|
|
self.courses = response;
|
|
self.loadingcourses = false;
|
|
return;
|
|
}).catch(notification.exception);
|
|
|
|
if (!this.toolboxcoursesonly && this.enablebadges) {
|
|
this.filterSystembadges();
|
|
if (this.allowcoursebadges) {
|
|
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: `
|
|
<div class="t-toolbox">
|
|
<p-sidebar
|
|
class="t-toolbox-sidebar"
|
|
:right='toolboxright'
|
|
shadow
|
|
v-model="value"
|
|
offsetRef="#page"
|
|
>
|
|
<div class="pt-3 pl-3 pr-3 border-bottom-1 border-primary"><h3>{{text.toolbox}}</h3></div>
|
|
<div class='t-toolbox-preface'>
|
|
<b-form-checkbox v-model="toolboxright" switch>{{text.toolbarRight}}</b-form-checkbox>
|
|
</div>
|
|
<b-tabs content-class='mt-3' class="t-toolbox-tabs">
|
|
<b-tab :title="text.courses" class="t-toolbox-tab">
|
|
<div v-if="loadingcourses"
|
|
><div class="spinner-border text-primary" role="status"></div
|
|
></div>
|
|
<div v-else class="ml-2 t-toolbox-courses">
|
|
<div class="flex-grow-0">
|
|
<input v-model="filters.courses" :placeholder="text.filter"></input>
|
|
<a @click="filters.courses = ''" v-if="filters.courses" href='#'
|
|
><i class='fa fa-times'></i></a
|
|
><b-spinner small v-if="loadingcategories.length > 0" variant="primary"></b-spinner>
|
|
</div>
|
|
<div class="t-toolbox-courselist">
|
|
<t-coursecat-list v-model="filteredCourses"></t-coursecat-list>
|
|
</div>
|
|
</div>
|
|
</b-tab>
|
|
<b-tab :title="text.flow" v-if="!toolboxcoursesonly" class="t-toolbox-tab">
|
|
<ul class="t-flow">
|
|
<li><drag
|
|
:type="filterComponentType"
|
|
:data="{type: 'junction'}"
|
|
@cut=""
|
|
><t-item-junction></t-item-junction>{{ text.toolJunction }}
|
|
<template v-slot:drag-image="{data}"><t-item-junction></t-item-junction></template>
|
|
</drag></li>
|
|
<li><drag
|
|
:type="filterComponentType"
|
|
:data="{type: 'finish'}"
|
|
@cut=""
|
|
><t-item-finish></t-item-finish>{{ text.toolFinish }}
|
|
<template v-slot:drag-image="{data}"><t-item-finish></t-item-finish></template>
|
|
</drag></li>
|
|
<li><drag
|
|
:type="filterComponentType"
|
|
:data="{type: 'start'}"
|
|
@cut=""
|
|
><t-item-start></t-item-start>{{ text.toolStart }}
|
|
<template v-slot:drag-image="{data}"><t-item-start></t-item-start></template>
|
|
</drag></li>
|
|
</ul>
|
|
</b-tab>
|
|
<b-tab :title="text.badges" v-if="!toolboxcoursesonly && enablebadges" class="t-toolbox-tab">
|
|
|
|
<b-tabs content-class='mt-2 ml-2' class="t-toolbox-badges">
|
|
<b-tab :title="text.relatedbadges" v-if="allowcoursebadges">
|
|
<div class="t-toolbox-badges-filter">
|
|
<input v-model="filters.relatedbadges" @input="filterRelatedbadges" :placeholder="text.filter"></input>
|
|
<a @click="resetRelatedbadges" v-if="filters.relatedbadges" href='#'
|
|
><i class='fa fa-times'></i></a>
|
|
</div>
|
|
<div class="t-toolbox-badges-list">
|
|
<ul class="t-badges">
|
|
<li v-for="b in relatedbadges"><drag
|
|
class="t-badge-drag"
|
|
:type="filterComponentType"
|
|
:data="{type: 'badge', badge: b}"
|
|
@cut=""
|
|
><img :class="(!b.active)?'disabled':''" :src="b.imageurl" :alt="b.name">
|
|
<span :class="(!b.active)?'disabled':''">{{b.name}}</span>
|
|
<template v-slot:drag-image="{data}"
|
|
><img :class="(!b.active)?'disabled':''" :src="b.imageurl" :alt="b.name"
|
|
></template>
|
|
</drag></li>
|
|
</ul>
|
|
</div>
|
|
</b-tab>
|
|
<b-tab :title="text.sitebadges">
|
|
<div class="t-toolbox-badges-filter">
|
|
<input v-model="filters.systembadges" @input="filterSystembadges" :placeholder="text.filter"></input>
|
|
<a @click="resetSystembadges" v-if="filters.systembadges" href='#'
|
|
><i class='fa fa-times'></i></a>
|
|
</div>
|
|
<div class="t-toolbox-badges-list">
|
|
<ul class="t-badges">
|
|
<li v-for="b in systembadges"><drag
|
|
class="t-badge-drag"
|
|
:type="filterComponentType"
|
|
:data="{type: 'badge', badge: b}"
|
|
@cut=""
|
|
><img :class="(!b.active)?'disabled':''" :src="b.imageurl" :alt="b.name">
|
|
<span :class="(!b.active)?'disabled':''">{{b.name}}</span>
|
|
<template v-slot:drag-image="{data}"
|
|
><img :class="(!b.active)?'disabled':''" :src="b.imageurl" :alt="b.name"
|
|
></template>
|
|
</drag></li>
|
|
</ul>
|
|
</div>
|
|
</b-tab>
|
|
</b-tabs>
|
|
</b-tab>
|
|
</b-tabs>
|
|
</p-sidebar>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
},
|
|
|
|
};
|