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

669 lines
27 KiB
JavaScript
Raw Normal View History

/*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 {load_strings} from './util/string-helper';
import {call} from 'core/ajax';
import notification from 'core/notification';
import Debugger from './util/debugger';
import Config from 'core/config';
import TSComponents from './treestudyplan-components';
// 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
invalid: {
error: 'error',
},
2024-02-18 23:27:57 +01:00
header: {
overall: 'overall',
students: 'students@core'
},
studentresults: {
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: {},
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,
}
);
}
}
}
}
}
}
},
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,
}
);
for(const student of group.users){
2024-02-19 11:34:40 +01:00
self.$set(
self.studentresults,
student.id,
{
loading: true,
results: [],
}
);
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 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-19 23:50:47 +01:00
colspanPeriod(period) {
if (this.expansion.periods[period.id].expanded) {
let sum = 0;
for (const l of period.lines) {
sum += this.colspanLine(period,l);
}
return sum;
} else {
return 1;
}
},
colspanLine(period,line) {
if (this.expansion.lines[period.id][line.id].expanded) {
let sum = 0;
for (const i of line.items) {
sum += this.colspanItem(i);
}
return sum;
} else {
return 1;
}
},
colspanItem(item) {
if (this.expansion.items[item.id].expanded) {
const cs = this.conditions(item);
return 1+cs.length;
} else {
return 1;
}
}
2024-02-18 23:27:57 +01:00
},
mounted() {
2024-02-18 23:27:57 +01:00
},
updated() {
},
2024-02-19 23:50:47 +01:00
/* https://css-tricks.com/position-sticky-and-table-headers/ */
/* TODO: Rework below to make use of tables. Use <Thead> as main element. Then create multiple <tr> as needed for the headers.
This should create a much better view than using divs overal.
*/
template: `
2024-02-19 23:50:47 +01:00
<thead class='q-header'>
<tr> <!-- period heading -->
<th rowspan='3' class='q-studentname'><span>{{text.students}}</span></th>
<th v-for="p in structure.periods"
class='q-period-heading'
:colspan='colspanPeriod(p)'
:rowspan='(expansion.periods[p.period.id].expanded)?1:3'
><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}}<
/th>
</tr>
<tr> <!-- line heading -->
<template v-for="p in structure.periods">
<template v-if="expansion.periods[p.period.id].expanded">
<th v-for='l in p.lines"
class='q-line-heading'
:colspan="colspanLine(p,l)"
><span v-html="l.line.shortname"></span
><th>
</template>
</template>
</tr>
<tr> <!-- item heading -->
<template v-for="p in structure.periods">
<template v-if="expansion.periods[p.period.id].expanded">
<template v-for='l in p.lines">
<template v-if="expansion.lines[p.period.id][l.line.id].expanded">
<th v-for="item in l.items"
class='q-item-heading'
:colspan="colspanitem(item)"
:rowspan='(expansion.items[item.id].expanded)?1:2'
><a v-if="expansion.items[item.id].expanded"
2024-02-18 23:27:57 +01:00
href='#' @click.prevent="$emit('expansion','items',item.id,false);"
2024-02-19 23:50:47 +01:00
><i class='fa fa-minus-square'></i></a
><a v-else
2024-02-18 23:27:57 +01:00
href='#' @click.prevent="$emit('expansion','items',item.id,true);"
2024-02-19 23:50:47 +01:00
><i class='fa fa-plus-square'></i></a
><span v-html="item.course.displayname"></span><
/th>
</template>
</template>
</template>
</template>
</tr>
<tr> <!-- condition heading -->
<template v-for="p in structure.periods">
<template v-if="expansion.periods[p.period.id].expanded">
<template v-for='l in p.lines">
<template v-if="expansion.lines[p.period.id][l.line.id].expanded">
<template v-for="item in l.items">
<th class='q-condition-heading overall
>{{ text.overall }}<
/th>
<template v-if="expansion.items[item.id].expanded">
<th v-for="c in conditions(item)"
class='q-condition-heading'
><span v-html="c.name"></span
></th>
</template>
</template>
</template>
</template>
</template>
</template>
</tr>
</thead>
`,
});
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: {
2024-02-19 23:50:47 +01:00
useritems(line) {
const list = [];
for (const item of line.items) {
let newitm = item;
for (const itm of this.results) {
if (item.id == itm.id) {
newitm = itm;
break;
}
}
list.push(newitm);
}
return list;
}
},
/* https://css-tricks.com/position-sticky-and-table-headers/ */
/* TODO: Rework below to make use of tables. Use <Thead> as main element. Then create multiple <tr> as needed for the headers.
This should create a much better view than using divs overal.
*/
template: `
<tr class='q-student-results'>
<th class='q-studentname'><span>{{student.firstname}} {{student.lastname}}</span></th>
<template v-for="p in structure.periods">
<template v-if="expansion.periods[p.period.id].expanded">
<template v-for='l in p.lines">
<template v-if="expansion.lines[p.period.id][l.line.id].expanded">
<template v-for="item in useritems(l)">
<td class='q-result overall
><q-courseresult
:item="item"
:student="student"
:loading="loading"
></q-courseresult><
/td>
<template v-if="expansion.items[item.id].expanded">
<td v-for="(c,idx) in conditions(item)"
class='q-result'
><q-conditionresult
:item="item"
:conditionidx="idx"
:student="student"
:loading="loading"
></q-conditionresult><
/td>
</template>
</template>
</template>
<td v-else class='q-result collapsed'>&nbsp;</td>
</template>
</template>
<td v-else class='q-result collapsed'>&nbsp;</td>
</template>
</tr>
`,
});
Vue.component('q-courseresult', {
props: {
student: {
type: Object,
},
item: {
type: Object,
},
loading: {
type: Boolean,
default: false
},
},
data() {
return {
text: strings.studentresults,
};
},
computed: {
hasprogressinfo() {
const course = this.item.course;
if (!course.enrolled) {
2024-02-18 23:27:57 +01:00
return false;
} else {
2024-02-19 23:50:47 +01:00
return (course.completion || course.competency || course.grades);
2024-02-18 23:27:57 +01:00
}
},
2024-02-19 23:50:47 +01:00
completion_icon() {
const completion = this.item.completion;
2024-02-18 23:27:57 +01:00
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";
}
},
2024-02-19 23:50:47 +01:00
},
methods: {
},
template: `
<div class='q-courseresult'>
<template v-if="loading">
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
</template>
<template v-else-if='!item.course.enrolled'>
<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>
<i v-b-popover.top
:class="'r-course-result fa fa-'+completion_icon+
' r-completion-'+item.completion"
:title="text['completion_'+item.completion]"></i>
</template>
</div>
`,
});
Vue.component('q-conditionresult', {
props: {
student: {
type: Object,
},
item: {
type: Object,
},
loading: {
type: Boolean,
default: false
},
conditionidx: {
type: Number,
}
},
data() {
return {
text: strings.studentresults,
};
},
computed: {
conditions() {
const course = this.item.course;
const list = [];
debug.info("Determining conditions", course);
if (course.completion) {
debug.info("Has Competencies");
return course.competencies;
} else if(course.completion) {
debug.info("Has Core completion");
const list = [];
for (const cnd of course.completion.conditions) {
for (const itm of cnd.items) {
list.push(itm);
}
}
return list;
} else if(course.grades) {
return course.grades;
}
},
condition() {
if (this.conditionidx >= 0 && this.conditionidx < this.conditions.length) {
return this.conditions[this.conditionidx];
} else {
return null;
}
},
hasprogressinfo() {
const course = this.item.course;
if (!course.enrolled) {
return false;
} else {
return (course.completion || course.competency || course.grades);
}
},
},
methods: {
completion_icon(completion) {
2024-02-18 23:27:57 +01:00
switch(completion){
default: // case "incomplete"
2024-02-19 23:50:47 +01:00
return "circle-o";
case "pending":
return "question-circle";
2024-02-18 23:27:57 +01:00
case "failed":
2024-02-19 23:50:47 +01:00
return "times-circle";
2024-02-18 23:27:57 +01:00
case "progress":
2024-02-19 23:50:47 +01:00
return "exclamation-circle";
2024-02-18 23:27:57 +01:00
case "completed":
2024-02-19 23:50:47 +01:00
return "check-circle";
2024-02-18 23:27:57 +01:00
case "good":
2024-02-19 23:50:47 +01:00
return "check-circle";
2024-02-18 23:27:57 +01:00
case "excellent":
2024-02-19 23:50:47 +01:00
return "check-circle";
2024-02-18 23:27:57 +01:00
}
},
},
2024-02-19 23:50:47 +01:00
// TODO: Show actual grades when relevant at all (don;t forget the grade point completion requirement)
2024-02-18 23:27:57 +01:00
template: `
2024-02-19 23:50:47 +01:00
<div class='q-conditionresult'>
<template v-if="loading">
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
</template>
<template v-else-if='!item.course.enrolled'>
<i v-b-popover.top
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
:title="text.student_not_tracked"></i>
</template>
<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>
2024-02-18 23:27:57 +01:00
</div>
`,
});
2024-02-19 23:50:47 +01:00
},
};