2024-02-18 13:47:08 +01:00
|
|
|
/*eslint no-var: "error"*/
|
|
|
|
/*eslint no-console: "off"*/
|
|
|
|
/*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
|
|
|
|
|
|
|
|
import {SimpleLine} from './simpleline/simpleline';
|
|
|
|
import {get_strings} from 'core/str';
|
|
|
|
import {load_strings} from './util/string-helper';
|
|
|
|
import {format_date,studyplanPageTiming,studyplanTiming} from './util/date-helper';
|
|
|
|
import {call} from 'core/ajax';
|
|
|
|
import notification from 'core/notification';
|
|
|
|
import {svgarcpath} from './util/svgarc';
|
|
|
|
import Debugger from './util/debugger';
|
|
|
|
import Config from 'core/config';
|
|
|
|
import {ProcessStudyplan, ProcessStudyplanPage, objCopy} from './studyplan-processor';
|
|
|
|
import TSComponents from './treestudyplan-components';
|
|
|
|
import {eventTypes as editSwEventTypes} from 'core/edit_switch';
|
|
|
|
|
|
|
|
// Make π available as a constant
|
|
|
|
const π = Math.PI;
|
|
|
|
// Gravity value for arrow lines - determines how much a line is pulled in the direction of the start/end before changing direction
|
|
|
|
const LINE_GRAVITY = 1.3;
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
install(Vue/*,options*/){
|
|
|
|
Vue.use(TSComponents);
|
|
|
|
let debug = new Debugger("treestudyplan-viewer");
|
|
|
|
|
|
|
|
let strings = load_strings({
|
|
|
|
report: {
|
|
|
|
loading: "loadinghelp@core",
|
|
|
|
studyplan_past: "studyplan_past",
|
|
|
|
studyplan_present: "studyplan_present",
|
|
|
|
studyplan_future: "studyplan_future",
|
|
|
|
back: "back",
|
|
|
|
},
|
2024-02-18 23:27:57 +01:00
|
|
|
|
2024-02-18 13:47:08 +01:00
|
|
|
invalid: {
|
|
|
|
error: 'error',
|
|
|
|
},
|
2024-02-18 23:27:57 +01:00
|
|
|
header: {
|
|
|
|
overall: 'overall',
|
|
|
|
students: 'students@core'
|
|
|
|
},
|
|
|
|
studentresults: {
|
2024-02-18 13:47:08 +01:00
|
|
|
completion_incomplete: "completion_incomplete",
|
|
|
|
completion_failed: "completion_failed",
|
|
|
|
completion_pending: "completion_pending",
|
|
|
|
completion_progress: "completion_progress",
|
|
|
|
completion_completed: "completion_completed",
|
|
|
|
completion_good: "completion_good",
|
|
|
|
completion_excellent: "completion_excellent",
|
|
|
|
student_not_tracked: "student_not_tracked",
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/************************************
|
|
|
|
* *
|
|
|
|
* Treestudyplan Viewer components *
|
|
|
|
* *
|
|
|
|
************************************/
|
|
|
|
|
|
|
|
Vue.component('q-studyplanreport', {
|
|
|
|
props: {
|
|
|
|
structure: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
students: [],
|
|
|
|
studentresults: {},
|
2024-02-18 23:27:57 +01:00
|
|
|
expansioninfo: {
|
|
|
|
periods: {},
|
|
|
|
lines: {},
|
|
|
|
items: {},
|
|
|
|
},
|
|
|
|
groupinfo: {},
|
2024-02-18 13:47:08 +01:00
|
|
|
|
|
|
|
sorting: {
|
|
|
|
name: "asc",
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
created() {
|
|
|
|
this.loadStudents();
|
|
|
|
},
|
|
|
|
computed: {
|
2024-02-18 23:27:57 +01:00
|
|
|
sortedstudents(){
|
|
|
|
return this.students;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
watch:{
|
|
|
|
structure: {
|
|
|
|
immediate: true,
|
|
|
|
handler (structure) {
|
|
|
|
// (Re)build expansion info structure
|
|
|
|
for (const period of structure.periods) {
|
|
|
|
const pid = period.period.id;
|
|
|
|
if (!this.expansioninfo.periods[pid]) {
|
|
|
|
// Use this.$set to make sure the properties are reactive.
|
|
|
|
this.$set(
|
|
|
|
this.expansioninfo.periods,
|
|
|
|
pid,
|
|
|
|
{
|
|
|
|
expanded: false,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
this.$set(
|
|
|
|
this.expansioninfo.lines,
|
|
|
|
period.period.id,
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
for (const line of period.lines) {
|
|
|
|
const lid = line.line.id;
|
|
|
|
if (!this.expansioninfo.lines[lid]) {
|
|
|
|
// Use this.$set to make sure the properties are reactive.
|
|
|
|
this.$set(
|
|
|
|
this.expansioninfo.lines[pid],
|
|
|
|
lid,
|
|
|
|
{
|
|
|
|
expanded: true,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
for (const item of line.items) {
|
|
|
|
if (!this.expansioninfo.items[item.id]) {
|
|
|
|
// Use this.$set to make sure the properties are reactive.
|
|
|
|
this.$set(
|
|
|
|
this.expansioninfo.items,
|
|
|
|
item.id,
|
|
|
|
{
|
|
|
|
expanded: false,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-02-18 13:47:08 +01:00
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
loadStudents() {
|
|
|
|
const self = this;
|
|
|
|
call([{
|
|
|
|
methodname: 'local_treestudyplan_all_associated_grouped',
|
|
|
|
args: { studyplan_id: this.structure.studyplan.id}
|
|
|
|
}])[0].then(function(response){
|
|
|
|
self.students = response;
|
|
|
|
for(const group of self.students) {
|
2024-02-18 23:27:57 +01:00
|
|
|
self.$set(
|
|
|
|
self.groupinfo,
|
|
|
|
group.label,
|
|
|
|
{
|
|
|
|
expand: true,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2024-02-18 13:47:08 +01:00
|
|
|
for(const student of group.users){
|
2024-02-19 11:34:40 +01:00
|
|
|
self.$set(
|
|
|
|
self.studentresults,
|
|
|
|
student.id,
|
|
|
|
{
|
|
|
|
loading: true,
|
|
|
|
results: [],
|
|
|
|
}
|
|
|
|
);
|
2024-02-18 13:47:08 +01:00
|
|
|
call([{
|
|
|
|
methodname: 'local_treestudyplan_get_report_data',
|
|
|
|
args: { pageid: self.structure.page.id,
|
|
|
|
userid: student.id,
|
|
|
|
firstperiod: self.structure.firstperiod,
|
|
|
|
lastperiod: self.structure.lastperiod,
|
|
|
|
}
|
|
|
|
}])[0].then(function(response){
|
|
|
|
self.studentresults[student.id].loading = false;
|
|
|
|
self.studentresults[student.id].results = response;
|
|
|
|
}).catch(notification.exception);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}).catch(notification.exception);
|
2024-02-18 23:27:57 +01:00
|
|
|
},
|
|
|
|
expansionChanged(parm, id, val) {
|
|
|
|
if(parm[0] == 'p') {
|
|
|
|
parm = 'periods';
|
|
|
|
} else if(parm[0] == 'l') {
|
|
|
|
parm = 'lines';
|
|
|
|
} else {
|
|
|
|
parm = 'items';
|
|
|
|
}
|
|
|
|
debug.info('Expansion Changed',parm,id,val);
|
|
|
|
|
|
|
|
if (parm == 'lines') {
|
|
|
|
this.expansioninfo[parm][id[0]][id[1]].expanded = val;
|
|
|
|
} else {
|
|
|
|
this.expansioninfo[parm][id].expanded = val;
|
|
|
|
}
|
|
|
|
|
2024-02-18 13:47:08 +01:00
|
|
|
}
|
|
|
|
},
|
2024-02-18 23:27:57 +01:00
|
|
|
template: `
|
|
|
|
<div>
|
|
|
|
<q-header
|
|
|
|
:sorting='sorting'
|
|
|
|
:structure='structure'
|
|
|
|
:expansion='expansioninfo'
|
|
|
|
@expansion='expansionChanged'
|
|
|
|
></q-header>
|
|
|
|
<div class='q-scrolly'>
|
|
|
|
<template v-for="group in sortedstudents">
|
|
|
|
<q-groupheading v-if="group.users" :label="group.label" :groupinfo="groupinfo[group.label]"></q-groupheading>
|
|
|
|
<template v-if='group.users && groupinfo[group.label].expand'>
|
|
|
|
<q-studentresults v-for="student in group.users"
|
|
|
|
:student='student'
|
|
|
|
:structure='structure'
|
2024-02-19 11:34:40 +01:00
|
|
|
:results='studentresults[student.id].results'
|
|
|
|
:loading='studentresults[student.id].loading'
|
2024-02-18 23:27:57 +01:00
|
|
|
:expansion='expansioninfo'
|
|
|
|
></q-studentresults>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('q-header', {
|
|
|
|
props: {
|
|
|
|
structure: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
sorting: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
expansion: {
|
|
|
|
type: Object
|
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.header,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
conditions(item) {
|
|
|
|
const course = item.course;
|
|
|
|
const list = [];
|
|
|
|
debug.info("Determining conditions", course);
|
|
|
|
if (course.completion) {
|
|
|
|
debug.info("Has Competencies");
|
|
|
|
for (const cmp of course.competencies) {
|
|
|
|
list.push({
|
|
|
|
name: cmp.title,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if(course.completion) {
|
|
|
|
debug.info("Has Core completion");
|
|
|
|
for (const cnd of course.completion.conditions) {
|
|
|
|
for (const itm of cnd.items) {
|
|
|
|
list.push({
|
|
|
|
name: itm.title,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if(course.grades) {
|
|
|
|
debug.info("Has selected grades");
|
|
|
|
for (const g of course.grades) {
|
|
|
|
list.push({
|
|
|
|
name: g.name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return list;
|
|
|
|
},
|
|
|
|
},
|
2024-02-18 13:47:08 +01:00
|
|
|
mounted() {
|
2024-02-18 23:27:57 +01:00
|
|
|
|
2024-02-18 13:47:08 +01:00
|
|
|
},
|
|
|
|
updated() {
|
|
|
|
},
|
|
|
|
template: `
|
2024-02-18 23:27:57 +01:00
|
|
|
<div class='q-header'>
|
|
|
|
<div class='q-studentname heading'><span>{{text.students}}</span></div>
|
|
|
|
<div v-for="p in structure.periods" class='q-period-heading' >
|
|
|
|
<div class='q-header-title'>
|
|
|
|
<a v-if="expansion.periods[p.period.id].expanded"
|
|
|
|
href='#' @click.prevent="$emit('expansion','periods',p.period.id,false);"><i class='fa fa-minus-square'></i></a>
|
|
|
|
<a v-else
|
|
|
|
href='#' @click.prevent="$emit('expansion','periods',p.period.id,true);"><i class='fa fa-plus-square'></i></a>
|
|
|
|
{{ p.period.fullname}}
|
|
|
|
</div>
|
|
|
|
<div v-if="expansion.periods[p.period.id].expanded" class='q-header-details'>
|
|
|
|
<div class='q-line-heading' v-for='l in p.lines'>
|
|
|
|
<div class='q-header-title'><span v-html="l.line.shortname"></span></div>
|
|
|
|
<div v-if="l.items.length == 1 || expansion.lines[p.period.id][l.line.id].expanded" class='q-header-details'>
|
|
|
|
<div class='q-item-heading' v-for='item in l.items'>
|
|
|
|
<div class='q-header-title'>
|
|
|
|
<a v-if="expansion.items[item.id].expanded"
|
|
|
|
href='#' @click.prevent="$emit('expansion','items',item.id,false);"
|
|
|
|
><i class='fa fa-minus-square'></i></a>
|
|
|
|
<a v-else
|
|
|
|
href='#' @click.prevent="$emit('expansion','items',item.id,true);"
|
|
|
|
><i class='fa fa-plus-square'></i></a>
|
|
|
|
{{ item.course.displayname}}
|
|
|
|
</div>
|
|
|
|
<div v-if="expansion.items[item.id].expanded" class='q-header-details'>
|
|
|
|
<div class='q-condition-heading'>
|
|
|
|
{{ text.overall }}
|
|
|
|
</div>
|
|
|
|
<div v-for="c in conditions(item)" class="q-condition-heading" >
|
|
|
|
<span v-html="c.name"></span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-02-18 13:47:08 +01:00
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2024-02-18 23:27:57 +01:00
|
|
|
Vue.component('q-groupheading', {
|
|
|
|
props: {
|
|
|
|
structure: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<div class='q-groupheading'>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('q-studentresults', {
|
|
|
|
props: {
|
|
|
|
student: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
structure: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
results: {
|
2024-02-19 11:34:40 +01:00
|
|
|
type: Array,
|
|
|
|
},
|
|
|
|
loading: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
|
|
|
},
|
2024-02-18 23:27:57 +01:00
|
|
|
expansion: {
|
2024-02-19 11:34:40 +01:00
|
|
|
type: Object,
|
2024-02-18 23:27:57 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.studentresults,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
hasprogressinfo(item) {
|
|
|
|
if (!item.course.enrolled) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
return (item.course.completion || item.course.competency || item.course.grades);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
completion_icon(completion) {
|
|
|
|
switch(completion){
|
|
|
|
default: // case "incomplete"
|
|
|
|
return "circle-o";
|
|
|
|
case "pending":
|
|
|
|
return "question-circle";
|
|
|
|
case "failed":
|
|
|
|
return "times-circle";
|
|
|
|
case "progress":
|
|
|
|
return "exclamation-circle";
|
|
|
|
case "completed":
|
|
|
|
return "check-circle";
|
|
|
|
case "good":
|
|
|
|
return "check-circle";
|
|
|
|
case "excellent":
|
|
|
|
return "check-circle";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
circle_icon(completion) {
|
|
|
|
switch(completion){
|
|
|
|
default: // case "incomplete"
|
|
|
|
return null;
|
|
|
|
case "failed":
|
|
|
|
return "times";
|
|
|
|
case "progress":
|
|
|
|
return "";
|
|
|
|
case "completed":
|
|
|
|
return "check";
|
|
|
|
case "good":
|
|
|
|
return "check";
|
|
|
|
case "excellent":
|
|
|
|
return "check";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
courseprogress(item) {
|
|
|
|
if (!item.course.enrolled) {
|
|
|
|
return 0;
|
|
|
|
} else if(item.course.completion) {
|
|
|
|
return (item.course.completion.progress / item.course.completion.count);
|
|
|
|
} else if(item.course.competency) {
|
|
|
|
return (item.course.competency.progress / item.course.competency.count);
|
|
|
|
} else if(item.course.grades) {
|
|
|
|
return (this.gradeprogress(item.course.grades) / item.course.grades.length);
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
gradeprogress(grades) {
|
|
|
|
let progress = 0;
|
|
|
|
for (const ix in grades) {
|
|
|
|
const g = grades[ix];
|
|
|
|
if (["completed","excellent","good"].includes(g.completion)) {
|
|
|
|
progress++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return progress;
|
|
|
|
},
|
|
|
|
conditions(item) {
|
|
|
|
|
|
|
|
},
|
|
|
|
useritems(line) {
|
|
|
|
const list = [];
|
|
|
|
for (const item of line.items) {
|
|
|
|
let newitm = item;
|
2024-02-19 11:34:40 +01:00
|
|
|
for (const itm of this.results) {
|
2024-02-18 23:27:57 +01:00
|
|
|
if (item.id == itm.id) {
|
|
|
|
newitm = itm;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
list.push(newitm);
|
|
|
|
}
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
},
|
2024-02-19 11:34:40 +01:00
|
|
|
/* https://css-tricks.com/position-sticky-and-table-headers/ */
|
2024-02-18 23:27:57 +01:00
|
|
|
template: `
|
|
|
|
<div class='q-student-results'>
|
|
|
|
<div class='q-studentname '><span>{{student.firstname}} {{student.lastname}}</span></div>
|
|
|
|
<div v-for="p in structure.periods" class='q-period-results' >
|
|
|
|
<div v-if="expansion.periods[p.period.id].expanded" class='q-result-details'>
|
|
|
|
<div class='q-line-results' v-for='l in p.lines'>
|
|
|
|
<div v-if="l.items.length == 1 || expansion.lines[p.period.id][l.line.id].expanded" class='q-result-details'>
|
|
|
|
<div class='q-item-results' v-for='item in useritems(l)'>
|
|
|
|
<div v-if="expansion.items[item.id].expanded" class='q-result-details'>
|
|
|
|
<div class='q-result q-overviewresult'>
|
2024-02-19 11:34:40 +01:00
|
|
|
<template v-if="loading">
|
|
|
|
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
|
|
|
|
</template>
|
|
|
|
<template v-else-if='!item.course.enrolled'>
|
2024-02-18 23:27:57 +01:00
|
|
|
<i v-b-popover.top
|
|
|
|
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
|
|
|
|
:title="text.student_not_tracked"></i>
|
|
|
|
</template>
|
|
|
|
<template v-else-if='hasprogressinfo(item)'>
|
2024-02-19 11:34:40 +01:00
|
|
|
<i v-b-popover.top
|
2024-02-18 23:27:57 +01:00
|
|
|
:class="'r-course-result fa fa-'+completion_icon(item.completion)+
|
|
|
|
' r-completion-'+item.completion"
|
|
|
|
:title="text['completion_'+item.completion]"></i>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<i v-b-popover.top
|
|
|
|
:class="'r-course-result fa fa-'+completion_icon(item.completion)+
|
|
|
|
' r-completion-'+item.completion"
|
|
|
|
:title="text['completion_'+item.completion]"></i>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
<div class='q-condition-heading' v-for='c in conditions(item)'>
|
|
|
|
|
|
|
|
|
2024-02-18 13:47:08 +01:00
|
|
|
|
2024-02-18 23:27:57 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div v-else class='q-result q-overviewresult'>
|
2024-02-19 11:34:40 +01:00
|
|
|
<template v-if="loading">
|
|
|
|
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
|
|
|
|
</template>
|
|
|
|
<template v-else-if='!item.course.enrolled'>
|
2024-02-18 23:27:57 +01:00
|
|
|
<i v-b-popover.top
|
|
|
|
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
|
|
|
|
:title="text.student_not_tracked"></i>
|
|
|
|
</template>
|
|
|
|
<template v-else-if='hasprogressinfo(item)'>
|
2024-02-19 11:34:40 +01:00
|
|
|
<i v-b-popover.top
|
2024-02-18 23:27:57 +01:00
|
|
|
:class="'r-course-result fa fa-'+completion_icon(item.completion)+
|
|
|
|
' r-completion-'+item.completion"
|
|
|
|
:title="text['completion_'+item.completion]"></i>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<i v-b-popover.top
|
|
|
|
:class="'r-course-result fa fa-'+completion_icon(item.completion)+
|
|
|
|
' r-completion-'+item.completion"
|
|
|
|
:title="text['completion_'+item.completion]"></i>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
2024-02-18 13:47:08 +01:00
|
|
|
|
|
|
|
},
|
|
|
|
};
|