moodle_local_treestudyplan/amd/src/studyplan-editor-components.js

3886 lines
174 KiB
JavaScript
Raw Normal View History

/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
2023-07-15 22:00:17 +02:00
/*eslint no-unused-vars: warn */
/*eslint max-len: ["error", { "code": 160 }] */
/*eslint-disable no-trailing-spaces */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
2023-08-19 17:54:40 +02:00
import {SimpleLine} from "./simpleline/simpleline";
import {call} from 'core/ajax';
import notification from 'core/notification';
import {get_strings} from 'core/str';
import {load_stringkeys, load_strings, strformat } from './util/string-helper';
import {format_date, add_days, datespaninfo } from './util/date-helper';
import {objCopy,transportItem} from './studyplan-processor';
2023-08-19 17:54:40 +02:00
import Debugger from './util/debugger';
import {download,upload} from './downloader';
import {ProcessStudyplan, ProcessStudyplanPage} from './studyplan-processor';
import {eventTypes as editSwEventTypes} from 'core/edit_switch';
2023-08-16 23:36:11 +02:00
2023-09-01 12:27:56 +02:00
import TSComponents from './treestudyplan-components';
2023-10-20 15:08:54 +02:00
import mFormComponents from "./util/mform-helper";
2023-09-01 12:27:56 +02:00
2023-09-08 12:47:29 +02:00
const STUDYPLAN_EDITOR_FIELDS =
2023-08-09 12:20:05 +02:00
['name','shortname','description','idnumber','context_id', 'aggregation','aggregation_config'];
2023-08-03 18:44:57 +02:00
const STUDYPLAN_EDITOR_PAGE_FIELDS = //TODO: Add 'fullname', 'shortname' and 'description' when implementing proper page management
['context_id', 'periods','startdate','enddate'];
2023-09-08 12:47:29 +02:00
const PERIOD_EDITOR_FIELDS =
2023-08-03 18:44:57 +02:00
['fullname','shortname','startdate','enddate'];
2023-08-04 22:54:32 +02:00
const LINE_GRAVITY = 1.3;
const datechanger_globals = {
default: false,
defaultchoice: false,
hidewarn: false,
};
export default {
STUDYPLAN_EDITOR_FIELDS: STUDYPLAN_EDITOR_FIELDS, // make copy available in plugin
install(Vue/*,options*/){
2023-09-01 12:27:56 +02:00
Vue.use(TSComponents);
2023-10-20 15:08:54 +02:00
Vue.use(mFormComponents);
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 string_keys = load_stringkeys({
conditions: [
{ value: 'ALL', textkey: 'condition_all'},
{ value: 'ANY', textkey: 'condition_any'},
],
});
let strings = load_strings({
studyplan_text: {
studyline_editmode: 'studyline_editmode',
toolbox_toggle: 'toolbox_toggle',
editmode_modules_hidden:'editmode_modules_hidden',
studyline_add: 'studyline_add',
2023-06-26 21:44:31 +02:00
add$core: 'add$core',
edit$core: 'edit$core',
studyline_name: 'studyline_name',
studyline_name_ph: 'studyline_name_ph',
studyline_shortname: 'studyline_shortname',
studyline_shortname_ph: 'studyline_shortname_ph',
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',
},
studyplan_advanced: {
advanced_tools: 'advanced_tools',
confirm_cancel: 'confirm_cancel',
confirm_ok: 'confirm_ok',
2023-06-26 21:44:31 +02:00
success$core: 'success$core',
error$core: 'failed$core',
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',
2023-11-11 20:17:45 +01:00
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',
2023-11-11 20:17:45 +01:00
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',
2023-11-11 20:17:45 +01:00
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",
2023-11-11 20:17:45 +01:00
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",
2023-06-26 21:44:31 +02:00
advanced_cascade_cohortsync_title: "advanced_cascade_cohortsync_title",
advanced_cascade_cohortsync_desc: "advanced_cascade_cohortsync_desc",
advanced_cascade_cohortsync: "advanced_cascade_cohortsync",
2023-11-11 20:17:45 +01:00
currentpage: "currentpage",
},
studyplan_edit: {
studyplan_edit: 'studyplan_edit',
studyplan_add: 'studyplan_add',
studyplanpage_add: 'studyplanpage_add',
studyplanpage_edit: 'studyplanpage_edit',
2023-12-12 23:44:02 +01:00
info_periodsextended: 'studyplanpage_info_periodsextended',
warning: 'warning@core',
},
period_edit: {
edit: 'period_edit',
fullname: 'studyplan_name',
shortname: 'studyplan_shortname',
startdate: 'studyplan_startdate',
enddate: 'studyplan_enddate',
},
course_timing: {
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',
2023-08-04 12:10:43 +02:00
hidewarning: 'course_timing_hidewarning',
periodspan: 'course_period_span',
2023-08-07 11:48:06 +02:00
periods: 'periods',
periodspan_desc: 'course_period_span_desc',
},
studyplan_associate: {
associations: 'associations',
associated_cohorts: 'associated_cohorts',
associated_users: 'associated_users',
associate_cohorts: 'associate_cohorts',
associate_users: 'associate_users',
add_association: 'add_association',
delete_association: 'delete_association',
associations_empty: 'associations_empty',
associations_search: 'associations_search',
cohorts: 'cohorts',
users: 'users',
selected: 'selected',
name: 'name',
context: 'context',
},
item_text: {
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",
},
item_course_text: {
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",
},
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",
},
2023-11-23 07:44:04 +01:00
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",
2023-11-23 07:44:04 +01:00
},
badge: {
share_badge: "share_badge",
dateissued: "dateissued",
dateexpire: "dateexpire",
badgeinfo: "badgeinfo",
},
});
/*
2023-07-07 21:45:09 +02:00
* T-STUDYPLAN-ADVANCED
*/
2023-07-07 21:45:09 +02:00
Vue.component('t-studyplan-advanced', {
props: {
value: {
type: Object,
default(){ return null;},
},
2023-11-11 20:17:45 +01:00
selectedpage: {
type: Object,
default(){ return null;},
}
2023-07-07 21:45:09 +02:00
},
data() {
return {
2023-07-07 21:45:09 +02:00
force_scales: {
selected_scale: null,
result: [],
},
2023-07-07 21:45:09 +02:00
text: strings.studyplan_advanced,
};
},
created() {
},
mounted() {
},
updated() {
},
computed: {
2023-07-07 21:45:09 +02:00
scales(){
return [{
id: null,
disabled: true,
name: this.text.advanced_pick_scale,
}].concat(this.value.advanced.force_scales.scales);
},
},
methods: {
2023-07-07 21:45:09 +02:00
force_scales_start(){
// set confirmation box
const self=this;
2023-07-07 21:45:09 +02:00
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.force_scales.selected_scale,
}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
self.force_scales.result = response;
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-07-07 21:45:09 +02:00
}
});
},
2023-11-11 20:17:45 +01:00
export_page(format){
2023-07-07 21:45:09 +02:00
const self = this;
if(format == undefined || !["json","csv"].includes(format)){
format = "json";
}
call([{
2023-11-11 20:17:45 +01:00
methodname: 'local_treestudyplan_export_page',
2023-07-07 21:45:09 +02:00
args: {
2023-11-11 20:17:45 +01:00
page_id: this.selectedpage.id,
2023-07-07 21:45:09 +02:00
format: format,
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-09-08 12:47:29 +02:00
2023-11-11 20:17:45 +01:00
download(self.value.shortname+".page."+format,response.content,response.format);
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-11-11 20:17:45 +01:00
},
export_plan(){
const self = this;
call([{
methodname: 'local_treestudyplan_export_plan',
args: {
studyplan_id: this.value.id,
format: "json",
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-11-11 20:17:45 +01:00
download(self.value.shortname+".plan.json",response.content,response.format);
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-11-11 20:17:45 +01:00
},
bulk_course_timing() {
const self = this;
call([{
methodname: 'local_treestudyplan_bulk_course_timing',
args: {
page_id: this.selectedpage.id,
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-11-11 20:17:45 +01:00
if(response.success){
// Reloading the webpage saves trouble reloading the specific page updated.
location.reload();
} else {
this.$bvModal.msgBoxOk(response.msg, {title: "Could not set bulk course timing"} );
debug.error("Could not set bulk course timing: ",response.msg);
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
},
2023-07-07 21:45:09 +02:00
import_studylines(){
//const self = this;
upload((filename,content)=>{
call([{
methodname: 'local_treestudyplan_import_studylines',
2023-11-11 20:17:45 +01:00
args: {
page_id: this.selectedpage.id,
content: content,
format: "application/json",
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-11-11 20:17:45 +01:00
if(response.success){
location.reload();
} else {
this.$bvModal.msgBoxOk(response.msg, {title: "Import failed"} );
debug.error("Import failed: ",response.msg);
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-11-11 20:17:45 +01:00
}, "application/json");
},
import_pages(){
//const self = this;
upload((filename,content)=>{
call([{
methodname: 'local_treestudyplan_import_pages',
2023-07-07 21:45:09 +02:00
args: {
studyplan_id: this.value.id,
content: content,
format: "application/json",
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
if(response.success){
location.reload();
} else {
2023-11-11 20:17:45 +01:00
this.$bvModal.msgBoxOk(response.msg, {title: "Import failed"} );
2023-07-07 21:45:09 +02:00
debug.error("Import failed: ",response.msg);
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-07-07 21:45:09 +02:00
}, "application/json");
},
2023-11-11 20:17:45 +01:00
purge_studyplan(){
call([{
2023-07-07 21:45:09 +02:00
methodname: 'local_treestudyplan_delete_studyplan',
args: {
id: this.value.id,
force: true,
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
if(response.success){
location.reload();
} else {
2023-11-11 20:17:45 +01:00
this.$bvModal.msgBoxOk(response.msg, {title: "Could not delete plan "} );
2023-07-07 21:45:09 +02:00
debug.error("Could not delete plan: ",response.msg);
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
},
2023-11-11 20:17:45 +01:00
purge_studyplanpage(){
if (this.selectedpage) {
call([{
methodname: 'local_treestudyplan_delete_studyplanpage',
args: {
id: this.selectedpage.id,
force: true,
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-11-11 20:17:45 +01:00
if(response.success){
location.reload();
} else {
this.$bvModal.msgBoxOk(response.msg, {title: "Could not delete page"} );
debug.error("Could not delete page: ",response.msg);
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-11-11 20:17:45 +01:00
}
},
2023-07-07 21:45:09 +02:00
cascade_cohortsync(){
const self = this;
call([{
methodname: 'local_treestudyplan_cascade_cohortsync',
args: {
studyplan_id: this.value.id,
},
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
self.$bvModal.msgBoxOk(response.success?self.text.success$core:self.text.error$core,
{ title: self.text.advanced_cascade_cohortsync});
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-07-07 21:45:09 +02:00
},
modal_close(){
this.force_scales.result = [];
}
},
2023-09-08 12:47:29 +02:00
template:
`
2023-07-07 21:45:09 +02:00
<span>
2023-09-08 12:47:29 +02:00
<a v-if="value.advanced"
href='#'
@click.prevent=''
class='text-danger'
2023-07-07 21:45:09 +02:00
v-b-modal="'t-studyplan-'+value.id+'-advanced'"
2023-09-02 20:34:33 +02:00
><i class='fa fa-wrench'></i> {{text.advanced_tools}}</a>
2023-09-08 12:47:29 +02:00
<b-modal v-if="value.advanced"
2023-07-07 21:45:09 +02:00
:id="'t-studyplan-'+value.id+'-advanced'"
size="lg"
:title="text.advanced_tools_heading"
ok-only
@hide="modal_close"
2023-11-11 20:17:45 +01:00
body-class="p-0"
>
2023-07-07 21:45:09 +02:00
<b-tabs card>
<b-tab :title="text.advanced_warning_title" active>
{{ text.advanced_warning}}
</b-tab>
<b-tab :title="text.advanced_course_manipulation_title" >
2023-11-11 20:17:45 +01:00
<h3>{{ text.advanced_cascade_cohortsync_title}}</h3>
<p>{{ text.advanced_cascade_cohortsync_desc}}</p>
<p class="mt-2"><b-button
2023-07-07 21:45:09 +02:00
variant="info"
@click="cascade_cohortsync"
2023-11-11 20:17:45 +01:00
>{{ 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="bulk_course_timing"
>{{ text.advanced_bulk_course_timing}}</b-button></p>
<template v-if="['bistate','tristate'].includes(value.aggregation)">
<h3>{{ text.advanced_force_scale_title}}</h3>
2023-07-07 21:45:09 +02:00
{{ text.advanced_force_scale_desc}}
2023-11-11 20:17:45 +01:00
<p class="mt-2"><b-form-select v-model="force_scales.selected_scale"
2023-07-07 21:45:09 +02:00
:options="scales" text-field="name" value-field="id"
2023-11-11 20:17:45 +01:00
></b-form-select>
<b-button
variant="danger"
:disabled="force_scales.selected_scale == null"
@click="force_scales_start"
>{{ text.advanced_force_scale_button}}</b-button>
</p>
<p class="mt-2">
2023-07-07 21:45:09 +02:00
<ul class='t-advanced-scrollable' v-if="force_scales.result.length > 0">
<li v-for="c in force_scales.result">
<span class='t-advanced-coursename'>{{c.course.fullname}}</span>
<ul v-if="c.grades.length > 0">
2023-11-11 20:17:45 +01:00
<li v-for='g in c.grades'
><span class='t-advanced-gradename'><span v-html="g.name"></span></span>
2023-07-07 21:45:09 +02:00
<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
2023-11-11 20:17:45 +01:00
><span v-else class='t-advanced-status skipped'
2023-07-07 21:45:09 +02:00
>{{text.advanced_error}}</span
2023-11-11 20:17:45 +01:00
></li>
2023-07-07 21:45:09 +02:00
</ul>
</li>
</ul>
2023-11-11 20:17:45 +01:00
</p>
</template>
2023-07-07 21:45:09 +02:00
</b-tab>
2023-11-11 20:17:45 +01:00
<b-tab :title='text.advanced_backup_restore'>
<h3>{{ text.advanced_backup }}</h3>
<p><b-button
variant="primary"
@click="export_page('json')"
>{{ text.advanced_backup_page }}</b-button>
{{text.currentpage}} <i>{{selectedpage.fullname}}</i></p>
<p><b-button
2023-07-07 21:45:09 +02:00
variant="primary"
2023-11-11 20:17:45 +01:00
@click="export_plan('json')"
>{{ text.advanced_backup_plan }}</b-button></p>
<h3>{{ text.advanced_restore }}</h3>
<p><b-button
variant="danger"
@click="import_studylines"
>{{ text.advanced_restore_lines}}</b-button></p>
<p><b-button
variant="danger"
@click="import_pages"
>{{ text.advanced_restore_pages }}</b-button></p>
<h3>{{ text.advanced_export }}</h3>
<p><b-button
2023-07-07 21:45:09 +02:00
variant="primary"
2023-11-11 20:17:45 +01:00
@click="export_page('csv')"
>{{ text.advanced_export_csv_page }}</b-button>
{{text.currentpage}} <i>{{selectedpage.fullname}}</i></p>
</b-tab>
2023-07-07 21:45:09 +02:00
<b-tab :title='text.advanced_purge'>
2023-11-11 20:17:45 +01:00
<p>{{text.advanced_purge_page_expl}}</p>
<p>{{text.currentpage}} <i>{{selectedpage.fullname}}</i></p>
2023-07-07 21:45:09 +02:00
<p><b-button
2023-11-11 20:17:45 +01:00
variant="danger"
@click="purge_studyplanpage"
>{{ text.advanced_purge_page}}</b-button></p>
<p>{{text.advanced_purge_plan_expl}}</p>
<p><b-button
variant="danger"
@click="purge_studyplan"
>{{ text.advanced_purge_plan}}</b-button></p>
</b-tab>
</b-tabs>
2023-09-08 12:47:29 +02:00
</b-modal>
</span>
`
});
2023-07-07 21:45:09 +02:00
/*
2023-07-07 21:45:09 +02:00
* T-STUDYPLAN-EDIT
*/
2023-07-07 21:45:09 +02:00
Vue.component('t-studyplan-edit', {
props: {
2023-07-07 21:45:09 +02:00
'value' :{
type: Object,
default(){ return null;},
},
2023-07-07 21:45:09 +02:00
'mode' :{
type: String,
default() { return "edit";},
},
'type' :{
type: String,
default() { return "link";},
},
'variant' : {
type: String,
default() { return "";},
2023-08-07 17:03:49 +02:00
},
'contextid': {
type: Number,
default: 1
},
},
data() {
return {
2023-07-07 21:45:09 +02:00
text: strings.studyplan_edit,
};
},
computed: {
},
methods: {
planSaved(updatedplan){
const self = this;
debug.info("Got new plan data",updatedplan);
2023-07-07 21:45:09 +02:00
if(self.mode == 'create'){
// Inform parent of the details of the newly created plan
self.$emit("created",updatedplan);
2023-07-07 21:45:09 +02:00
}
else {
// determine if the plan moved context...
const moved_from = self.value.context_id;
const moved_to = updatedplan.context_id;
const moved = (moved_from != moved_to);
2023-07-07 21:45:09 +02:00
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}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
self.value = ProcessStudyplan(response,true);
debug.info('studyplan processed');
2023-08-16 23:36:11 +02:00
self.$emit('input',self.value);
2023-12-13 23:49:06 +01:00
}).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, moved_from, moved_to);
2023-07-07 21:45:09 +02:00
}
}
}
2023-09-09 20:53:39 +02:00
},
2023-07-07 21:45:09 +02:00
}
,
2023-09-08 12:47:29 +02:00
template:
`
2023-07-07 21:45:09 +02:00
<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.studyplan_edit"
><slot><i class='fa fa-gear'></i></slot></mform>
2023-09-08 12:47:29 +02:00
</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.studyplan_edit,
};
},
computed: {
},
methods: {
planSaved(updatedpage){
const self = this;
debug.info("Got new page data",updatedpage);
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');
2023-12-12 23:44:02 +01:00
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);
2023-12-12 23:44:02 +01:00
}
},
}
,
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>
`
});
/*
2023-07-07 21:45:09 +02:00
* T-STUDYPLAN-ASSOCIATE
*/
2023-07-07 21:45:09 +02:00
Vue.component('t-studyplan-associate', {
props: ['value',],
data() {
return {
show: false,
config: {
2023-09-08 12:47:29 +02:00
userfields: [
{ key: "selected",},
{ key: "firstname", "sortable": true,},
{ key: "lastname", "sortable": true,},
],
2023-09-08 12:47:29 +02:00
cohortfields:[
{ key: "selected",},
{ key: "name", "sortable": true,},
{ key: "context", "sortable": true,},
]
},
2023-07-07 21:45:09 +02:00
association: {
cohorts: [],
users: [],
},
2023-07-07 21:45:09 +02:00
loading: {
cohorts: false,
users: false,
},
2023-07-07 21:45:09 +02:00
search: {users: [], cohorts:[]},
selected: {
2023-09-08 12:47:29 +02:00
search: {users: [] , cohorts:[]},
2023-07-07 21:45:09 +02:00
associated: {users: [] , cohorts:[]}
2023-09-08 12:47:29 +02:00
},
2023-07-07 21:45:09 +02:00
text: strings.studyplan_associate,
};
},
created() {
2023-09-08 12:47:29 +02:00
2023-07-07 21:45:09 +02:00
},
mounted() {
},
updated() {
},
methods: {
showModal(){
this.show = true;
this.loadAssociations();
},
cohortOptionModel(c){
return {
2023-09-02 20:34:33 +02:00
value: c.id,
2023-07-07 21:45:09 +02:00
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,}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
self.association.users = response.map(self.userOptionModel);
self.loading.users = false;
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-07-07 21:45:09 +02:00
call([{
methodname: 'local_treestudyplan_associated_cohorts',
args: { studyplan_id: self.value.id,}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
self.association.cohorts = response.map(self.cohortOptionModel);
self.loading.cohorts = false;
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-09-08 12:47:29 +02:00
},
2023-07-07 21:45:09 +02:00
searchCohorts(searchtext){
const self = this;
if(searchtext.length > 0)
{
call([{
methodname: 'local_treestudyplan_list_cohort',
args: { like: searchtext, exclude_id: self.value.id}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
self.search.cohorts = response.map(self.cohortOptionModel);
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-07-07 21:45:09 +02:00
}
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){
2023-09-08 12:47:29 +02:00
const r = searchselected[i];
2023-07-07 21:45:09 +02:00
requests.push({
methodname: 'local_treestudyplan_connect_cohort',
args: {studyplan_id: self.value.id, cohort_id: r},
fail: notification.exception,
done: function(response){
if(response.success){
transportItem(associated,search,r);
}
}
2023-07-07 21:45:09 +02:00
});
}
2023-07-07 21:45:09 +02:00
call(requests);
},
cohortDisassociate(){
const self = this;
let requests = [];
const associatedselected = self.selected.associated.cohorts;
const associated = self.association.cohorts;
const search = self.search.cohorts;
for(const i in associatedselected){
2023-09-08 12:47:29 +02:00
const r = associatedselected[i];
2023-07-07 21:45:09 +02:00
requests.push({
methodname: 'local_treestudyplan_disconnect_cohort',
args: {studyplan_id: self.value.id, cohort_id: r},
fail: notification.exception,
done: function(response){
if(response.success){
transportItem(search,associated,r);
}
}
});
}
2023-07-07 21:45:09 +02:00
call(requests);
2023-09-08 12:47:29 +02:00
},
2023-07-07 21:45:09 +02:00
searchUsers(searchtext){
const self = this;
if(searchtext.length > 0)
{
call([{
methodname: 'local_treestudyplan_find_user',
args: { like: searchtext, exclude_id: self.value.id}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
self.search.users = response.map(self.userOptionModel);
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
}
2023-07-07 21:45:09 +02:00
else {
self.search.users = [];
}
},
2023-07-07 21:45:09 +02:00
userAssociate(){
const self = this;
2023-07-07 21:45:09 +02:00
let requests = [];
const associated = self.association.users;
const search = self.search.users;
const searchselected = self.selected.search.users;
for(const i in searchselected){
2023-09-08 12:47:29 +02:00
const r = searchselected[i];
2023-07-07 21:45:09 +02:00
requests.push({
methodname: 'local_treestudyplan_connect_user',
args: {studyplan_id: self.value.id, user_id: r},
fail: notification.exception,
done: function(response){
if(response.success){
transportItem(associated,search,r);
}
}
});
}
2023-07-07 21:45:09 +02:00
call(requests);
},
userDisassociate(){
const self = this;
let requests = [];
const associated = self.association.users;
const associatedselected = self.selected.associated.users;
const search = self.search.users;
for(const i in associatedselected){
2023-09-08 12:47:29 +02:00
const r = associatedselected[i];
2023-07-07 21:45:09 +02:00
requests.push({
methodname: 'local_treestudyplan_disconnect_user',
args: {studyplan_id: self.value.id, user_id: r},
fail: notification.exception,
done: function(response){
if(response.success){
transportItem(search,associated,r);
}
}
2023-07-07 21:45:09 +02:00
});
}
call(requests);
},
}
,
2023-09-08 12:47:29 +02:00
template:
2023-07-07 21:45:09 +02:00
`
<span class='s-studyplan-associate'
><a href='#' @click.prevent="showModal" ><slot><i class='fa fa-users'></i></slot></a>
<b-modal
2023-09-08 12:47:29 +02:00
v-model="show"
size="lg"
2023-07-07 21:45:09 +02:00
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>
2023-09-08 12:47:29 +02:00
<b-form-input
type="text" @input="searchCohorts($event)"
2023-07-07 21:45:09 +02:00
: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>
2023-07-07 21:45:09 +02:00
</b-col>
</b-row>
<b-row class='mt-2'>
<b-col>
<b-button variant='danger' @click="cohortDisassociate()"
><i class='fa fa-chain-broken'></i>&nbsp;{{text.delete_association}}</b-button>
</b-col>
<b-col>
<b-button variant='success' @click="cohortAssociate()"
2023-09-03 15:21:30 +02:00
><i class='fa fa-link'></i>&nbsp;{{text.add_association}}</b-button>
2023-07-07 21:45:09 +02:00
</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>
2023-09-08 12:47:29 +02:00
<b-form-input
type="text"
@input="searchUsers($event)"
2023-07-07 21:45:09 +02:00
placeholder="Search 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>
2023-07-07 21:45:09 +02:00
</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="userDisassociate()"
><i class='fa fa-chain-broken'></i>&nbsp;{{text.delete_association}}</b-button>
</b-col>
<b-col>
<b-button variant='success' @click="userAssociate()"
2023-09-03 15:21:30 +02:00
><i class='fa fa-link'></i>&nbsp;{{text.add_association}}</b-button>
2023-07-07 21:45:09 +02:00
</b-col>
2023-09-08 12:47:29 +02:00
</b-row>
2023-09-03 15:21:30 +02:00
</b-container>
2023-07-07 21:45:09 +02:00
</b-tab>
</b-tabs>
</b-modal>
2023-09-03 15:21:30 +02:00
</span>
2023-07-07 21:45:09 +02:00
`
});
/*******************
2023-09-08 12:47:29 +02:00
*
* Period editor
2023-09-08 12:47:29 +02:00
*
*************/
Vue.component('t-period-edit', {
props: {
'value' :{
type: Object,
default(){ return null;},
},
'type' :{
type: String,
default() { return "link";},
},
'variant' : {
type: String,
default() { return "";},
2023-09-03 15:21:30 +02:00
},
'minstart' : {
type: String,
default() { return null;},
},
'maxend' : {
type: String,
default() { return null;},
}
},
data() {
return {
show: false,
2023-09-08 12:47:29 +02:00
editdata: {
fullname: '',
shortname: '',
startdate: (new Date()).getFullYear() + '-08-01',
enddate: ((new Date()).getFullYear()+1) + '-08-01',
},
text: strings.period_edit,
};
},
created() {
},
mounted() {
},
updated() {
},
computed: {
},
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([{
2023-09-03 15:21:30 +02:00
methodname: 'local_treestudyplan_edit_period',
args: args
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
objCopy(self.value,response,PERIOD_EDITOR_FIELDS);
self.$emit('input',self.value);
2023-09-03 15:21:30 +02:00
self.$emit('edited',self.value);
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
},
2023-09-03 15:21:30 +02:00
refresh(){
const self = this;
call([{
methodname: 'local_treestudyplan_get_period',
args: { 'id': this.value.id },
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-09-03 15:21:30 +02:00
objCopy(self.value,response,PERIOD_EDITOR_FIELDS);
self.$emit('input',self.value);
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-09-03 15:21:30 +02:00
},
add_day(date,days) {
if( days === undefined ){
days = 1;
}
return add_days(date,days);
},
sub_day(date,days) {
if( days === undefined ){
days = 1;
}
return add_days(date,0 - days);
},
}
,
2023-09-08 12:47:29 +02:00
template:
`
<span class='t-period-edit'>
<b-button :variant="variant" v-if='type == "button"' @click.prevent='editStart()'
2023-09-02 20:34:33 +02:00
><slot><i class='fa fa-gear'></i></slot></b-button>
<a variant="variant" v-else href='#' @click.prevent='editStart()'
2023-09-02 20:34:33 +02:00
><slot><i class='fa fa-gear'></i></slot></a>
2023-09-08 12:47:29 +02:00
<b-modal
v-model="show"
size="lg"
ok-variant="primary"
2023-08-07 16:11:14 +02:00
: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">
2023-09-08 12:47:29 +02:00
<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">
2023-09-08 12:47:29 +02:00
<b-form-input v-model="editdata.shortname"
:state='editdata.shortname.length>0'
></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_startdate}}</b-col>
<b-col cols="8">
2023-09-03 15:21:30 +02:00
<b-form-datepicker
2023-09-08 12:47:29 +02:00
start-weekday="1"
2023-09-03 15:21:30 +02:00
v-model="editdata.startdate"
:min="(minstart ? minstart : '')"
:max="sub_day(value.enddate)"
2023-09-03 15:21:30 +02:00
></b-form-datepicker>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_enddate}}</b-col>
<b-col cols="8">
2023-09-03 15:21:30 +02:00
<b-form-datepicker
2023-09-08 12:47:29 +02:00
start-weekday="1"
2023-09-03 15:21:30 +02:00
v-model="editdata.enddate"
:min="add_day(value.startdate)"
2023-09-03 15:21:30 +02:00
:max="(maxend ? maxend : '')"
></b-form-datepicker>
</b-col>
</b-row>
</b-container>
2023-09-08 12:47:29 +02:00
</b-modal>
</span>
`
});
// TAG: Start studyplan component
/*
2023-07-07 21:45:09 +02:00
* T-STUDYPLAN
*/
2023-07-07 21:45:09 +02:00
Vue.component('t-studyplan', {
props: [ 'value', 'index', 'initeditmode'],
data() {
return {
config: {
2023-09-08 12:47:29 +02:00
userfields: [
{ key: "selected",},
{ key: "firstname", "sortable": true,},
{ key: "lastname", "sortable": true,},
],
2023-09-08 12:47:29 +02:00
cohortfields:[
{ key: "selected",},
{ key: "name", "sortable": true,},
{ key: "context", "sortable": true,},
]
},
2023-07-07 21:45:09 +02:00
create: {
studyline: {
'name': '',
'shortname': '',
'color': '#DDDDDD',
},
page: {
'id': -1,
'name' : '',
'shortname' : ''
}
},
2023-07-07 21:45:09 +02:00
edit: {
toolbox_shown: false,
2023-07-07 21:45:09 +02:00
studyline: {
editmode: false,
2023-09-08 12:47:29 +02:00
data: {
2023-07-07 21:45:09 +02:00
name: '',
shortname: '',
color: '#DDDDDD',
},
original: {},
},
studyplan: {
2023-09-08 12:47:29 +02:00
data: {
2023-07-07 21:45:09 +02:00
name: '',
shortname: '',
description: '',
slots : 4,
startdate: '2020-08-01',
enddate: '',
aggregation: '',
aggregation_config: '',
aggregation_info: {
useRequiredGrades: true,
useItemCondition: false,
},
},
original: {},
}
},
2023-07-07 21:45:09 +02:00
text: strings.studyplan_text,
2023-08-07 16:11:14 +02:00
cache: {
linelayers: {},
},
selectedpageindex: 0,
emptyline: {
id: -1,
name: '<No study lines defined>',
shortname: '<No study lines>',
color: '#FF0000',
filterslots: [{}],
courseslots: [{}]
}
};
},
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('editModeSet', this.onMoodleEditModeChanged);
ItemEventBus.$on('coursechange', () => {
self.$emit('pagechanged',this.selectedpage);
});
},
mounted() {
/*if(this.value.pages[0].studylines.length == 0){
2023-11-11 20:17:45 +01:00
// start in editmode if studylines on first page are empty
2023-07-07 21:45:09 +02:00
this.edit.studyline.editmode = true;
}*/
this.edit.studyline.editmode = this.initeditmode;
2023-07-07 21:45:09 +02:00
this.$root.$emit('redrawLines');
this.$emit('pagechanged',this.selectedpage);
},
updated() {
2023-07-07 21:45:09 +02:00
this.$root.$emit('redrawLines');
ItemEventBus.$emit('redrawLines');
},
computed: {
selectedpage() {
return this.value.pages[this.selectedpageindex];
}
},
methods: {
onMoodleEditModeChanged(mode){
this.edit.studyline.editmode = mode;
},
columns(page) {
return 1+ (page.periods * 2);
},
columns_stylerule(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+";";
},
trashbin_accepts(type){
if(type.item){
return true;
} else {
return false;
}
},
countLineLayers(line,page){
2023-08-07 16:11:14 +02:00
// 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.....
2023-09-08 12:47:29 +02:00
if( this.cache.linelayers[line.id]
2023-08-07 16:11:14 +02:00
&& ((new Date()) - this.cache.linelayers[line.id].timestamp < 1000 )
){
2023-09-08 12:47:29 +02:00
return this.cache.linelayers[line.id].value;
2023-08-07 16:11:14 +02:00
}
2023-09-08 12:47:29 +02:00
else
2023-08-07 16:11:14 +02:00
{
let maxLayer = -1;
for(let i = 0; i <= page.periods; i++){
2023-08-16 23:36:11 +02:00
if(line.slots[i]){
const slot = line.slots[i];
// Determine the amount of used layers in a studyline slot
2023-08-28 11:26:14 +02:00
for(const ix in line.slots[i].courses){
const item = line.slots[i].courses[ix];
2023-08-16 23:36:11 +02:00
if(item.layer > maxLayer){
maxLayer = item.layer;
}
2023-08-07 16:11:14 +02:00
}
2023-08-16 23:36:11 +02:00
for(const ix in line.slots[i].filters){
const item = line.slots[i].filters[ix];
if(item.layer > maxLayer){
maxLayer = item.layer;
}
2023-08-07 16:11:14 +02:00
}
2023-08-16 23:36:11 +02:00
2023-07-15 22:00:17 +02:00
}
}
2023-08-07 16:11:14 +02:00
this.cache.linelayers[line.id] = {
value: (maxLayer + 1),
2023-09-08 12:47:29 +02:00
timestamp: (new Date()),
2023-08-07 16:11:14 +02:00
};
return maxLayer+1;
2023-07-15 22:00:17 +02:00
}
},
2023-07-07 21:45:09 +02:00
slotsempty(slots) {
if(Array.isArray(slots)){
let count = 0;
for(let i = 0; i < slots.length; i++) {
2023-08-28 11:26:14 +02:00
if(Array.isArray(slots[i].courses)){
count += slots[i].courses.length;
2023-07-07 21:45:09 +02:00
}
if(Array.isArray(slots[i].filters)){
count += slots[i].filters.length;
}
}
return (count == 0);
} else {
return false;
}
},
2023-07-07 21:45:09 +02:00
movedStudyplan(plan,from,to) {
this.$emit('moved',plan,from,to); // Throw the event up....
},
addStudyLine(page,newlineinfo) {
call([{
2023-07-07 21:45:09 +02:00
methodname: 'local_treestudyplan_add_studyline',
args: {
'page_id': page.id,
2023-07-07 21:45:09 +02:00
'name': newlineinfo.name,
'shortname': newlineinfo.shortname,
'color': newlineinfo.color,
'sequence': page.studylines.length,
2023-07-07 21:45:09 +02:00
}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
page.studylines.push(response);
2023-07-07 21:45:09 +02:00
newlineinfo.name = '';
newlineinfo.shortname = '';
newlineinfo.color = "#dddddd";
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-07-07 21:45:09 +02:00
},
editLineStart(line) {
Object.assign(this.edit.studyline.data,line);
this.edit.studyline.original = line;
this.$bvModal.show('modal-edit-studyline-'+this.value.id);
},
editLineFinish() {
let editedline = this.edit.studyline.data;
let originalline = this.edit.studyline.original;
call([{
2023-07-07 21:45:09 +02:00
methodname: 'local_treestudyplan_edit_studyline',
args: { 'id': editedline.id,
'name': editedline.name,
'shortname': editedline.shortname,
'color': editedline.color,}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
originalline['name'] = response['name'];
originalline['shortname'] = response['shortname'];
originalline['color'] = response['color'];
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
},
deleteLine(page,line) {
2023-07-07 21:45:09 +02:00
const self=this;
get_strings([
{key: 'studyline_confirm_remove', param: line.name, component: 'local_treestudyplan' },
{key: 'delete', component: 'core' },
]).then(function(s){
self.$bvModal.msgBoxConfirm(s[0], {
okTitle: s[1],
okVariant: 'danger',
}).then(function(modalresponse){
if(modalresponse){
call([{
methodname: 'local_treestudyplan_delete_studyline',
args: { 'id': line.id, }
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
if(response.success == true){
let index = page.studylines.indexOf(line);
page.studylines.splice(index, 1);
2023-07-07 21:45:09 +02:00
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
}
});
2023-07-07 21:45:09 +02:00
});
},
2023-07-07 21:45:09 +02:00
reorderLines(event,lines){
// apply reordering
event.apply(lines);
// send the new sequence to the server
let sequence = [];
for(let idx in lines)
{
2023-07-07 21:45:09 +02:00
sequence.push({'id': lines[idx].id,'sequence': idx});
}
2023-07-07 21:45:09 +02:00
call([{
methodname: 'local_treestudyplan_reorder_studylines',
args: { 'sequence': sequence }
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
}).catch(notification.exception);
},
2023-07-07 21:45:09 +02:00
deletePlan(studyplan){
const self=this;
get_strings([
{key: 'studyplan_confirm_remove', param: studyplan.name, component: 'local_treestudyplan' },
{key: 'delete', component: 'core' },
]).then(function(s){
self.$bvModal.msgBoxConfirm(s[0], {
okTitle: s[1],
okVariant: 'danger',
}).then(function(modalresponse){
if(modalresponse){
call([{
methodname: 'local_treestudyplan_delete_studyplan',
2023-08-16 23:15:48 +02:00
args: { 'id': studyplan.id, force: true}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
if(response.success == true){
self.$root.$emit("studyplanRemoved",studyplan);
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
}
});
2023-07-07 21:45:09 +02:00
});
},
2023-07-07 21:45:09 +02:00
deleteStudyItem(event){
//const self = this;
let item = event.data;
call([{
methodname: 'local_treestudyplan_delete_studyitem',
args: { 'id': item.id, }
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
2023-07-07 21:45:09 +02:00
if(response.success == true){
event.source.$emit('cut',event);
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-07-07 21:45:09 +02:00
},
showslot(page,line,index, layeridx, type){
2023-08-04 11:54:16 +02:00
// 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;
2023-08-04 11:54:16 +02:00
let show = true;
for(let i = 0; i < periods; i++){
2023-08-28 11:26:14 +02:00
if(line.slots[index-i] && line.slots[index-i].courses){
const list = line.slots[index-i].courses;
2023-08-04 12:10:43 +02:00
for(const ix in list){ // Really wish that 'for of' would work with the minifier moodle uses
2023-08-04 11:54:16 +02:00
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;
}
}
}
}
}
}
2023-09-08 12:47:29 +02:00
2023-08-04 11:54:16 +02:00
return show;
2023-09-03 15:21:30 +02:00
},
periodEdited(pi) {
const prev = this.$refs["periodeditor-" + (pi.period - 1)][0];
if(prev) {
prev.refresh();
}
const next = this.$refs["periodeditor-" + (pi.period + 1)][0];
if(next) {
next.refresh();
}
},
add_day(date,days) {
if( days === undefined ){
days = 1;
}
return add_days(date,days);
},
sub_day(date,days) {
if( days === undefined ){
days = 1;
}
return add_days(date,0 - days);
},
toolbox_switched(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);
}
}
,
2023-09-08 12:47:29 +02:00
template:
2023-07-07 21:45:09 +02:00
`
<div>
<div class='controlbox t-studyplan-controlbox'>
<div class="controlbox-group">
<b-form-checkbox v-model="edit.studyline.editmode" class="sw-studyplan-toolbar" switch
@change="toolbox_switched(edit.toolbox_shown && !edit.studyline.editmode); "
>{{ text.studyline_editmode }}</b-form-checkbox>
<b-form-checkbox v-if="!edit.studyline.editmode" v-model="edit.toolbox_shown" class="sw-studyplan-toolbar" switch
@change="toolbox_switched"
>{{ text.toolbox_toggle}}</b-form-checkbox>
2023-09-08 12:47:29 +02:00
<drop
mode='copy'
class='t-item-deletebox text-danger border-danger'
@drop='deleteStudyItem'
:accepts-type="trashbin_accepts"
><i class='fa fa-trash'></i>
</drop>
</div>
<div class="controlbox-group">
<span class='control editable'>
2023-11-11 20:17:45 +01:00
<t-studyplan-advanced v-model="value" :selectedpage="selectedpage"></t-studyplan-advanced>
</span>
<span class='control editable'>
2023-09-08 12:47:29 +02:00
<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"
2023-09-08 12:47:29 +02:00
><i class='fa fa-gear'></i> {{text.edit$core}}</t-studyplan-edit>
</span>
<span class='control deletable'>
<a v-if='value.pages.length == 0' href='#' @click='deletePlan(value)'
><i class='text-danger fa fa-trash'></i></a>
</span>
</div>
2023-07-07 21:45:09 +02:00
</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
:studyplan="value"
v-model="create.page"
type="link"
mode="create"
@created="pagecreated"
><i class='fa fa-plus'></i></t-studyplan-page-edit>
2023-07-07 21:45:09 +02:00
</template>
<b-tab
v-for="(page,pageindex) in value.pages"
:key="page.id"
>
<template #title>
{{page.shortname}}
<t-studyplan-page-edit
v-if="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"
xreorder="$event.apply(page.studylines)"
@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-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="columns_stylerule(page)">
<!-- add period information -->
<template v-for="(n,index) in (page.periods+1)">
<s-studyline-header-period
:identifier='Number(page.id)'
v-if="index > 0"
v-model="page.perioddesc[index-1]"
><t-period-edit
:ref="'periodeditor-'+index"
@edited="periodEdited"
v-model="page.perioddesc[index-1]"
:minstart="(index > 1) ? add_day(page.perioddesc[index-2].startdate,2) : null"
:maxend="(index < page.periods) ? sub_day(page.perioddesc[index].enddate,2) : null"
></t-period-edit
></s-studyline-header-period>
<div class="s-studyline-header-filter"></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')"
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'
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
></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="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$core"
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-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-container>
</b-modal>
</b-tab>
</b-tabs>
</b-card>
2023-07-07 21:45:09 +02:00
</div>
`
});
2023-07-07 21:45:09 +02:00
/*
* T-STUDYLINE-HEADER
*/
Vue.component('t-studyline-heading', {
props: {
value : {
type: Object, // Studyline
default: function(){ return {};},
},
2023-07-18 13:19:48 +02:00
layers: {
type: Number,
default: 1,
},
},
2023-07-07 21:45:09 +02:00
data() {
return {
2023-07-18 13:19:48 +02:00
layerHeights: {}
2023-07-07 21:45:09 +02:00
};
},
2023-07-10 23:11:23 +02:00
created() {
2023-07-28 00:04:24 +02:00
// 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);
2023-07-10 23:11:23 +02:00
},
2023-07-07 21:45:09 +02:00
computed: {
2023-09-08 12:47:29 +02:00
2023-07-07 21:45:09 +02:00
},
methods: {
2023-07-18 13:19:48 +02:00
onLineHeightChange(lineid,layerid,newheight){
// All layers for this line have the first slot send an update message on layer height change.
2023-09-08 12:47:29 +02:00
// When one of those updates is received, record the height and recalculate the total height of the
2023-07-18 13:19:48 +02:00
// header
2023-07-28 00:04:24 +02:00
if(this.$refs.main && lineid == this.value.id){
2023-07-18 13:19:48 +02:00
const items = document.querySelectorAll(
2023-07-18 13:40:30 +02:00
`.t-studyline-slot-0[data-studyline='${this.value.id}']`);
2023-09-08 12:47:29 +02:00
2023-07-18 13:40:30 +02:00
// determine the height of all the lines and add them up.
2023-07-18 13:19:48 +02:00
let heightSum = 0;
2023-07-18 13:40:30 +02:00
items.forEach((el) => {
// getBoundingClientRect() Gets the actual fractional height instead of rounded to integer pixels
const r = el.getBoundingClientRect();
const height = r.height;
2023-07-18 13:19:48 +02:00
heightSum += height;
2023-07-18 13:40:30 +02:00
});
2023-07-18 13:19:48 +02:00
const heightStyle=`${heightSum}px`;
2023-07-28 00:04:24 +02:00
this.$refs.main.style.height = heightStyle;
2023-07-10 23:11:23 +02:00
}
}
2023-07-07 21:45:09 +02:00
},
template: `
2023-09-08 12:47:29 +02:00
<div class="t-studyline t-studyline-heading "
2023-07-28 00:04:24 +02:00
:data-studyline="value.id" ref="main"
><div class="t-studyline-handle" :style="'background-color: ' + value.color"></div>
2023-07-07 21:45:09 +02:00
<div class="t-studyline-title">
2023-09-02 20:34:33 +02:00
<abbr v-b-tooltip.hover.right :title="value.name">{{ value.shortname }}</abbr>
2023-07-07 21:45:09 +02:00
</div>
</div>
`,
});
2023-09-08 12:47:29 +02:00
/*
2023-07-07 21:45:09 +02:00
* T-STUDYLINE (Used only for study line edit mode)
*/
Vue.component('t-studyline-edit', {
props: {
value : {
type: Object, // Studyline
default: function(){ 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++) {
2023-08-28 11:26:14 +02:00
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: `
2023-09-08 12:47:29 +02:00
<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>
2023-07-26 23:24:34 +02:00
<div class='controlbox'>
<template v-if='editable || deletable'>
<span class='control editable' v-if='editable'>
<a href='#' @click='onEdit'><i class='fa fa-pencil'></i></a>
</span>
<span class='control deletable' v-if='deletable'>
<a v-if='deletable' href='#' @click='onDelete'><i class='text-danger fa fa-trash'></i></a>
</span>
2023-07-26 23:24:34 +02:00
</template>
</div>
</div>
`,
});
2023-07-15 22:00:17 +02:00
/*
* During a redisign it was decided to have the studyline still get the entire array as a value,
2023-09-08 12:47:29 +02:00
* 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
2023-07-15 22:00:17 +02:00
*/
Vue.component('t-studyline-slot', {
props: {
type : {
type: String,
default: 'gradable',
},
slotindex : {
type: Number,
default: '',
},
line : {
type: Object,
default(){ return null;},
},
2023-07-15 22:00:17 +02:00
layer : {
type: Number,
},
value: {
2023-07-15 22:00:17 +02:00
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;},
2023-09-08 12:47:29 +02:00
},
},
2023-07-10 23:11:23 +02:00
mounted() {
const self=this;
if(self.type == "gradable" && self.slotindex == 1){
self.resizeListener = new ResizeObserver(() => {
2023-07-28 00:04:24 +02:00
if(self.$refs.main){
const size = self.$refs.main.getBoundingClientRect();
2023-09-08 12:47:29 +02:00
2023-07-28 00:04:24 +02:00
ItemEventBus.$emit('lineHeightChange', self.line.id, self.layer, size.height);
2023-07-18 13:19:48 +02:00
}
2023-07-28 00:04:24 +02:00
}).observe(self.$refs.main);
2023-07-10 23:11:23 +02:00
}
},
unmounted() {
if(this.resizeListener) {
this.resizeListener.disconnect();
}
},
computed: {
slotkey(){
return `${this.type}'-'${this.line.id}-${this.slotindex}-${this.layer}`;
},
2023-08-07 20:27:36 +02:00
itemidx(){
for(const ix in this.value){
const itm = this.value[ix];
if(itm.layer == this.layer){
return ix;
}
}
return null;
},
2023-07-15 22:00:17 +02:00
item(){
for(const ix in this.value){
const itm = this.value[ix];
2023-07-15 22:00:17 +02:00
if(itm.layer == this.layer){
return itm;
}
}
return null;
},
listtype() {
return this.type;
},
dragacceptlist(){
if(this.type == "gradable"){
2023-07-15 22:00:17 +02:00
return ["course", "gradable-item"];
} else {
return ["filter", "filter-item"];
}
},
2023-07-15 22:00:17 +02:00
courseHoverDummy(){
return {course: this.hover.component};
2023-08-03 18:44:57 +02:00
},
2023-08-04 11:54:16 +02:00
spanCss(){
if(this.item && this.item.span > 1){
const span = (2 * this.item.span) - 1;
return `width: 100%; grid-column: span ${span};`;
} else {
return "";
}
2023-07-15 22:00:17 +02:00
}
},
data() {
return {
text: strings.course_timing,
2023-07-10 23:11:23 +02:00
resizeListener: null,
2023-09-08 12:47:29 +02:00
hover: {
2023-07-15 22:00:17 +02:00
component:null,
type: null,
},
datechanger: {
coursespan: null,
periodspan: null,
default: false,
defaultchoice: false,
hidewarn: false,
2023-07-15 22:00:17 +02:00
}
};
},
methods: {
2023-07-15 22:00:17 +02:00
onDrop(event) {
this.hover.component = null;
this.hover.type = null;
2023-08-15 15:34:53 +02:00
debug.info(event);
const self = this;
2023-08-04 11:54:16 +02:00
if(event.type.item) {
let item = event.data;
2023-09-08 12:47:29 +02:00
2023-08-04 11:54:16 +02:00
// 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).done(()=>{
if(this.$refs.timingChecker){
this.$refs.timingChecker.validate_course_period();
}
2023-08-04 11:54:16 +02:00
});
}
2023-08-04 11:54:16 +02:00
else if(event.type.component){
2023-08-15 15:34:53 +02:00
debug.info("Adding new component");
if(event.type.type == "gradable"){
call([{
methodname: 'local_treestudyplan_add_studyitem',
2023-09-08 12:47:29 +02:00
args: {
"line_id": self.line.id,
"slot" : self.slotindex,
2023-07-15 22:00:17 +02:00
"layer" : self.layer,
"type": 'course',
"details": {
"competency_id": null,
'conditions':'',
'course_id':event.data.id,
'badge_id':null,
'continuation_id':null,
}
}
2023-12-13 23:49:06 +01:00
}])[0].then((response) => {
let item = response;
2023-07-15 22:00:17 +02:00
self.relocateStudyItem(item).done(()=>{
self.value.push(item);
self.$emit("input",self.value);
2023-08-04 11:54:16 +02:00
// 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.validate_course_period();
}
});
ItemEventBus.$emit('coursechange');
});
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-09-08 12:47:29 +02:00
}
2023-08-04 11:54:16 +02:00
else if(event.type.type == "filter") {
2023-08-15 15:34:53 +02:00
debug.info("Adding new filter compenent");
call([{
methodname: 'local_treestudyplan_add_studyitem',
2023-09-08 12:47:29 +02:00
args: {
"line_id": self.line.id,
"slot" : self.slotindex,
"type": event.data.type,
"details":{
"badge_id": event.data.badge?event.data.badge.id:undefined,
}
}
2023-12-13 23:49:06 +01:00
}])[0].then((response) => {
let item = response;
2023-07-15 22:00:17 +02:00
self.relocateStudyItem(item).done(()=>{
2023-08-15 15:34:53 +02:00
item.layer = this.layer;
2023-07-15 22:00:17 +02:00
self.value.push(item);
2023-09-08 12:47:29 +02:00
self.$emit("input",self.value);
});
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
2023-09-08 12:47:29 +02:00
}
}
},
onCut(event) {
const self=this;
let id = event.data.id;
2023-09-08 12:47:29 +02:00
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
}
}
2023-07-15 22:00:17 +02:00
// Do something to signal that this item has been removed
this.$emit("input",this.value);
ItemEventBus.$emit('coursechange');
},
2023-07-15 22:00:17 +02:00
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',
2023-07-15 22:00:17 +02:00
args: { 'items': [iteminfo] } // function was designed to relocate multiple items at once, hence the array
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
},
2023-07-15 22:00:17 +02:00
onDragEnter(event){
this.hover.component = event.data;
this.hover.type = event.type;
},
onDragLeave(){
this.hover.component = null;
this.hover.type = null;
},
2023-08-04 11:54:16 +02:00
maxSpan(){
// Determine the maximum span for components in this slot
return this.page.periods - this.slotindex + 1;
2023-08-04 11:54:16 +02:00
},
makeType(item){
return {
item: true,
component: false,
span: item.span,
type: this.type,
};
},
checkType(type) {
if(type.type == this.type){
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 ':' ')"
2023-07-28 00:04:24 +02:00
:data-studyline="line.id" ref="main"
2023-08-04 11:54:16 +02:00
:style='spanCss'
2023-07-15 22:00:17 +02:00
><drag v-if="item"
2023-09-08 12:47:29 +02:00
:key="item.id"
class="t-slot-item"
:data="item"
:type="makeType(item)"
2023-07-15 22:00:17 +02:00
@cut="onCut"
2023-09-08 12:47:29 +02:00
><t-item
@deleted="onCut"
v-model="value[itemidx]"
:plan="plan"
:line='line'
:page='page'
:period='period'
:maxspan='maxSpan()'
></t-item
></drag
2023-07-15 22:00:17 +02:00
><drop v-else
:class="'t-slot-drop '+type + (layer > 0?' secondary':' primary')"
2023-08-04 11:54:16 +02:00
:accepts-type="checkType"
2023-07-15 22:00:17 +02:00
@drop="onDrop"
mode="cut"
2023-07-15 22:00:17 +02:00
@dragenter="onDragEnter"
@dragleave="onDragLeave"
><template v-if="hover.component">
2023-09-08 12:47:29 +02:00
<div v-if="hover.type.item"
class="t-slot-item feedback"
2023-07-15 22:00:17 +02:00
:key="hover.component.id"
><t-item v-model="hover.component" dummy></t-item
></div
2023-09-08 12:47:29 +02:00
><div v-else-if="hover.type.type == 'gradable'"
class="t-slot-item feedback"
2023-07-15 22:00:17 +02:00
:key="'course-'+hover.component.id"
><t-item-course v-model="courseHoverDummy"></t-item-course></div
2023-09-08 12:47:29 +02:00
><div v-else-if="hover.type.type == 'filter'"
class="t-slot-item feedback"
2023-07-15 22:00:17 +02:00
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
2023-09-08 12:47:29 +02:00
><div v-else
class="t-slot-item feedback"
2023-07-15 22:00:17 +02:00
:key="hover.type">--{{ hover.type }}--</div
></template
></drop>
<t-item-timing-checker hidden
2023-08-07 20:27:36 +02:00
v-if="value && value[itemidx] && value[itemidx].course"
ref="timingChecker"
:maxspan="maxSpan()"
:page="page"
:line="line"
:period="period"
2023-08-07 20:27:36 +02:00
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];
},
course_period_matches() {
const self=this;
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);
2023-09-08 12:47:29 +02:00
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.course_timing,
datechanger: {
coursespan: null,
periodspan: null,
globals: datechanger_globals,
}
};
},
methods: {
validate_course_period() {
const self = this;
2023-09-08 12:47:29 +02:00
debug.info("Validating course and period");
if(!(self.course_period_matches)){
debug.info("Course timing does not match period timing");
if(self.value.course.canupdatecourse){
if(!self.hidden || !self.datechanger.globals.default){
2023-09-08 12:47:29 +02:00
// 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){
// go for it without asking
self.change_course_period();
}
}
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);
}
},
has_filter() {
const slots = this.page.slots;
},
change_course_period() {
const self=this;
// Save the state
if(self.datechanger.globals.default){
self.datechanger.globals.defaultvalue = true;
}
return call([{
methodname: 'local_treestudyplan_course_period_timing',
2023-09-08 12:47:29 +02:00
args: { period_id: self.period.id,
course_id: this.value.course.id,
span: this.value.span,
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception).done((response) => {
self.value.course.startdate = response.startdate;
self.value.course.enddate = response.enddate;
self.value.course.timing = response.timing;
self.$emit("input",self.value);
});
},
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;
},
shift_collisions(span) {
// Check all periods for collision
const shiftfilters = [];
const shiftcourses = [];
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).done((response) => {
});
}
},
change_span(span) {
const self=this;
this.shift_collisions(span);
return call([{
methodname: 'local_treestudyplan_set_studyitem_span',
args: { id: self.value.id,
span: span
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception).done((response) => {
self.value.span = response.span;
self.$emit('input',self.value);
self.$nextTick(() => {
self.validate_course_period();
});
2023-09-08 12:47:29 +02:00
} );
},
format_duration(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: `
2023-08-07 11:48:06 +02:00
<div :class="'t-item-timing-checker'" :style="hidden?'display: none ':''">
<template v-if="!hidden" >
<span class="mr-1" v-if="course_period_matches">
<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="validate_course_period()" class="text-warning"
v-b-tooltip.hover.bottomleft :title="text.timing_off"
2023-09-08 12:47:29 +02:00
><i class="fa fa-calendar-times-o"
2023-08-07 11:48:06 +02:00
></i
><i class="fa fa-question-circle text-black-50"
style="font-size: 0.8em; top: -0.3em; position: relative;"
2023-09-08 12:47:29 +02:00
2023-08-07 11:48:06 +02:00
></i
></a>
</span>
<span class="ml-1" v-b-tooltip.hover.bottomleft :title="text.periodspan_desc"
>{{ text.periodspan
}}&nbsp;<b-form-select v-if="maxspan > 1"
2023-08-07 11:48:06 +02:00
class=""
size="sm" @change="change_span" v-model="value.span">
<b-form-select-option v-for="(n,i) in maxspan" :value='n' :key='i'
>{{ n }}</b-form-select-option>
2023-08-07 11:48:06 +02:00
</b-form-select
><span v-else>{{value.span}}</span>&nbsp;{{
2023-09-08 12:47:29 +02:00
(value.span == 1)?text.period.toLowerCase():text.periods.toLowerCase()
}}<i
class="fa fa-question-circle text-black-50"
2023-08-07 11:48:06 +02:00
style="font-size: 0.8em; top: -0.3em; position: relative;"
></i>
</span>
2023-08-07 11:48:06 +02:00
</template>
2023-09-08 12:47:29 +02:00
<b-modal
:id="'t-course-timing-matching-'+this.id"
size="lg"
:title="text.title"
@ok="change_course_period"
:ok-title="text.yes"
2023-08-04 12:10:43 +02:00
ok-variant="danger"
:cancel-title="text.no"
2023-09-08 12:47:29 +02:00
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>
2023-08-07 16:11:14 +02:00
<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>
{{ format_duration(datechanger.coursespan)}}</p>
</b-col>
<b-col cols="6">
<h3> {{ text.period }} </h3>
2023-08-07 16:11:14 +02:00
<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>
{{ format_duration(datechanger.periodspan)}}</p>
</b-col>
</b-row>
2023-08-07 16:11:14 +02:00
<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>
2023-09-08 12:47:29 +02:00
<b-modal
:id="'t-course-timing-warning-'+this.id"
size="lg"
2023-08-04 12:10:43 +02:00
ok-variant="primary"
:title="text.title"
:ok-title="text.yes"
2023-08-04 12:10:43 +02:00
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>
2023-08-07 16:11:14 +02:00
<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>
{{ format_duration(datechanger.coursespan)}}</p>
</b-col>
<b-col cols=>"6">
<h3> {{ text.period }} </h3>
2023-08-07 16:11:14 +02:00
<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>
{{ format_duration(datechanger.periodspan)}}</p>
</b-col>
</b-row>
2023-08-07 16:11:14 +02:00
<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>
2023-08-07 16:11:14 +02:00
</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;},
2023-09-08 12:47:29 +02:00
},
},
data() {
return {
dragLine: null,
dragEventListener: null,
deleteMode: false,
condition_options: string_keys.conditions,
text: strings.item_text,
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,{
2023-08-04 22:54:32 +02:00
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);
},
2023-07-18 13:19:48 +02:00
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
2023-07-18 13:19:48 +02:00
},
onDrop(event){
let from_id = event.data.id;
let to_id = this.value.id;
this.redrawLines();
call([{
methodname: 'local_treestudyplan_connect_studyitems',
args: { 'from_id': from_id, 'to_id': to_id }
2023-12-13 23:49:06 +01:00
}])[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);
2023-12-13 23:49:06 +01:00
}).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]){
2023-09-08 12:47:29 +02:00
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
2023-09-08 12:47:29 +02:00
}
// create a new line if the start and finish items are visible
if(start !== null && end !== null && isVisible(start) && isVisible(end)){
2023-08-04 22:54:32 +02:00
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 }
2023-12-13 23:49:06 +01:00
}])[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);
}
2023-12-13 23:49:06 +01:00
}).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,
2023-09-08 12:47:29 +02:00
'conditions': this.value.conditions,
'continuation_id': this.value.continuation_id,}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
},
doShowContext(event) {
if(this.hasContext){
this.showContext=true;
event.preventDefault();
}
},
redrawLines(){
2023-08-15 15:34:53 +02:00
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){
2023-08-15 15:34:53 +02:00
if(this.value.connections && this.value.connections.out){
for(let i in this.value.connections.in){
let c_in = this.value.connections.in[i];
if(conn.id == c_in.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(){
2023-11-11 20:17:45 +01:00
this.redrawLines();
},
// When an item is disPositioned - (temporarily) removed from the list,
// all connections need to be deleted.
onDisPositioned(re_id){
2023-08-15 15:34:53 +02:00
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 == re_id){
this.removeLine(conn);
2023-09-08 12:47:29 +02:00
}
2023-08-15 15:34:53 +02:00
}
}
},
2023-09-08 12:47:29 +02:00
// When an item is deleted
// all connections to/from that item need to be cleaned up
onItemDeleted(item_id){
const self = this;
2023-08-15 15:34:53 +02:00
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 == item_id){
self.removeLine(conn);
self.value.connections.out.splice(i, 1);
2023-09-08 12:47:29 +02:00
}
2023-08-15 15:34:53 +02:00
}
}
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 == item_id){
self.value.connections.out.splice(i, 1);
2023-09-08 12:47:29 +02:00
}
2023-08-15 15:34:53 +02:00
}
}
},
onRedrawLines(){
this.redrawLines();
},
removeLine(conn){
if(this.lines[conn.to_id]){
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
2023-09-08 12:47:29 +02:00
}
},
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, }
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
if(response.success == true){
self.$emit("deleted",{ data: self.value });
}
2023-12-13 23:49:06 +01:00
}).catch(notification.exception);
}
}).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);
2023-09-08 12:47:29 +02:00
// 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);
}
},
beforeUpdate(){
},
updated(){
if(!this.dummy) {
this.redrawLines();
}
},
template: `
<div class="t-item-base" :id="'studyitem-'+value.id">
2023-09-08 12:47:29 +02:00
<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="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="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"
>
2023-09-08 12:47:29 +02:00
<b-form-select size="sm"
@input="updateItem"
v-model="value.conditions"
:options="condition_options"
></b-form-select>
</b-form-group>
<template #modal-footer="{ ok, cancel, hide }" >
<a href='#' @click='deleteItem()' class="text-danger"
><i class="fa fa-trash"></i>
{{ text.delete }}
</a>
<b-button size="sm" variant="primary" @click="ok()">
{{ text.ok }}
</b-button>
</template>
</b-modal>
</div>
`,
});
Vue.component('t-item-invalid', {
props: {
'value' :{
type: Object,
default: function(){ 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='$emit("deleterq")' class="text-danger"
><i class="fa fa-trash"></i></a>
</b-card-body>
</b-col>
</b-row>
</b-card>
</div>
`,
});
2023-11-23 07:44:04 +01:00
//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;}
2023-09-08 12:47:29 +02:00
},
},
data() {
return {
condition_options: string_keys.conditions,
text: strings.item_course_text,
};
},
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(){
2023-11-23 07:44:04 +01:00
if(this.hasGrades() || this.hasCompletions() || this.hasCompetencies()) {
return "t-configured-ok";
} else {
return "t-configured-alert";
}
},
configurationIcon(){
2023-11-23 07:44:04 +01:00
if(this.hasGrades() || this.hasCompletions() || this.hasCompetencies()) {
return "check";
} else {
return "exclamation-circle";
}
},
startdate(){
return format_date(this.value.course.startdate);
},
enddate(){
if(this.value.course.enddate){
return format_date(this.value.course.enddate);
2023-09-08 12:47:29 +02:00
}
else {
return this.text.noenddate;
}
}
},
methods: {
hasGrades() {
2023-06-26 21:44:31 +02:00
if(this.value.course.grades && this.value.course.grades.length > 0){
for(const g of this.value.course.grades){
if(g.selected){
return true;
}
}
2023-09-08 12:47:29 +02:00
}
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;
},
2023-11-23 07:44:04 +01:00
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,
2023-09-08 12:47:29 +02:00
'item_id': this.value.id,
'include': newValue,
'required': g.required,
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
},
requiredChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
2023-09-08 12:47:29 +02:00
'item_id': this.value.id,
'include': g.selected,
'required': newValue,
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
2023-09-08 12:47:29 +02:00
},
updateConditions() {
call([{
methodname: 'local_treestudyplan_edit_studyitem',
args: { 'id': this.value.id,
2023-09-08 12:47:29 +02:00
'conditions': this.value.conditions,
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
},
},
created() {
},
template: `
<div class="t-item-course">
<b-card no-body >
2023-08-04 16:54:41 +02:00
<div class='d-flex flex-wrap mr-0 ml-0'>
<div>
2023-09-08 12:47:29 +02:00
<span
:title="text['coursetiming_'+value.course.timing]"
v-b-popover.hover.top="startdate+' - '+enddate"
:class="'t-timing-indicator timing-'+value.course.timing"></span>
2023-08-04 16:54:41 +02:00
</div>
<div class="flex-fill align-items-center">
<b-card-body >
<a class="t-item-course-config"
2023-09-08 12:47:29 +02:00
v-b-modal="'t-item-course-config-'+value.id"
href="#" @click.prevent=""
><i :class="'fa fa-'+configurationIcon+' ' + configurationState"></i></a>
<a v-b-modal="'t-item-course-config-'+value.id"
2023-09-08 12:47:29 +02:00
:id="'t-item-course-details-'+value.id"
:href="'/course/view.php?id='+value.course.id"
@click.prevent.stop="">{{ value.course.displayname }}</a>
</b-card-body>
2023-08-04 16:54:41 +02:00
</div>
</div>
<b-modal class=""
2023-09-08 12:47:29 +02:00
: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="'/course/view.php?id='+value.course.id" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a>
2023-09-08 12:47:29 +02:00
<a v-if='!!value.course.completion'
:href="'/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>
2023-08-07 11:48:06 +02:00
<t-item-timing-checker
class="mt-1"
:maxspan="maxspan"
:page="page"
:line="line"
:period="period"
v-model="value"
></t-item-timing-checker>
</div>
</template>
2023-09-08 12:47:29 +02:00
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="above"
></s-course-extrafields>
2023-09-08 12:47:29 +02:00
<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
2023-09-08 12:47:29 +02:00
v-if='!!value.course.completion'
v-model='value.course.completion'
:course='value.course'
></t-item-course-completion>
2023-11-23 07:44:04 +01:00
<t-item-course-competency
v-if='!!value.course.competency'
v-model='value.course.competency'
:item='value'
2023-11-23 07:44:04 +01:00
></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='$emit("deleterq")' class="text-danger"
><i class="fa fa-trash"></i>
{{ text.delete }}
</a>
<b-button size="sm" variant="primary" @click="ok()">
{{ text.ok }}
</b-button>
</template>
</b-modal>
</b-card></div>
`,
});
Vue.component('t-item-course-grades', {
props: {
'value' :{
type: Object,
default(){ return null;},
},
'plan' :{
type: Object,
default(){ return null;},
},
},
data() {
return {
condition_options: string_keys.conditions,
text: strings.item_course_text,
};
},
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;
2023-09-08 12:47:29 +02:00
},
},
methods: {
includeChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
2023-09-08 12:47:29 +02:00
'item_id': this.value.id,
'include': newValue,
'required': g.required,
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
},
requiredChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
2023-09-08 12:47:29 +02:00
'item_id': this.value.id,
'include': g.selected,
'required': newValue,
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
2023-09-08 12:47:29 +02:00
},
},
created() {
},
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>
2023-09-08 12:47:29 +02:00
<s-edit-mod
:title="value.course.fullname"
@saved="(fd) => g.name = fd.get('name')"
2023-09-08 12:47:29 +02:00
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: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
text: strings.completion,
};
},
created(){
const self = this;
// Get text strings for condition settings
let stringkeys = [];
for(const key in this.text){
stringkeys.push({ key: key, component: 'local_treestudyplan'});
}
get_strings(stringkeys).then(function(strings){
let i = 0;
for(const key in self.text){
self.text[key] = strings[i];
i++;
}
});
},
computed: {
hasCompletions() {
if(this.value.conditions) {
for(const cgroup of this.value.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
},
methods: {
completion_icon(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";
}
},
2023-09-08 12:47:29 +02:00
completion_tag(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="'/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>
`,
});
2023-11-23 07:44:04 +01:00
//TAG: Course competency
Vue.component('t-item-course-competency',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
item: {
2023-11-23 07:44:04 +01:00
type: Object,
default: function(){ return { id: null};},
}
2023-11-23 07:44:04 +01:00
},
data() {
return {
text: strings.competency,
};
},
created(){
2023-11-23 07:44:04 +01:00
},
computed: {
hasCompletions() {
if(this.value.conditions) {
for(const cgroup of this.value.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
},
methods: {
pathtags(competency) {
2023-11-23 07:44:04 +01:00
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>`;
2023-11-23 07:44:04 +01:00
}
return s;
2023-11-23 07:44:04 +01:00
},
requiredChanged(newValue,c){
call([{
methodname: 'local_treestudyplan_require_competency',
args: { 'competency_id': c.id,
'item_id': this.item.id,
'required': newValue,
}
2023-12-13 23:49:06 +01:00
}])[0].catch(notification.exception);
},
2023-11-23 07:44:04 +01:00
},
template: `
<table class="t-item-course-competency-list">
2023-11-23 07:44:04 +01:00
<tr v-if="value.competencies.length == 0">
2024-01-26 12:26:01 +01:00
<td colspan='2'>{{text.competency_not_configured}}
<br><a :href="'/admin/tool/lp/coursecompetencies.php?courseid='+item.course.id" target='_blank'>{{text.configure_competency}}</a>
2023-11-23 07:44:04 +01:00
</td>
</tr>
<template v-else>
<tr class='t-item-course-competency-headers'>
<th>{{text.heading}}</th>
<th></th>
<th>{{text.required}}</th>
</tr>
2023-11-23 07:44:04 +01:00
<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>
2023-11-23 07:44:04 +01:00
</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="'/admin/tool/lp/competencies.php?competencyid='+c.id" target="_blank"
>{{c.title}} {{c.details}} </a
2023-11-23 07:44:04 +01:00
></h1>
<div><span v-html="pathtags(c)"></span></div>
2023-11-23 07:44:04 +01:00
</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>
2023-11-23 07:44:04 +01:00
</b-modal>
</tr>
</template>
</table>
`,
});
/************************************
* *
2023-08-04 11:54:16 +02:00
* Toolbox list components *
* *
************************************/
Vue.component('t-item-junction',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
condition_options: string_keys.conditions,
};
},
methods: {
},
template: `
2023-09-08 12:47:29 +02:00
<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: function(){ return {};},
},
},
data() {
return {
};
},
methods: {
},
template: `
2023-09-08 12:47:29 +02:00
<div class='t-item-finish t-item-filter'>
<i class="fa fa-stop-circle"></i>
2023-09-08 12:47:29 +02:00
</div>
`,
});
Vue.component('t-item-start',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
};
},
created(){
2023-09-08 12:47:29 +02:00
},
methods: {
},
template: `
2023-09-08 12:47:29 +02:00
<div class='t-item-start t-item-filter'>
<i class="fa fa-play-circle"></i>
2023-09-08 12:47:29 +02:00
</div>
`,
});
Vue.component('t-item-badge',{
props: {
value : {
type: Object,
default: function(){ return { badge: {}};},
},
},
data() {
return {
txt: strings,
text: strings.item_text,
};
},
methods: {
},
template: `
2023-09-08 12:47:29 +02:00
<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>
2023-09-08 12:47:29 +02:00
<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=""
2023-09-08 12:47:29 +02:00
: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='$emit("deleterq")' class="text-danger"
><i class="fa fa-trash"></i>
{{ text.delete }}
</a>
<b-button size="sm" variant="primary" @click="ok()">
{{ text.ok }}
</b-button>
2023-09-08 12:47:29 +02:00
</template>
</b-modal>
2023-09-08 12:47:29 +02:00
</div>
`,
});
Vue.component('t-coursecat-list',{
props: {
value : {
type: Array,
default: function(){ return {};},
},
},
data() {
return {
};
},
methods: {
},
template: `
<ul class="t-coursecat-list">
2023-09-08 12:47:29 +02:00
<t-coursecat-list-item
v-for="coursecat,idx in value"
v-model="value[idx]"
:key="coursecat.id"></t-coursecat-list-item>
2023-09-08 12:47:29 +02:00
</ul>
`,
});
Vue.component('t-coursecat-list-item',{
props: {
value : {
type: Object,
default: function(){ 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.children.length == 0)) ||
(this.value.hascourses && (!this.value.courses || this.value.courses.length == 0));
},
onShowDetails(){
const self = this;
if(this.canLoadMore()) {
call([{
methodname: 'local_treestudyplan_get_category',
args: { "id": this.value.id}
2023-12-13 23:49:06 +01:00
}])[0].then(function(response){
self.$emit('input', response);
2023-12-13 23:49:06 +01:00
}).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"></i>
<i class="when-open fa fa-caret-down"></i>
<span class="t-coursecat-heading">
<i class="t-coursecat-list-item fa fa-tasks"></i>
{{ value.category.name }}
</span>
</span>
<span v-else>
<span class="t-coursecat-heading">
<i class="t-coursecat-list-item fa fa-tasks"></i>
{{ value.category.name }}
</span>
</span>
2023-09-08 12:47:29 +02:00
<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: function(){ return {};},
},
},
data() {
return {
};
},
methods: {
2023-08-04 11:54:16 +02:00
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">
2023-09-08 12:47:29 +02:00
<drag
class="draggable-course"
:data="course"
2023-08-04 11:54:16 +02:00
:type="makeType()"
@cut=""
>
<i class="t-course-list-item fa fa-book"></i> {{ course.shortname }} - {{ course.fullname }}
</drag>
</li>
2023-09-08 12:47:29 +02:00
</ul>
`,
});
2023-09-08 12:47:29 +02:00
},
};