2869 lines
127 KiB
JavaScript
2869 lines
127 KiB
JavaScript
/*eslint no-var: "error"*/
|
|
/*eslint no-console: "off"*/
|
|
/*eslint-disable no-trailing-spaces */
|
|
/*eslint-env es6*/
|
|
// Put this file in path/to/plugin/amd/src
|
|
|
|
import LeaderLine from './leaderline';
|
|
import {call} from 'core/ajax';
|
|
import notification from 'core/notification';
|
|
import {debounce} from './debounce';
|
|
import {get_strings} from 'core/str';
|
|
import {load_stringkeys, load_strings} from './string-helper';
|
|
import {objCopy,transportItem} from './studyplan-processor';
|
|
import Debugger from './debugger';
|
|
import {download,upload} from './downloader';
|
|
|
|
const STUDYPLAN_EDITOR_FIELDS = ['name','shortname','description','context_id',
|
|
'slots','startdate','enddate','aggregation','aggregation_config'];
|
|
|
|
export default {
|
|
STUDYPLAN_EDITOR_FIELDS: STUDYPLAN_EDITOR_FIELDS, // make copy available in plugin
|
|
install(Vue/*,options*/){
|
|
|
|
let debug = new Debugger("treestudyplan-editor");
|
|
debug.enable();
|
|
/************************************
|
|
* *
|
|
* 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();
|
|
const LineGravity = 70;
|
|
|
|
let string_keys = load_stringkeys({
|
|
conditions: [
|
|
{ value: null, textkey: 'condition_default'},
|
|
{ value: 'ALL', textkey: 'condition_all'},
|
|
{ value: '67', textkey: 'condition_67'},
|
|
{ value: '50', textkey: 'condition_50'},
|
|
{ value: 'ANY', textkey: 'condition_any'},
|
|
],
|
|
});
|
|
|
|
let strings = load_strings({
|
|
studyplan_text: {
|
|
studyline_editmode: 'studyline_editmode',
|
|
editmode_modules_hidden:'editmode_modules_hidden',
|
|
studyline_add: 'studyline_add',
|
|
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_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',
|
|
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',
|
|
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_disable_autoenddate_title: 'advanced_disable_autoenddate_title',
|
|
advanced_disable_autoenddate_desc: 'advanced_disable_autoenddate_desc',
|
|
advanced_disable_autoenddate_button: 'advanced_disable_autoenddate_button',
|
|
advanced_confirm_header: 'advanced_confirm_header',
|
|
advanced_force_scale_confirm: 'advanced_force_scale_confirm',
|
|
advanced_import: 'advanced_import',
|
|
advanced_export: 'advanced_export',
|
|
advanced_export_csv: 'advanced_export_csv',
|
|
advanced_import_from_file: 'advanced_import_from_file',
|
|
advanced_purge: "advanced_purge",
|
|
advanced_purge_expl: "advanced_purge_expl",
|
|
advanced_cascade_cohortsync_title: "advanced_cascade_cohortsync_title",
|
|
advanced_cascade_cohortsync_desc: "advanced_cascade_cohortsync_desc",
|
|
advanced_cascade_cohortsync: "advanced_cascade_cohortsync",
|
|
},
|
|
studyplan_edit: {
|
|
studyplan_edit: 'studyplan_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_context: 'studyplan_context',
|
|
studyplan_slots: 'studyplan_slots',
|
|
studyplan_startdate: 'studyplan_startdate',
|
|
studyplan_enddate: 'studyplan_enddate',
|
|
choose_aggregation_style: 'choose_aggregation_style',
|
|
setting_bistate_thresh_excellent: 'setting_bistate_thresh_excellent',
|
|
settingdesc_bistate_thresh_excellent: 'settingdesc_bistate_thresh_excellent',
|
|
setting_bistate_thresh_good: 'setting_bistate_thresh_good',
|
|
settingdesc_bistate_thresh_good: 'settingdesc_bistate_thresh_good',
|
|
setting_bistate_thresh_completed: 'setting_bistate_thresh_completed',
|
|
settingdesc_bistate_thresh_completed: 'settingdesc_bistate_thresh_completed',
|
|
setting_bistate_support_failed: 'setting_bistate_support_failed',
|
|
settingdesc_bistate_support_failed: 'settingdesc_bistate_support_failed',
|
|
setting_bistate_thresh_progress: 'setting_bistate_thresh_progress',
|
|
settingdesc_bistate_thresh_progress: 'settingdesc_bistate_thresh_progress',
|
|
setting_bistate_accept_pending_submitted: 'setting_bistate_accept_pending_submitted',
|
|
settingdesc_bistate_accept_pending_submitted: 'settingdesc_bistate_accept_pending_submitted',
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
badge: {
|
|
share_badge: "share_badge",
|
|
dateissued: "dateissued",
|
|
dateexpire: "dateexpire",
|
|
badgeinfo: "badgeinfo",
|
|
},
|
|
|
|
});
|
|
|
|
|
|
/*
|
|
* T-STUDYPLAN
|
|
*/
|
|
Vue.component('t-studyplan', {
|
|
props: ['value', 'index'],
|
|
data() {
|
|
return {
|
|
config: {
|
|
userfields: [
|
|
{ key: "selected",},
|
|
{ key: "firstname", "sortable": true,},
|
|
{ key: "lastname", "sortable": true,},
|
|
],
|
|
cohortfields:[
|
|
{ key: "selected",},
|
|
{ key: "name", "sortable": true,},
|
|
{ key: "context", "sortable": true,},
|
|
]
|
|
},
|
|
create: {
|
|
studyline: {
|
|
'name': '',
|
|
'shortname': '',
|
|
'color': '#DDDDDD',
|
|
},
|
|
},
|
|
edit: {
|
|
studyline: {
|
|
editmode: false,
|
|
data: {
|
|
name: '',
|
|
shortname: '',
|
|
color: '#DDDDDD',
|
|
},
|
|
original: {},
|
|
},
|
|
studyplan: {
|
|
data: {
|
|
name: '',
|
|
shortname: '',
|
|
description: '',
|
|
slots : 4,
|
|
startdate: '2020-08-01',
|
|
enddate: '',
|
|
aggregation: '',
|
|
aggregation_config: '',
|
|
aggregation_info: {
|
|
useRequiredGrades: true,
|
|
useItemCondition: false,
|
|
},
|
|
|
|
},
|
|
original: {},
|
|
}
|
|
},
|
|
text: strings.studyplan_text,
|
|
};
|
|
},
|
|
created() {
|
|
|
|
},
|
|
mounted() {
|
|
if(this.value.studylines.length == 0){
|
|
// start in editmode if studylines are empty
|
|
this.edit.studyline.editmode = true;
|
|
}
|
|
this.$root.$emit('redrawLines');
|
|
},
|
|
updated() {
|
|
console.info("UPDATED Studyplan");
|
|
this.$root.$emit('redrawLines');
|
|
ItemEventBus.$emit('redrawLines');
|
|
},
|
|
computed: {
|
|
},
|
|
methods: {
|
|
slotsempty(slots) {
|
|
if(Array.isArray(slots)){
|
|
let count = 0;
|
|
for(let i = 0; i < slots.length; i++) {
|
|
if(Array.isArray(slots[i].competencies)){
|
|
count += slots[i].competencies.length;
|
|
}
|
|
if(Array.isArray(slots[i].filters)){
|
|
count += slots[i].filters.length;
|
|
}
|
|
}
|
|
return (count == 0);
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
movedStudyplan(plan,from,to) {
|
|
this.$emit('moved',plan,from,to); // Throw the event up....
|
|
},
|
|
addStudyLine(studyplan,newlineinfo) {
|
|
call([{
|
|
methodname: 'local_treestudyplan_add_studyline',
|
|
args: {
|
|
'studyplan_id': studyplan.id,
|
|
'name': newlineinfo.name,
|
|
'shortname': newlineinfo.shortname,
|
|
'color': newlineinfo.color,
|
|
'sequence': studyplan.studylines.length,
|
|
}
|
|
}])[0].done(function(response){
|
|
debug.info("New studyline:",response);
|
|
studyplan.studylines.push(response);
|
|
newlineinfo.name = '';
|
|
newlineinfo.shortname = '';
|
|
newlineinfo.color = "#dddddd";
|
|
}).fail(notification.exception);
|
|
},
|
|
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;
|
|
debug.info('Edit Line',this.edit.studyline);
|
|
call([{
|
|
methodname: 'local_treestudyplan_edit_studyline',
|
|
args: { 'id': editedline.id,
|
|
'name': editedline.name,
|
|
'shortname': editedline.shortname,
|
|
'color': editedline.color,}
|
|
}])[0].done(function(response){
|
|
debug.info('Edit response:', response);
|
|
originalline['name'] = response['name'];
|
|
originalline['shortname'] = response['shortname'];
|
|
originalline['color'] = response['color'];
|
|
}).fail(notification.exception);
|
|
},
|
|
deleteLine(studyplan,line) {
|
|
debug.info('Delete Line',line);
|
|
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, }
|
|
}])[0].done(function(response){
|
|
debug.info('Delete response:', response);
|
|
if(response.success == true){
|
|
let index = studyplan.studylines.indexOf(line);
|
|
studyplan.studylines.splice(index, 1);
|
|
}
|
|
}).fail(notification.exception);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
reorderLines(event,lines){
|
|
debug.info("Reorder lines",event,lines);
|
|
|
|
// apply reordering
|
|
event.apply(lines);
|
|
// send the new sequence to the server
|
|
let sequence = [];
|
|
for(let idx in lines)
|
|
{
|
|
sequence.push({'id': lines[idx].id,'sequence': idx});
|
|
}
|
|
call([{
|
|
methodname: 'local_treestudyplan_reorder_studylines',
|
|
args: { 'sequence': sequence }
|
|
}])[0].done(function(response){
|
|
debug.info('Reorder response:', response);
|
|
}).fail(notification.exception);
|
|
},
|
|
deletePlan(studyplan){
|
|
const self=this;
|
|
debug.info('Delete studyplan:', studyplan);
|
|
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',
|
|
args: { 'id': studyplan.id, }
|
|
}])[0].done(function(response){
|
|
debug.info('Delete response:', response);
|
|
if(response.success == true){
|
|
self.$root.$emit("studyplanRemoved",studyplan);
|
|
}
|
|
}).fail(notification.exception);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
deleteStudyItem(event){
|
|
debug.info('Delete studyitem:', event);
|
|
//const self = this;
|
|
let item = event.data;
|
|
|
|
call([{
|
|
methodname: 'local_treestudyplan_delete_studyitem',
|
|
args: { 'id': item.id, }
|
|
}])[0].done(function(response){
|
|
debug.info('Delete response:', response);
|
|
if(response.success == true){
|
|
event.source.$emit('cut',event);
|
|
}
|
|
}).fail(notification.exception);
|
|
|
|
},
|
|
}
|
|
,
|
|
template:
|
|
`
|
|
<div>
|
|
<div class='controlbox t-studyplan-controlbox'>
|
|
<b-form-checkbox v-model="edit.studyline.editmode" class="sw-studyline-editmode" switch
|
|
>{{ text.studyline_editmode }}</b-form-checkbox>
|
|
<drop
|
|
mode='copy'
|
|
class='t-item-deletebox text-danger border-danger'
|
|
@drop='deleteStudyItem'
|
|
:accepts-type="['gradable-item','filter-item']"
|
|
><i class='fa fa-trash'></i>
|
|
</drop>
|
|
<span class='control deletable'>
|
|
<a v-if='value.studylines.length == 0' href='#' @click='deletePlan(value)'
|
|
><i class='text-danger fa fa-trash'></i></a>
|
|
</span>
|
|
<span class='control editable'>
|
|
<t-studyplan-edit v-model="value" @moved="movedStudyplan"
|
|
><i class='fa fa-pencil'></i> {{text.edit$core}}</t-studyplan-edit>
|
|
</span>
|
|
<span class='control editable'>
|
|
<t-studyplan-associate
|
|
v-model="value"><i class='fa fa-users'></i> {{text.associations}}</t-studyplan-associate>
|
|
</span>
|
|
<span class='control editable'>
|
|
<t-studyplan-advanced v-model="value"></t-studyplan-advanced>
|
|
</span>
|
|
</div>
|
|
<div class='t-studyplan-content'>
|
|
<drop-list v-if="edit.studyline.editmode"
|
|
:items="value.studylines"
|
|
class="t-slot-droplist"
|
|
:accepts-type="'studyline-'+value.id"
|
|
xreorder="$event.apply(value.studylines)"
|
|
@reorder="reorderLines($event,value.studylines)"
|
|
mode="copy"
|
|
row
|
|
>
|
|
<template v-slot:item="{item}">
|
|
<drag
|
|
:key="item.id"
|
|
class='t-studyplan-drag'
|
|
:data="item"
|
|
:type="'studyline-'+value.id"
|
|
>
|
|
<template v-slot:drag-image>
|
|
<i class="fa fa-arrows text-primary"></i>
|
|
</template>
|
|
<t-studyline
|
|
:color='item.color'
|
|
:name='item.name'
|
|
:code='item.shortname'
|
|
:editable='true'
|
|
:deletable='slotsempty(item.slots)'
|
|
:sequence='item.sequence'
|
|
:numlines='value.studylines.length'
|
|
@edit='editLineStart(item)'
|
|
@delete='deleteLine(value,item)'
|
|
>
|
|
<template v-slot:movebox><i class='fa fa-arrows text-primary'></i></template>
|
|
<div v-if="!slotsempty(item.slots)"> {{ text.editmode_modules_hidden}} </div>
|
|
</t-studyline>
|
|
</drag>
|
|
</template>
|
|
|
|
</drop-list>
|
|
<template v-else>
|
|
<t-studyline v-for="(item,lineindex) in value.studylines"
|
|
:key="item.id"
|
|
:color='item.color'
|
|
:name='item.name'
|
|
:code='item.shortname'
|
|
:sequence='lineindex'
|
|
:numlines='value.studylines.length'
|
|
@edit='editLineStart(item)'
|
|
>
|
|
<template v-for="(n,index) in (value.slots+1)">
|
|
<t-studyline-slot
|
|
v-if="index > 0"
|
|
type='gradable'
|
|
v-model="item.slots[index].competencies"
|
|
:key="'c-'+index"
|
|
:slotindex="index"
|
|
:lineid="item.id"
|
|
:plan="value">
|
|
</t-studyline-slot>
|
|
<t-studyline-slot
|
|
type='filter'
|
|
v-model="item.slots[index].filters"
|
|
:key="'f-'+index"
|
|
:slotindex="index"
|
|
:lineid="item.id"
|
|
:plan="value"
|
|
>
|
|
</t-studyline-slot>
|
|
</template>
|
|
</t-studyline>
|
|
</template>
|
|
<div :id="'studyplan-linewrapper-'+value.id" class='l-leaderline-linewrapper'></div>
|
|
</div>
|
|
<div v-if="edit.studyline.editmode" class='t-studyline-add'>
|
|
<a href="#" v-b-modal="'modal-add-studyline-'+value.id" @click="false;"
|
|
><i class='fa fa-plus'></i>{{ text.studyline_add }}</a>
|
|
</div>
|
|
<b-modal
|
|
:id="'modal-add-studyline-'+value.id"
|
|
size="lg"
|
|
:ok-title="text.add$core"
|
|
ok-variant="primary"
|
|
:title="text.studyline_add"
|
|
@ok="addStudyLine(value,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-'+value.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" />
|
|
<!--hsluv-picker
|
|
v-model="edit.studyline.data.color"
|
|
horizontal displaysize="175" ></hsluv-picker -->
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-modal>
|
|
</div>
|
|
`
|
|
});
|
|
|
|
/*
|
|
* T-STUDYPLAN-ADVANCED
|
|
*/
|
|
Vue.component('t-studyplan-advanced', {
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default(){ return null;},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
force_scales: {
|
|
selected_scale: null,
|
|
result: [],
|
|
},
|
|
text: strings.studyplan_advanced,
|
|
};
|
|
},
|
|
created() {
|
|
},
|
|
mounted() {
|
|
},
|
|
updated() {
|
|
|
|
},
|
|
computed: {
|
|
scales(){
|
|
return [{
|
|
id: null,
|
|
disabled: true,
|
|
name: this.text.advanced_pick_scale,
|
|
}].concat(this.value.advanced.force_scales.scales);
|
|
},
|
|
},
|
|
methods: {
|
|
disable_autoenddate(){
|
|
const self=this;
|
|
call([{
|
|
methodname: 'local_treestudyplan_disable_autoenddate',
|
|
args: {
|
|
studyplan_id: this.value.id,
|
|
}
|
|
}])[0].done(function(response){
|
|
self.$bvModal.msgBoxConfirm((response.success?self.text.success$core:self.text.error$core)
|
|
+ "\n" + response.msg);
|
|
}).fail(notification.exception);
|
|
},
|
|
force_scales_start(){
|
|
// set confirmation box
|
|
const self=this;
|
|
this.$bvModal.msgBoxConfirm(this.text.advanced_force_scale_confirm,{
|
|
title: this.text.advanced_force_scale_confirm,
|
|
okVariant: 'danger',
|
|
okTitle: this.text.confirm_ok,
|
|
cancelTitle: this.text.confirm_cancel,
|
|
}).then( value => {
|
|
if(value == true){
|
|
call([{
|
|
methodname: 'local_treestudyplan_force_studyplan_scale',
|
|
args: {
|
|
studyplan_id: this.value.id,
|
|
scale_id: this.force_scales.selected_scale,
|
|
}
|
|
}])[0].done(function(response){
|
|
self.force_scales.result = response;
|
|
}).fail(notification.exception);
|
|
}
|
|
});
|
|
},
|
|
export_plan(format){
|
|
const self = this;
|
|
if(format == undefined || !["json","csv"].includes(format)){
|
|
format = "json";
|
|
}
|
|
call([{
|
|
methodname: 'local_treestudyplan_export_plan',
|
|
args: {
|
|
studyplan_id: this.value.id,
|
|
format: format,
|
|
},
|
|
}])[0].done(function(response){
|
|
|
|
download(self.value.shortname+"."+format,response.content,response.format);
|
|
}).fail(notification.exception);
|
|
},
|
|
import_studylines(){
|
|
//const self = this;
|
|
upload((filename,content)=>{
|
|
call([{
|
|
methodname: 'local_treestudyplan_import_studylines',
|
|
args: {
|
|
studyplan_id: this.value.id,
|
|
content: content,
|
|
format: "application/json",
|
|
},
|
|
}])[0].done(function(response){
|
|
if(response.success){
|
|
location.reload();
|
|
} else {
|
|
debug.error("Import failed: ",response.msg);
|
|
}
|
|
|
|
}).fail(notification.exception);
|
|
}, "application/json");
|
|
},
|
|
purge_studyline(){
|
|
call([{
|
|
methodname: 'local_treestudyplan_delete_studyplan',
|
|
args: {
|
|
id: this.value.id,
|
|
force: true,
|
|
},
|
|
}])[0].done(function(response){
|
|
if(response.success){
|
|
location.reload();
|
|
} else {
|
|
debug.error("Could not delete plan: ",response.msg);
|
|
}
|
|
|
|
}).fail(notification.exception);
|
|
},
|
|
cascade_cohortsync(){
|
|
const self = this;
|
|
call([{
|
|
methodname: 'local_treestudyplan_cascade_cohortsync',
|
|
args: {
|
|
studyplan_id: this.value.id,
|
|
},
|
|
}])[0].done(function(response){
|
|
self.$bvModal.msgBoxConfirm(self.text.cascade_cohortsync + "\n"
|
|
+ (response.success?self.text.success$core:self.text.error$core)
|
|
+ "\n" + response.msg);
|
|
}).fail(notification.exception);
|
|
},
|
|
modal_close(){
|
|
this.force_scales.result = [];
|
|
}
|
|
},
|
|
template:
|
|
`
|
|
<span>
|
|
<a v-if="value.advanced"
|
|
href='#'
|
|
@click.prevent=''
|
|
class='text-danger'
|
|
v-b-modal="'t-studyplan-'+value.id+'-advanced'"
|
|
><i class='fa fa-cog'></i>{{text.advanced_tools}}</a>
|
|
<b-modal v-if="value.advanced"
|
|
:id="'t-studyplan-'+value.id+'-advanced'"
|
|
size="lg"
|
|
:title="text.advanced_tools_heading"
|
|
ok-only
|
|
@hide="modal_close"
|
|
>
|
|
<b-card no-body>
|
|
<b-tabs card>
|
|
<b-tab :title="text.advanced_warning_title" active>
|
|
{{ text.advanced_warning}}
|
|
</b-tab>
|
|
<b-tab :title="text.advanced_course_manipulation_title" >
|
|
<b-container>
|
|
<b-row><b-col cols="*">
|
|
<h3>{{ text.advanced_cascade_cohortsync_title}}</h3>
|
|
{{ text.advanced_cascade_cohortsync_desc}}
|
|
</b-col></b-row>
|
|
<b-row><b-col cols="*">
|
|
<b-button
|
|
variant="info"
|
|
@click="cascade_cohortsync"
|
|
>{{ text.advanced_cascade_cohortsync}}</b-button>
|
|
</b-col></b-row>
|
|
<b-row class="mt-3"><b-col cols="*">
|
|
<h3>{{ text.advanced_force_scale_title}}</h3>
|
|
{{ text.advanced_force_scale_desc}}
|
|
</b-col></b-row>
|
|
<b-row>
|
|
<b-col>
|
|
<b-form-select v-model="force_scales.selected_scale"
|
|
:options="scales" text-field="name" value-field="id"
|
|
></b-form-select>
|
|
</b-col>
|
|
<b-col cols="4">
|
|
<b-button
|
|
variant="danger"
|
|
:disabled="force_scales.selected_scale == null"
|
|
@click="force_scales_start"
|
|
>{{ text.advanced_force_scale_button}}</b-button>
|
|
</b-col>
|
|
|
|
</b-row>
|
|
<b-row class="mt-3"><b-col cols="*">
|
|
<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">
|
|
<li v-for='g in c.grades'>
|
|
<span class='t-advanced-gradename'>{{g.name}}</span>
|
|
<span v-if="g.changed == 'converted'" class='t-advanced-status changed'
|
|
>{{text.advanced_converted}}</span
|
|
><span v-else-if="g.changed == 'skipped'" class='t-advanced-status skipped'
|
|
>{{text.advanced_skipped}}</span
|
|
>
|
|
<span v-else class='t-advanced-status skipped'
|
|
>{{text.advanced_error}}</span
|
|
>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</b-col></b-row>
|
|
<b-row><b-col cols="*">
|
|
<h3>{{ text.advanced_disable_autoenddate_title}}</h3>
|
|
{{ text.advanced_disable_autoenddate_desc}}
|
|
</b-col></b-row>
|
|
<b-row><b-col cols="*">
|
|
<b-button
|
|
variant="danger"
|
|
@click="disable_autoenddate"
|
|
>{{ text.advanced_disable_autoenddate_button}}</b-button>
|
|
</b-col></b-row>
|
|
</b-container>
|
|
</b-tab>
|
|
<b-tab :title='text.advanced_export'>
|
|
<b-button
|
|
variant="primary"
|
|
@click="export_plan"
|
|
>{{ text.advanced_export}}</b-button>
|
|
<b-button
|
|
variant="danger"
|
|
@click="import_studylines"
|
|
>{{ text.advanced_import}}</b-button>
|
|
<b-button
|
|
variant="primary"
|
|
@click="export_plan('csv')"
|
|
>{{ text.advanced_export_csv}}</b-button>
|
|
</b-tab>
|
|
<b-tab :title='text.advanced_purge'>
|
|
<p>{{text.advanced_purge_expl}}</p>
|
|
<p><b-button
|
|
variant="danger"
|
|
@click="purge_studyline"
|
|
>{{ text.advanced_purge}}</b-button></p>
|
|
</b-tab> </b-tabs>
|
|
</b-card>
|
|
</b-modal>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
/*
|
|
* T-STUDYPLAN-EDIT
|
|
*/
|
|
Vue.component('t-studyplan-edit', {
|
|
props: {
|
|
'value' :{
|
|
type: Object,
|
|
default(){ return null;},
|
|
},
|
|
'mode' :{
|
|
type: String,
|
|
default() { return "edit";},
|
|
},
|
|
'type' :{
|
|
type: String,
|
|
default() { return "link";},
|
|
},
|
|
'variant' : {
|
|
type: String,
|
|
default() { return "";},
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
show: false,
|
|
config: {
|
|
userfields: [
|
|
{ key: "selected",},
|
|
{ key: "firstname", "sortable": true,},
|
|
{ key: "lastname", "sortable": true,},
|
|
],
|
|
cohortfields:[
|
|
{ key: "selected",},
|
|
{ key: "name", "sortable": true,},
|
|
{ key: "context", "sortable": true,},
|
|
]
|
|
},
|
|
editdata: {
|
|
name: '',
|
|
shortname: '',
|
|
description: '',
|
|
context_id: 1,
|
|
slots : 4,
|
|
startdate: (new Date()).getFullYear() + '-08-01',
|
|
enddate: ((new Date()).getFullYear()+1) + '-08-01',
|
|
aggregation: 'bistate',
|
|
aggregation_config: '',
|
|
},
|
|
aggregation_parsed: {
|
|
|
|
},
|
|
aggregators: [],
|
|
categories: [ { context_id: 1, category: { path: "System"}}], // overwritten during load...
|
|
text: strings.studyplan_edit,
|
|
};
|
|
},
|
|
created() {
|
|
// retrieve aggregator info
|
|
const self = this;
|
|
call([{
|
|
methodname: 'local_treestudyplan_list_aggregators',
|
|
args: [],
|
|
}])[0].done(function(response){
|
|
self.aggregators = response;
|
|
for(const ix in self.aggregators){
|
|
const ag = self.aggregators[ix];
|
|
|
|
try{
|
|
if(ag.defaultconfig && ag.defaultconfig.length > 0){
|
|
self.aggregation_parsed[ag.id] = JSON.parse(ag.defaultconfig);
|
|
}
|
|
}
|
|
catch(e){
|
|
debug.warn(e);
|
|
}
|
|
}
|
|
}).fail(notification.exception);
|
|
call([{
|
|
methodname: 'local_treestudyplan_list_accessible_categories',
|
|
args: {operation: "edit",}
|
|
}])[0].done(function(response){
|
|
for(const ix in response){
|
|
const cat = response[ix];
|
|
cat.category.pathname = cat.category.path.join(" / ");
|
|
}
|
|
self.categories = response;
|
|
}).fail(notification.exception);
|
|
},
|
|
mounted() {
|
|
},
|
|
updated() {
|
|
|
|
},
|
|
computed: {
|
|
},
|
|
methods: {
|
|
editPlanStart(){
|
|
if(this.mode != 'create'){
|
|
objCopy(this.editdata,this.value,STUDYPLAN_EDITOR_FIELDS);
|
|
}
|
|
// decode the aggregation config data that is stored
|
|
if(this.editdata.aggregation_config && this.editdata.aggregation_config.length > 0){
|
|
try{
|
|
this.aggregation_parsed[this.editdata.aggregation] = JSON.parse(this.editdata.aggregation_config);
|
|
}
|
|
catch(e){
|
|
debug.warn(e);
|
|
}
|
|
}
|
|
this.show = true;
|
|
},
|
|
editPlanFinish(){
|
|
const self = this;
|
|
let args = { };
|
|
let method = 'local_treestudyplan_edit_studyplan';
|
|
if(this.mode == 'create'){
|
|
method = 'local_treestudyplan_add_studyplan';
|
|
} else {
|
|
args['id'] = this.value.id;
|
|
}
|
|
|
|
// store the configuration for this aggregation type if it is relevant
|
|
if(this.aggregation_parsed[this.editdata.aggregation]){
|
|
this.editdata.aggregation_config = JSON.stringify(this.aggregation_parsed[this.editdata.aggregation]);
|
|
}
|
|
objCopy(args,this.editdata,STUDYPLAN_EDITOR_FIELDS);
|
|
|
|
call([{
|
|
methodname: method,
|
|
args: args
|
|
}])[0].done(function(response){
|
|
if(self.mode == 'create'){
|
|
self.$emit("created", response);
|
|
// And reset the edit fields to default
|
|
self.editdata = {
|
|
name: '',
|
|
shortname: '',
|
|
description: '',
|
|
context_id: 1,
|
|
slots : 4,
|
|
startdate: (new Date()).getFullYear() + '-08-01',
|
|
enddate: ((new Date()).getFullYear()+1) + '-08-01',
|
|
aggregation: 'bistate',
|
|
aggregation_config: '',
|
|
};
|
|
}
|
|
else {
|
|
// determine if the plan moved context...
|
|
const moved_from = self.value.context_id;
|
|
const moved_to = response.context_id;
|
|
const moved = (moved_from != moved_to);
|
|
|
|
objCopy(self.value,response,STUDYPLAN_EDITOR_FIELDS);
|
|
self.$emit('input',self.value);
|
|
if(moved){
|
|
self.$emit('moved',self.value,moved_from, moved_to);
|
|
}
|
|
}
|
|
}).fail(notification.exception);
|
|
},
|
|
numberFilter(value){
|
|
return value;
|
|
}
|
|
}
|
|
,
|
|
template:
|
|
`
|
|
<span class='s-studyplan-edit'>
|
|
<b-button :variant="variant" v-if='type == "button"' @click.prevent='editPlanStart()'
|
|
><slot><i class='fa fa-pencil'></i></slot></b-button>
|
|
<a variant="variant" v-else href='#' @click.prevent='editPlanStart()'
|
|
><slot><i class='fa fa-pencil'></i></slot></a>
|
|
<b-modal
|
|
v-model="show"
|
|
size="lg"
|
|
ok-variant="primary"
|
|
:title="text.studyplan_edit"
|
|
@ok="editPlanFinish()"
|
|
:ok-disabled="Math.min(editdata.name.length,editdata.shortname.length) == 0"
|
|
>
|
|
<b-container>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.studyplan_name}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="editdata.name"
|
|
:state='editdata.name.length>0'
|
|
:placeholder="text.studyplan_name_ph"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.studyplan_shortname}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="editdata.shortname"
|
|
:state='editdata.shortname.length>0'
|
|
:placeholder="text.studyplan_shortname_ph"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.studyplan_description}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="editdata.description"
|
|
:placeholder="text.studyplan_description_ph"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.studyplan_context}}</b-col>
|
|
<b-col>
|
|
<b-form-select v-model="editdata.context_id"
|
|
:options="categories" text-field="category.pathname" value-field="context_id"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.studyplan_slots}}</b-col>
|
|
<b-col>
|
|
<b-form-input type=number v-model="editdata.slots"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.studyplan_startdate}}</b-col>
|
|
<b-col>
|
|
<b-form-datepicker v-model="editdata.startdate"></b-form-datepicker>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.studyplan_enddate}}</b-col>
|
|
<b-col>
|
|
<b-form-datepicker v-model="editdata.enddate" ></b-form-datepicker>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.choose_aggregation_style}}</b-col>
|
|
<b-col>
|
|
<b-form-select v-model="editdata.aggregation"
|
|
:options="aggregators" text-field="name" value-field="id"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<template v-if="aggregation_parsed.bistate && editdata.aggregation == 'bistate'">
|
|
<b-row >
|
|
<b-col cols="4">{{ text.setting_bistate_thresh_excellent}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="aggregation_parsed.bistate.thresh_excellent"
|
|
type="number" number :formatter="numberFilter"
|
|
></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row >
|
|
<b-col cols="4">{{ text.setting_bistate_thresh_good}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="aggregation_parsed.bistate.thresh_good"
|
|
type="number" number :formatter="numberFilter"
|
|
></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row >
|
|
<b-col cols="4">{{ text.setting_bistate_thresh_completed}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="aggregation_parsed.bistate.thresh_completed"
|
|
type="number" number :formatter="numberFilter"
|
|
></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col cols="4">{{ text.setting_bistate_thresh_progress}}</b-col>
|
|
<b-col>
|
|
<b-form-input v-model="aggregation_parsed.bistate.thresh_progress"
|
|
type="number" number :formatter="numberFilter"
|
|
></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row><b-col cols="*"> </b-col></b-row>
|
|
<b-row>
|
|
<b-col cols="7">{{ text.setting_bistate_support_failed}}</b-col>
|
|
<b-col>
|
|
<b-form-checkbox v-model="aggregation_parsed.bistate.use_failed"
|
|
></b-form-checkbox>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row><b-col cols="*"> </b-col></b-row>
|
|
<b-row >
|
|
<b-col cols="7">{{ text.setting_bistate_accept_pending_submitted}}</b-col>
|
|
<b-col>
|
|
<b-form-checkbox v-model="aggregation_parsed.bistate.accept_pending_as_submitted"
|
|
></b-form-checkbox>
|
|
</b-col>
|
|
</b-row>
|
|
</template>
|
|
</b-container>
|
|
</b-modal>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
/*
|
|
* T-STUDYPLAN-ASSOCIATE
|
|
*/
|
|
Vue.component('t-studyplan-associate', {
|
|
props: ['value',],
|
|
data() {
|
|
return {
|
|
show: false,
|
|
config: {
|
|
userfields: [
|
|
{ key: "selected",},
|
|
{ key: "firstname", "sortable": true,},
|
|
{ key: "lastname", "sortable": true,},
|
|
],
|
|
cohortfields:[
|
|
{ key: "selected",},
|
|
{ key: "name", "sortable": true,},
|
|
{ key: "context", "sortable": true,},
|
|
]
|
|
},
|
|
association: {
|
|
cohorts: [],
|
|
users: [],
|
|
},
|
|
loading: {
|
|
cohorts: false,
|
|
users: false,
|
|
},
|
|
search: {users: [], cohorts:[]},
|
|
selected: {
|
|
search: {users: [] , cohorts:[]},
|
|
associated: {users: [] , cohorts:[]}
|
|
},
|
|
text: strings.studyplan_associate,
|
|
};
|
|
},
|
|
created() {
|
|
|
|
},
|
|
mounted() {
|
|
},
|
|
updated() {
|
|
|
|
},
|
|
methods: {
|
|
showModal(){
|
|
this.show = true;
|
|
this.loadAssociations();
|
|
},
|
|
cohortOptionModel(c){
|
|
return {
|
|
value: c.id,
|
|
text: c.name + ' (' + c.context.path.join(' / ') + ')',
|
|
};
|
|
},
|
|
userOptionModel(u){
|
|
return {
|
|
value: u.id,
|
|
text: u.firstname + ' ' + u.lastname,
|
|
};
|
|
},
|
|
loadAssociations(){
|
|
const self = this;
|
|
self.loading.cohorts = true;
|
|
self.loading.users = true;
|
|
call([{
|
|
methodname: 'local_treestudyplan_associated_users',
|
|
args: { studyplan_id: self.value.id,}
|
|
}])[0].done(function(response){
|
|
self.association.users = response.map(self.userOptionModel);
|
|
self.loading.users = false;
|
|
}).fail(notification.exception);
|
|
|
|
call([{
|
|
methodname: 'local_treestudyplan_associated_cohorts',
|
|
args: { studyplan_id: self.value.id,}
|
|
}])[0].done(function(response){
|
|
self.association.cohorts = response.map(self.cohortOptionModel);
|
|
self.loading.cohorts = false;
|
|
}).fail(notification.exception);
|
|
},
|
|
searchCohorts(searchtext){
|
|
const self = this;
|
|
|
|
if(searchtext.length > 0)
|
|
{
|
|
call([{
|
|
methodname: 'local_treestudyplan_list_cohort',
|
|
args: { like: searchtext, exclude_id: self.value.id}
|
|
}])[0].done(function(response){
|
|
self.search.cohorts = response.map(self.cohortOptionModel);
|
|
}).fail(notification.exception);
|
|
}
|
|
else {
|
|
self.search.cohorts = [];
|
|
}
|
|
},
|
|
cohortAssociate(){
|
|
const self = this;
|
|
let requests = [];
|
|
const associated = self.association.cohorts;
|
|
const search = self.search.cohorts;
|
|
const searchselected = self.selected.search.cohorts;
|
|
for(const i in searchselected){
|
|
const r = searchselected[i];
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
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){
|
|
const r = associatedselected[i];
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
call(requests);
|
|
},
|
|
searchUsers(searchtext){
|
|
const self = this;
|
|
if(searchtext.length > 0)
|
|
{
|
|
call([{
|
|
methodname: 'local_treestudyplan_find_user',
|
|
args: { like: searchtext, exclude_id: self.value.id}
|
|
}])[0].done(function(response){
|
|
self.search.users = response.map(self.userOptionModel);
|
|
}).fail(notification.exception);
|
|
}
|
|
else {
|
|
self.search.users = [];
|
|
}
|
|
},
|
|
userAssociate(){
|
|
const self = this;
|
|
let requests = [];
|
|
const associated = self.association.users;
|
|
const search = self.search.users;
|
|
const searchselected = self.selected.search.users;
|
|
for(const i in searchselected){
|
|
const r = searchselected[i];
|
|
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
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){
|
|
const r = associatedselected[i];
|
|
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
call(requests);
|
|
},
|
|
}
|
|
,
|
|
template:
|
|
`
|
|
<span class='s-studyplan-associate'
|
|
><a href='#' @click.prevent="showModal" ><slot><i class='fa fa-users'></i></slot></a>
|
|
<b-modal
|
|
v-model="show"
|
|
size="lg"
|
|
ok-variant="primary"
|
|
:title="text.associations + ' - ' + value.name"
|
|
ok-only>
|
|
<b-tabs class='s-studyplan-associate-window'>
|
|
<b-tab :title="text.cohorts">
|
|
<b-container>
|
|
<b-row class='mb-2 mt-2'>
|
|
<b-col>{{text.associated_cohorts}}</b-col>
|
|
<b-col>{{text.associate_cohorts}}</b-col>
|
|
</b-row>
|
|
<b-row class='mb-2'>
|
|
<b-col>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
type="text" @input="searchCohorts($event)"
|
|
:placeholder="text.search"></b-form-input>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.associated.cohorts"
|
|
:options="association.cohorts"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-select
|
|
multiple
|
|
v-model="selected.search.cohorts"
|
|
:options="search.cohorts"
|
|
:select-size="10"
|
|
></b-form-select>
|
|
</b-col>
|
|
</b-row>
|
|
<b-row class='mt-2'>
|
|
<b-col>
|
|
<b-button variant='danger' @click="cohortDisassociate()"
|
|
><i class='fa fa-chain-broken'></i> {{text.delete_association}}</b-button>
|
|
</b-col>
|
|
<b-col>
|
|
<b-button variant='success' @click="cohortAssociate()"
|
|
><i class='fa fa-link'></i> {{text.add_association}}</b-button>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-tab>
|
|
<b-tab :title="text.users">
|
|
<b-container>
|
|
<b-row class='mb-2 mt-2'>
|
|
<b-col>{{text.associated_users}}</b-col>
|
|
<b-col>{{text.associate_users}}</b-col>
|
|
</b-row>
|
|
<b-row class='mb-2'>
|
|
<b-col>
|
|
</b-col>
|
|
<b-col>
|
|
<b-form-input
|
|
type="text"
|
|
@input="searchUsers($event)"
|
|
placeholder="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>
|
|
</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> {{text.delete_association}}</b-button>
|
|
</b-col>
|
|
<b-col>
|
|
<b-button variant='success' @click="userAssociate()"
|
|
><i class='fa fa-link'></i> {{text.add_association}}</b-button>
|
|
</b-col>
|
|
</b-row>
|
|
</b-container>
|
|
</b-tab>
|
|
</b-tabs>
|
|
</b-modal>
|
|
</span>
|
|
`
|
|
});
|
|
|
|
/*
|
|
* T-STUDYLINE
|
|
*/
|
|
Vue.component('t-studyline', {
|
|
props: ['color','name','code', 'slots','deletable','editable','sequence','numlines'],
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
computed: {
|
|
|
|
},
|
|
methods: {
|
|
onEdit() {
|
|
this.$emit('edit',this.value);
|
|
},
|
|
onDelete() {
|
|
this.$emit('delete',this.value);
|
|
},
|
|
|
|
},
|
|
template: `
|
|
<div :class="'t-studyline ' + (!editable?((sequence%2)?'odd':'even'):'') +
|
|
(sequence==0?' first':'') + (sequence==numlines-1?' last':'')">
|
|
<div class="t-studyline-handle" :style="'background-color: ' + color"></div>
|
|
<div class="t-studyline-title">
|
|
<div>
|
|
<slot name='movebox'>
|
|
</slot>
|
|
<abbr v-b-tooltip.hover :title="name">{{ code }}</abbr>
|
|
<span class='control editable' v-if='!editable'>
|
|
<a href='#' @click='onEdit'><i class='fa fa-pencil'></i></a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="editable" class='t-studyline-editmode-content'>
|
|
<slot></slot>
|
|
</div>
|
|
<slot v-else></slot>
|
|
<div class='controlbox' 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'>
|
|
<a v-if='deletable' href='#' @click='onDelete'><i class='text-danger fa fa-trash'></i></a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-studyline-slot', {
|
|
props: {
|
|
type : {
|
|
type: String,
|
|
default: 'gradable',
|
|
},
|
|
slotindex : {
|
|
type: Number,
|
|
default: '',
|
|
},
|
|
lineid : {
|
|
type: Number,
|
|
default: '',
|
|
},
|
|
value: {
|
|
type: Array,
|
|
default(){ return [];},
|
|
},
|
|
plan: {
|
|
type: Object,
|
|
default(){ return null;},
|
|
}
|
|
},
|
|
computed: {
|
|
listtype() {
|
|
return this.type;
|
|
},
|
|
dragacceptlist(){
|
|
if(this.type == "gradable"){
|
|
return ["course","competency","gradable-item"];
|
|
} else {
|
|
return ["filter", "filter-item"];
|
|
}
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
methods: {
|
|
dragacceptitem(){
|
|
if(this.type == "gradable"){
|
|
return ["gradable-item"];
|
|
} else {
|
|
return ["filter-item"];
|
|
}
|
|
},
|
|
dragacceptcomponent(){
|
|
if(this.type == "gradable"){
|
|
return ["course","competency",];
|
|
} else {
|
|
return ["filter",];
|
|
}
|
|
},
|
|
onInsert(event) {
|
|
const self = this;
|
|
if(self.dragacceptitem().includes(event.type)) {
|
|
let item = event.data;
|
|
self.value.splice( event.index,0, item);
|
|
self.afterReorder(self.value).done(function(){
|
|
self.$emit("input",self.value);
|
|
});
|
|
}
|
|
else if(self.dragacceptcomponent().includes(event.type) ){
|
|
if(event.type == "competency"){
|
|
call([{
|
|
methodname: 'local_treestudyplan_add_studyitem',
|
|
args: {
|
|
"line_id": self.lineid,
|
|
"slot" : self.slotindex,
|
|
"type": 'competency',
|
|
"details": {
|
|
"competency_id": event.data.id,
|
|
'conditions':'',
|
|
'course_id':null,
|
|
'badge_id':null,
|
|
'continuation_id':null,
|
|
}
|
|
}
|
|
}])[0].done((response) => {
|
|
console.info('Add item response:', response);
|
|
let item = response;
|
|
self.value.splice(event.index, 0, item);
|
|
self.afterReorder(self.value).done(function () {
|
|
self.$emit("input", self.value);
|
|
});
|
|
}).fail(notification.exception);
|
|
}
|
|
else if(event.type == "course"){
|
|
call([{
|
|
methodname: 'local_treestudyplan_add_studyitem',
|
|
args: {
|
|
"line_id": self.lineid,
|
|
"slot" : self.slotindex,
|
|
"type": 'course',
|
|
"details": {
|
|
"competency_id": null,
|
|
'conditions':'',
|
|
'course_id':event.data.id,
|
|
'badge_id':null,
|
|
'continuation_id':null,
|
|
}
|
|
}
|
|
}])[0].done((response) => {
|
|
console.info('Add item response:', response);
|
|
let item = response;
|
|
self.value.splice(event.index, 0, item);
|
|
self.afterReorder(self.value).done(function () {
|
|
self.$emit("input", self.value);
|
|
});
|
|
}).fail(notification.exception);
|
|
}
|
|
else if(event.type == "filter") {
|
|
call([{
|
|
methodname: 'local_treestudyplan_add_studyitem',
|
|
args: {
|
|
"line_id": self.lineid,
|
|
"slot" : self.slotindex,
|
|
"type": event.data.type,
|
|
"details":{
|
|
"badge_id": event.data.badge?event.data.badge.id:undefined,
|
|
}
|
|
}
|
|
}])[0].done((response) => {
|
|
console.info('Add item response:', response);
|
|
let item = response;
|
|
self.value.splice(event.index, 0, item);
|
|
self.afterReorder(self.value).done(function () {
|
|
self.$emit("input", self.value);
|
|
});
|
|
}).fail(notification.exception);
|
|
}
|
|
}
|
|
},
|
|
onCut(event) {
|
|
const self=this;
|
|
let id = event.data.id;
|
|
|
|
for(let i = 0; i < self.value.length; i++){
|
|
if(self.value[i].id == id){
|
|
self.value.splice(i, 1); i--;
|
|
break; // just remove one
|
|
}
|
|
}
|
|
this.afterReorder(self.value);
|
|
this.$emit("input",this.value);
|
|
},
|
|
onReorder(event) {
|
|
const self=this;
|
|
// apply list first
|
|
event.apply(self.value);
|
|
this.afterReorder(self.value);
|
|
},
|
|
afterReorder() {
|
|
const self=this;
|
|
// send the new order to the server
|
|
let items = [];
|
|
for(let idx in self.value)
|
|
{
|
|
self.value[idx].layer = idx;
|
|
items.push({'id': self.value[idx].id,'layer': idx, 'slot': this.slotindex, 'line_id': this.lineid});
|
|
}
|
|
return call([{
|
|
methodname: 'local_treestudyplan_reorder_studyitems',
|
|
args: { 'items': items }
|
|
}])[0].fail(notification.exception);
|
|
},
|
|
feedbackDummy(type,data){
|
|
let item = {};
|
|
item[type] = data;
|
|
return item;
|
|
}
|
|
},
|
|
template: `
|
|
<div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex">
|
|
<drop-list
|
|
:items="value"
|
|
:class="'t-slot-droplist '+type"
|
|
:accepts-type="dragacceptlist"
|
|
@insert="onInsert"
|
|
@reorder="onReorder"
|
|
mode="cut"
|
|
row
|
|
>
|
|
<template v-slot:item="{item}">
|
|
<drag
|
|
:key="item.id"
|
|
class="t-slot-item"
|
|
:data="item"
|
|
:type="type+'-item'"
|
|
@cut="onCut"><t-item v-model="item" :plan="plan"></t-item></drag>
|
|
</template>
|
|
<template v-slot:feedback="{data,type}">
|
|
<div v-if="type == listtype+'-item'"
|
|
class="t-slot-item feedback"
|
|
:key="data.id"><t-item v-model="data" dummy></t-item></div>
|
|
<div v-else-if="type == 'competency'"
|
|
class="t-slot-item feedback"
|
|
:key="'competency-'+data.idnumber"><t-item-competency v-model="data"></t-item-competency></div>
|
|
<div v-else-if="type == 'course'"
|
|
class="t-slot-item feedback"
|
|
:key="'course-;'+data.id"><t-item-course v-model="feedbackDummy('course',data)"></t-item-course></div>
|
|
<div v-else-if="type == 'filter'"
|
|
class="t-slot-item feedback"
|
|
key="tooldrop">
|
|
<t-item-junction v-if="data.type == 'junction'" ></t-item-junction>
|
|
<t-item-start v-else-if="data.type == 'start'" ></t-item-start>
|
|
<t-item-finish v-else-if="data.type == 'finish'" ></t-item-finish>
|
|
<t-item-badge v-else-if="data.type == 'badge'" ></t-item-badge>
|
|
</div>
|
|
<div v-else
|
|
class="t-slot-item feedback"
|
|
:key="type">--{{ type }}--</div>
|
|
</template>
|
|
</drop-list>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
|
|
|
|
Vue.component('t-item', {
|
|
props: {
|
|
'value' :{
|
|
type: Object,
|
|
default(){ return null;},
|
|
},
|
|
'dummy' :{
|
|
type: Boolean,
|
|
default() { return false;},
|
|
},
|
|
'plan': {
|
|
type: Object,
|
|
default() { return null;},
|
|
},
|
|
},
|
|
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 LeaderLine(start,dragelement,{
|
|
color: '#777',
|
|
positionByWindowResize: false,
|
|
startSocket: 'right',
|
|
endSocket: 'left',
|
|
startSocketGravity: LineGravity,
|
|
endSocketGravity: LineGravity,
|
|
});
|
|
// Add separate event listener to reposition mouse move
|
|
document.addEventListener("mousemove",this.onMouseMove);
|
|
},
|
|
dragEnd(){
|
|
if(this.dragLine !== null) {
|
|
this.dragLine.remove();
|
|
}
|
|
let dragelement = document.getElementById('t-item-cdrag-'+this.value.id);
|
|
dragelement.style.removeProperty('left');
|
|
dragelement.style.removeProperty('top');
|
|
dragelement.style.removeProperty('position');
|
|
document.removeEventListener("mousemove",this.onMouseMove);
|
|
},
|
|
onMouseMove: debounce(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';
|
|
this.dragLine.position();
|
|
},5),
|
|
onDrop(event){
|
|
let from_id = event.data.id;
|
|
let to_id = this.value.id;
|
|
|
|
call([{
|
|
methodname: 'local_treestudyplan_connect_studyitems',
|
|
args: { 'from_id': from_id, 'to_id': to_id }
|
|
}])[0].done((result)=>{
|
|
console.info("Drop result",result);
|
|
let conn = {'id': result.id, 'from_id': result.from_id, 'to_id': result.to_id};
|
|
ItemEventBus.$emit("createdConnection",conn);
|
|
this.value.connections.in.push(conn);
|
|
}).fail(notification.exception);
|
|
},
|
|
redrawLine(conn){
|
|
let lineColor = "#383";
|
|
|
|
// prepare lineinfo link or delete old line
|
|
let lineinfo = this.lines[conn.to_id];
|
|
if(lineinfo){
|
|
if(lineinfo.line){
|
|
if(lineinfo.lineElm ){
|
|
lineinfo.lineElm.parentNode.removeChild(lineinfo.lineElm);
|
|
lineinfo.lineElm = undefined;
|
|
} else {
|
|
lineinfo.line.remove();
|
|
}
|
|
lineinfo.line = undefined;
|
|
}
|
|
} else {
|
|
lineinfo = {};
|
|
this.lines[conn.to_id] = lineinfo;
|
|
}
|
|
// draw new line
|
|
let start = document.getElementById('studyitem-'+conn.from_id);
|
|
let end= document.getElementById('studyitem-'+conn.to_id);
|
|
LeaderLine.positionByWindowResize = false;
|
|
if(start !== null && end !== null && isVisible(start) && isVisible(end)){
|
|
lineinfo.line = new LeaderLine(start,end,{
|
|
color: lineColor,
|
|
startSocket: 'right',
|
|
endSocket: 'left',
|
|
startSocketGravity: LineGravity,
|
|
endSocketGravity: LineGravity,
|
|
});
|
|
|
|
let elmWrapper = (this.plan.id >=0)?document.getElementById('studyplan-linewrapper-'+this.plan.id):null;
|
|
if(elmWrapper !== null){
|
|
let elmLine = document.querySelector('body > .leader-line:last-child');
|
|
elmWrapper.appendChild(elmLine);
|
|
lineinfo.lineElm = elmLine; // store line element so it can more easily be removed from the dom
|
|
}
|
|
setTimeout(function(){
|
|
if(lineinfo.line !== undefined){
|
|
lineinfo.line.position();
|
|
}
|
|
},1);
|
|
}
|
|
},
|
|
deleteLine(conn){
|
|
const self = this;
|
|
// console.info("Delete Line",conn);
|
|
call([{
|
|
methodname: 'local_treestudyplan_disconnect_studyitems',
|
|
args: { 'from_id': conn.from_id, 'to_id': conn.to_id }
|
|
}])[0].done((result)=>{
|
|
if(result.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);
|
|
}
|
|
}).fail(notification.exception);
|
|
},
|
|
highlight(conn){
|
|
let lineinfo = this.lines[conn.to_id];
|
|
if(lineinfo && lineinfo.line){
|
|
lineinfo.line.setOptions({color:"#f33",});
|
|
}
|
|
},
|
|
normalize(conn){
|
|
let lineinfo = this.lines[conn.to_id];
|
|
if(lineinfo && lineinfo.line){
|
|
lineinfo.line.setOptions({color:"#383",});
|
|
}
|
|
},
|
|
updateItem() {
|
|
call([{
|
|
methodname: 'local_treestudyplan_edit_studyitem',
|
|
args: { 'id': this.value.id,
|
|
'conditions': this.value.conditions,
|
|
'continuation_id': this.value.continuation_id,}
|
|
}])[0].fail(notification.exception);
|
|
},
|
|
doShowContext(event) {
|
|
if(this.hasContext){
|
|
this.showContext=true;
|
|
event.preventDefault();
|
|
}
|
|
},
|
|
redrawLines(){
|
|
for(let i in this.value.connections.out){
|
|
let conn = this.value.connections.out[i];
|
|
// console.info('Connection out', conn);
|
|
this.redrawLine(conn);
|
|
}
|
|
},
|
|
|
|
// EVENT LISTENERS
|
|
onCreatedConnection(conn){
|
|
if(conn.from_id == this.value.id){
|
|
// console.info("incomingConnection",conn);
|
|
this.value.connections.out.push(conn);
|
|
this.redrawLine(conn);
|
|
}
|
|
},
|
|
// Listener for the signal that a connection was removed by the outgoing item
|
|
onRemovedConnection(conn){
|
|
for(let i in this.value.connections.in){
|
|
let c_in = this.value.connections.in[i];
|
|
if(conn.id == c_in.id){
|
|
// console.info("Deleting incoming connection",conn);
|
|
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(){
|
|
for(let i in this.value.connections.out){
|
|
let conn = this.value.connections.out[i];
|
|
//if(conn.to_id == re_id){
|
|
this.redrawLine(conn);
|
|
//}
|
|
}
|
|
},
|
|
// When an item is disPositioned - (temporarily) removed from the list,
|
|
// all connections need to be deleted.
|
|
onDisPositioned(re_id){
|
|
for(let i in this.value.connections.out){
|
|
let conn = this.value.connections.out[i];
|
|
if(conn.to_id == re_id){
|
|
this.removeLine(conn);
|
|
} else {
|
|
this.redrawLine(conn);
|
|
}
|
|
}
|
|
},
|
|
|
|
// When an item is deleted
|
|
// all connections to/from that item need to be cleaned up
|
|
onItemDeleted(item_id){
|
|
const self = this;
|
|
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);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
|
|
onRedrawLines(){
|
|
this.redrawLines();
|
|
},
|
|
|
|
removeLine(conn){
|
|
let lineinfo = this.lines[conn.to_id];
|
|
if(lineinfo){
|
|
if(lineinfo.line){
|
|
if(lineinfo.lineElm ){
|
|
lineinfo.lineElm.parentNode.removeChild(lineinfo.lineElm);
|
|
lineinfo.lineElm = undefined;
|
|
} else {
|
|
lineinfo.line.remove();
|
|
}
|
|
lineinfo.line = undefined;
|
|
}
|
|
}
|
|
},
|
|
|
|
},
|
|
computed: {
|
|
hasConnectionsOut() {
|
|
return !(["finish",].includes(this.value.type));
|
|
},
|
|
hasConnectionsIn() {
|
|
return !(["start",].includes(this.value.type));
|
|
},
|
|
hasContext() {
|
|
return ['junction','finish'].includes(this.value.type);
|
|
}
|
|
},
|
|
created(){
|
|
// Add event listeners on the message bus
|
|
// But only if not in "dummy" mode - mode which is used for droplist placeholders
|
|
// Since an item is "fully made" with all references, not specifying dummy mode really messes things up
|
|
if(!this.dummy){
|
|
|
|
// Listener for the signal that a new connection was made and needs to be drawn
|
|
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
|
|
ItemEventBus.$on('createdConnection', this.onCreatedConnection);
|
|
// Listener for the signal that a connection was removed by the outgoing item
|
|
ItemEventBus.$on('removedConnection', this.onRemovedConnection);
|
|
// Listener for reposition events
|
|
// When an item in the list is repositioned, all lines need to be redrawn
|
|
ItemEventBus.$on('rePositioned', this.onRePositioned);
|
|
// When an item is disPositioned - (temporarily) removed from the list,
|
|
// all connections need to be deleted.
|
|
ItemEventBus.$on('disPositioned', this.onDisPositioned);
|
|
// When an item is deleted
|
|
// all connections to/from that item need to be cleaned up
|
|
ItemEventBus.$on('itemDeleted', this.onItemDeleted);
|
|
ItemEventBus.$on('redrawLines', this.onRedrawLines);
|
|
|
|
|
|
}
|
|
},
|
|
mounted(){
|
|
// Initialize connection lines when mounting
|
|
// But only if not in "dummy" mode - mode which is used for droplist placeholders
|
|
// Since an item is "fully made" with all references, not specifying dummy mode really messes things up
|
|
|
|
if(!this.dummy)
|
|
{
|
|
// console.info('Mounted', this);
|
|
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">
|
|
<t-item-competency v-model="value" v-if="value.type == 'competency'" ></t-item-competency>
|
|
<t-item-course v-model="value" v-if="value.type == 'course'"
|
|
:plan='plan' ></t-item-course>
|
|
<t-item-junction v-model="value" v-if="value.type == 'junction'" ></t-item-junction>
|
|
<t-item-start v-model="value" v-if="value.type == 'start'" ></t-item-start>
|
|
<t-item-finish v-model="value" v-if="value.type == 'finish'" ></t-item-finish>
|
|
<t-item-badge v-model="value" v-if="value.type == 'badge'" ></t-item-badge>
|
|
<t-item-invalid v-model="value" v-if="value.type == 'invalid'" ></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='t-item-contextview'
|
|
:id="'t-item-config-'+value.id"
|
|
:title="text['item_configuration']"
|
|
scrollable
|
|
ok-only
|
|
>
|
|
<b-form-group
|
|
:label="text.select_conditions"
|
|
>
|
|
<b-form-select size="sm"
|
|
@input="updateItem"
|
|
v-model="value.conditions"
|
|
:options="condition_options"
|
|
></b-form-select>
|
|
</b-form-group>
|
|
</b-modal>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-invalid', {
|
|
props: {
|
|
'value' :{
|
|
type: Object,
|
|
default: function(){ return null;},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
text: strings.invalid,
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<b-card no-body class="t-item-invalid">
|
|
<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}}
|
|
</b-card-body>
|
|
</b-col>
|
|
</b-row>
|
|
</b-card>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-competency', {
|
|
props: {
|
|
'value' :{
|
|
type: Object,
|
|
default: function(){ return null;},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
dragLine: null,
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<b-card no-body class="t-item-competency">
|
|
<b-card-body>
|
|
<a :id="'t-item-competency-details-'+value.id" href="#"
|
|
><i class="fa fa-chevron-down">{{ value.competency.shortname }}</i> </a>
|
|
</b-card-body>
|
|
<b-popover
|
|
placement="bottom"
|
|
:target="'t-item-competency-details-'+value.id"
|
|
:title="value.competency.shortname+' - ' + value.competency.idnumber"
|
|
triggers="click">
|
|
<ul class="t-item-module-children">
|
|
<li v-for="g in value.competency.children">{{g.shortname}}</li>
|
|
</ul>
|
|
</b-popover>
|
|
</b-card>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-course', {
|
|
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: {
|
|
useItemConditions() {
|
|
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useItemConditions !== undefined){
|
|
return this.plan.aggregation_info.useItemConditions;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
configurationState(){
|
|
if(this.hasGrades() || this.hasCompletions()) {
|
|
return "t-configured-ok";
|
|
} else {
|
|
return "t-configured-alert";
|
|
}
|
|
},
|
|
|
|
configurationIcon(){
|
|
if(this.hasGrades() || this.hasCompletions()) {
|
|
return "check";
|
|
} else {
|
|
return "exclamation-circle";
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
hasGrades() {
|
|
if(this.value.course.grades && this.value.course.grades.length > 0){
|
|
for(const g of this.value.course.grades){
|
|
if(g.selected){
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
hasCompletions() {
|
|
if(this.value.course.completion && this.value.course.completion.conditions) {
|
|
for(const cgroup of this.value.course.completion.conditions){
|
|
if(cgroup.items && cgroup.items.length > 0){
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
includeChanged(newValue,g){
|
|
call([{
|
|
methodname: 'local_treestudyplan_include_grade',
|
|
args: { 'grade_id': g.id,
|
|
'item_id': this.value.id,
|
|
'include': newValue,
|
|
'required': g.required,
|
|
}
|
|
}])[0].fail(notification.exception);
|
|
},
|
|
requiredChanged(newValue,g){
|
|
call([{
|
|
methodname: 'local_treestudyplan_include_grade',
|
|
args: { 'grade_id': g.id,
|
|
'item_id': this.value.id,
|
|
'include': g.selected,
|
|
'required': newValue,
|
|
}
|
|
}])[0].fail(notification.exception);
|
|
},
|
|
updateConditions() {
|
|
call([{
|
|
methodname: 'local_treestudyplan_edit_studyitem',
|
|
args: { 'id': this.value.id,
|
|
'conditions': this.value.conditions,
|
|
}
|
|
}])[0].fail(notification.exception);
|
|
},
|
|
},
|
|
created() {
|
|
|
|
},
|
|
template: `
|
|
<b-card no-body class="t-item-course">
|
|
<b-row no-gutters>
|
|
<b-col md="1">
|
|
<span
|
|
:title="text['coursetiming_'+value.course.timing]"
|
|
v-b-popover.hover.top="value.course.startdate+' - '+value.course.enddate"
|
|
:class="'t-timing-indicator timing-'+value.course.timing"></span>
|
|
</b-col>
|
|
<b-col md="11">
|
|
<b-card-body class="align-items-center">
|
|
<a class="t-item-course-config"
|
|
v-b-modal="'t-item-course-config-'+value.id"
|
|
href="#" @click.prevent=""
|
|
><i :class="'fa fa-'+configurationIcon+' ' + configurationState"></i></a>
|
|
<a v-b-modal="'t-item-course-config-'+value.id"
|
|
: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>
|
|
</b-col>
|
|
</b-row>
|
|
<b-modal
|
|
:id="'t-item-course-config-'+value.id"
|
|
:title="value.course.displayname + ' - ' + value.course.fullname"
|
|
ok-only
|
|
size="lg"
|
|
scrollable
|
|
>
|
|
<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>
|
|
<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>
|
|
{{ value.course.startdate }} - {{ value.course.enddate }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<t-item-course-grades
|
|
v-if='!!value.course.grades && value.course.grades.length > 0'
|
|
v-model='value' :plan="plan"
|
|
></t-item-course-grades>
|
|
<t-item-course-completion
|
|
v-if='!!value.course.completion'
|
|
v-model='value.course.completion'
|
|
:course='value.course'
|
|
></t-item-course-completion>
|
|
</b-modal>
|
|
</b-card>
|
|
`,
|
|
});
|
|
|
|
|
|
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;
|
|
},
|
|
},
|
|
methods: {
|
|
includeChanged(newValue,g){
|
|
call([{
|
|
methodname: 'local_treestudyplan_include_grade',
|
|
args: { 'grade_id': g.id,
|
|
'item_id': this.value.id,
|
|
'include': newValue,
|
|
'required': g.required,
|
|
}
|
|
}])[0].fail(notification.exception);
|
|
},
|
|
requiredChanged(newValue,g){
|
|
call([{
|
|
methodname: 'local_treestudyplan_include_grade',
|
|
args: { 'grade_id': g.id,
|
|
'item_id': this.value.id,
|
|
'include': g.selected,
|
|
'required': newValue,
|
|
}
|
|
}])[0].fail(notification.exception);
|
|
},
|
|
},
|
|
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>
|
|
<s-edit-mod
|
|
:title="value.course.fullname"
|
|
@saved="(fd) => g.name = fd.get('name')"
|
|
v-if="g.cmid > 0"
|
|
:cmid="g.cmid"
|
|
:coursectxid="value.course.ctxid"
|
|
genericonly>{{g.name}}</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";
|
|
}
|
|
},
|
|
|
|
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>
|
|
`,
|
|
});
|
|
|
|
|
|
/************************************
|
|
* *
|
|
* Competency map Vue components *
|
|
* *
|
|
************************************/
|
|
Vue.component('t-competency-heading', {
|
|
props: {
|
|
value : {
|
|
type: Object,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
computed: {
|
|
inuse() {
|
|
return (this.value.inuse !== undefined && !!this.value.inuse);
|
|
}
|
|
},
|
|
methods: {
|
|
onCut(){
|
|
// console.info('cutevent-competency',event);
|
|
this.value.inuse=true;
|
|
this.$emit('input',this.value);
|
|
},
|
|
},
|
|
template: `
|
|
<span class="t-competency-heading">
|
|
<i :class="'t-'+value.type+' fa fa-puzzle-piece'" v-if="value.type == 'module'"></i>
|
|
<i :class="'t-'+value.type+' fa fa-map'" v-else-if="value.type == 'category'"></i>
|
|
<i :class="'t-'+value.type+' fa fa-check-square'" v-else-if="value.type == 'goal'"></i>
|
|
<i :class="'t-'+value.type+' fa fa-circle'" v-else ></i>
|
|
<drag
|
|
class="draggable-competency"
|
|
v-if="value.type == 'module' && !inuse"
|
|
:data="value"
|
|
type="competency"
|
|
@cut="onCut">
|
|
{{ value.shortname }}
|
|
</drag>
|
|
<span v-else-if="value.type == 'module'" class="disabled-competency">{{ value.shortname }}</span>
|
|
<span v-else class="competency-info">{{ value.shortname }}</span>
|
|
</span>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-competency-display', {
|
|
props: {
|
|
value : {
|
|
type: Object,
|
|
default: function(){ return {};},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
dragLine: null,
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
computed: {
|
|
haschildren() {
|
|
return this.value.children && this.value.children.length > 0;
|
|
},
|
|
},
|
|
template: `
|
|
<li>
|
|
<span v-if="haschildren" v-b-toggle="'cmp-'+value.id">
|
|
<i class="when-closed fa fa-caret-right"></i>
|
|
<i class="when-open fa fa-caret-down"></i>
|
|
<t-competency-heading v-model="value"></t-competency-heading>
|
|
</span>
|
|
<span v-else>
|
|
<t-competency-heading v-model="value"></t-competency-heading>
|
|
</span>
|
|
<b-collapse v-if="haschildren" :id="'cmp-'+value.id">
|
|
<t-competency-list v-model="value.children"></t-competency-list>
|
|
</b-collapse>
|
|
</li>
|
|
`,
|
|
});
|
|
Vue.component('t-competency-list', {
|
|
props: {
|
|
value : {
|
|
type: Array,
|
|
default: function(){ return [];},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<ul class='t-competency-list'>
|
|
<t-competency-display
|
|
v-for="(child, index) in value"
|
|
:key='child.idnumber'
|
|
v-model="value[index]"
|
|
></t-competency-display>
|
|
</ul>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-junction',{
|
|
props: {
|
|
value : {
|
|
type: Object,
|
|
default: function(){ return {};},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
condition_options: string_keys.conditions,
|
|
};
|
|
},
|
|
methods: {
|
|
|
|
},
|
|
template: `
|
|
<div class='t-item-junction t-item-filter'>
|
|
<i class="fa fa-check-circle"></i>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-finish',{
|
|
props: {
|
|
value : {
|
|
type: Object,
|
|
default: function(){ return {};},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<div class='t-item-finish t-item-filter'>
|
|
<i class="fa fa-stop-circle"></i>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-start',{
|
|
props: {
|
|
value : {
|
|
type: Object,
|
|
default: function(){ return {};},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
created(){
|
|
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<div class='t-item-start t-item-filter'>
|
|
<i class="fa fa-play-circle"></i>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-item-badge',{
|
|
props: {
|
|
value : {
|
|
type: Object,
|
|
default: function(){ return { badge: {}};},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
txt: strings,
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<div class='t-item-badge t-item-filter' v-b-tooltip.hover :title="value.badge.name">
|
|
<svg class="t-badge-backdrop " width='50px' height='50px' viewBox="0 0 100 100">
|
|
<title>{{value.badge.name}}</title>
|
|
<circle cx="50" cy="50" r="46"
|
|
style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/>
|
|
<image class="badge-image" clip-path="circle() fill-box"
|
|
:href="value.badge.imageurl" x="12" y="12" width="76" height="76"/>
|
|
</svg>
|
|
<a class="t-item-config badge"
|
|
v-b-modal="'t-item-badge-details-'+value.id" href="#" @click.prevent=""><i class="fa fa-gear"></i></a>
|
|
<b-modal
|
|
:id="'t-item-badge-details-'+value.id"
|
|
:title="value.badge.name"
|
|
size="lg"
|
|
ok-only
|
|
centered
|
|
scrollable
|
|
>
|
|
<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>
|
|
</b-modal>
|
|
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-coursecat-list',{
|
|
props: {
|
|
value : {
|
|
type: Array,
|
|
default: function(){ return {};},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
};
|
|
},
|
|
methods: {
|
|
},
|
|
template: `
|
|
<ul class="t-coursecat-list">
|
|
<t-coursecat-list-item
|
|
v-for="coursecat,idx in value"
|
|
v-model="value[idx]"
|
|
:key="coursecat.id"></t-coursecat-list-item>
|
|
</ul>
|
|
`,
|
|
});
|
|
|
|
Vue.component('t-coursecat-list-item',{
|
|
props: {
|
|
value : {
|
|
type: Object,
|
|
default: 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}
|
|
}])[0].done(function(response){
|
|
debug.info("Course info:",response);
|
|
self.$emit('input', response);
|
|
}).fail(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>
|
|
<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: {
|
|
},
|
|
template: `
|
|
<ul class="t-course-list">
|
|
<li class="t-course-list-item" v-for="course in value" :key="course.id">
|
|
<drag
|
|
class="draggable-course"
|
|
:data="course"
|
|
type="course"
|
|
@cut=""
|
|
>
|
|
<i class="t-course-list-item fa fa-book"></i> {{ course.shortname }} - {{ course.fullname }}
|
|
</drag>
|
|
</li>
|
|
</ul>
|
|
`,
|
|
});
|
|
|
|
},
|
|
};
|