/* eslint no-var: "error"*/ /* eslint no-unused-vars: warn */ /* eslint max-depth: ["error", 6] */ /* eslint promise/no-nesting: "off" */ /* eslint camelcase: "off" */ /* eslint-env es6*/ // Put this file in path/to/plugin/amd/src import {loadStrings} from './util/string-helper'; import {call} from 'core/ajax'; import notification from 'core/notification'; import TSComponents from './treestudyplan-components'; import FitTextVue from './util/fittext-vue'; import {formatDatetime} from "./util/date-helper"; /** * Strip tags from html * @param {*} html * @returns {string} */ function striptags(html) { const tmp = document.createElement("DIV"); tmp.innerHTML = html; const text = tmp.textContent || tmp.innerText; tmp.remove(); return text; } /** * Retrieve condition headers * @param {Object} item * @returns {Array} */ function conditionHeaders(item) { const course = item.course; const list = []; if (course.competency) { for (const cmp of course.competency.competencies) { list.push({ name: (cmp.details ? (`${cmp.title} - ${cmp.details}`) : cmp.title), tooltip: cmp.description, }); } } else if (course.completion) { for (const cnd of course.completion.conditions) { for (const itm of cnd.items) { list.push({ name: itm.title, tooltip: `${itm.details.type}: ${itm.details.requirement}`, }); } } } else if (course.grades) { for (const g of course.grades) { if (g.selected) { list.push({ name: g.name, tooltip: `${g.typename}: ${striptags(g.name)}`, }); } } } return list; } /** * Retrieve conditions * @param {Object} item * @returns {Array} */ function conditions(item) { const course = item.course; const list = []; if (course.competency) { for (const cmp of course.competency.competencies) { list.push(cmp); } } else if (course.completion) { for (const cnd of course.completion.conditions) { for (const itm of cnd.items) { list.push(itm); } } } else if (course.grades) { for (const g of course.grades) { if (g.selected) { list.push(g); } } } return list; } export default { install(Vue /* ,options */) { Vue.use(TSComponents); Vue.use(FitTextVue); let strings = loadStrings({ report: { loading: "loadinghelp@core", studyplan_past: "studyplan_past", studyplan_present: "studyplan_present", studyplan_future: "studyplan_future", back: "back", }, invalid: { error: 'error', }, header: { overall: 'overall', students: 'students@core', firstname: 'firstname@core', lastname: 'lastname@core', email: 'email@core', lastaccess: 'lastaccess@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", never: "never@core", } }); /* ********************************** * * * Treestudyplan Viewer components * * * * **********************************/ Vue.component('q-studyplanreport', { props: { structure: { type: Object, }, }, data() { return { students: [], studentresults: {}, studentsloading: true, expansioninfo: { periods: {}, lines: {}, items: {}, }, groupinfo: {}, sorting: { header: 'lastname', asc: true, } }; }, watch: { structure: { immediate: true, handler(structure) { this.loadStudents(); // Reload the student list // (Re)build expansion info structure let firstperiod = true; 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: ((firstperiod && period.lines.length > 0) ? true : 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, } ); } } } firstperiod = false; } } } }, computed: { sortedstudents() { // Probably could make a deep copy for purity's sake, but this works just as well. const students = this.students; for (const group of this.students) { group.users.sort((a, b) => { let d = a; let e = b; if (!this.sorting.asc) { d = b; e = a; } if (this.sorting.header == "lastaccess") { const dvalue = (d[this.sorting.header] ? d[this.sorting.header] : 0); const evalue = (e[this.sorting.header] ? e[this.sorting.header] : 0); return dvalue - evalue; } else { return String(d[this.sorting.header]).localeCompare(String(e[this.sorting.header])); } }); } return students; }, resultColCount() { let count = 0; for (const period of this.structure.periods) { const pid = period.period.id; if (!this.expansioninfo.periods[pid].expanded) { // This period is not expanded. Make it 3 units wide count += 2; } else { for (const line of period.lines) { const lid = line.line.id; if (!this.expansioninfo.lines[pid][lid].expanded) { count += 1; } else { for (const item of line.items) { if (!this.expansioninfo.items[item.id].expanded) { count += 1; } else { count += 1 + conditions(item).length; } } } } } } return count; } }, methods: { loadStudents() { const self = this; self.studentsloading = true; call([{ methodname: 'local_treestudyplan_all_associated_grouped', args: {'studyplan_id': this.structure.studyplan.id} }])[0].then((response) => { self.students = response; for (const group of self.students) { self.$set( self.groupinfo, group.id, { expanded: true, } ); for (const student of group.users) { 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((response) => { self.studentresults[student.id].loading = false; self.studentresults[student.id].results = response; return; }).catch(notification.exception); } } self.studentsloading = false; return; }).catch(notification.exception); }, expansionChanged(parm, id, val) { if (parm[0] == 'p') { parm = 'periods'; } else if (parm[0] == 'l') { parm = 'lines'; } else { parm = 'items'; } if (parm == 'lines') { this.expansioninfo[parm][id[0]][id[1]].expanded = val; } else { this.expansioninfo[parm][id].expanded = val; } }, groupExpansionChanged(group) { this.groupinfo[group.id].expanded = !this.groupinfo[group.id].expanded; }, toggleSort(header) { if (this.sorting.header == header) { this.sorting.asc = !this.sorting.asc; } else { this.sorting.header = header; this.sorting.asc = true; } } }, template: `
`, }); Vue.component('q-header', { props: { structure: { type: Object, }, sorting: { type: Object, }, expansion: { type: Object }, }, data() { return { text: strings.header, }; }, computed: { }, methods: { conditions(item) { return conditionHeaders(item); }, colspanPeriod(period) { const pid = period.period.id; if (this.expansion.periods[pid].expanded) { let sum = 0; for (const l of period.lines) { sum += this.colspanLine(period, l); } return sum; } else { return 2; } }, colspanLine(period, line) { const pid = period.period.id; const lid = line.line.id; if (this.expansion.lines[pid][lid].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; } }, togglePeriod(period, val) { if (val === undefined) { val = !(this.expansion.periods[period.id].expanded); } this.$emit('expansion', 'periods', period.id, val); }, toggleLine(period, line, val) { if (val === undefined) { val = !(this.expansion.lines[period.id][line.id].expanded); } this.$emit('expansion', 'lines', [period.id, line.id], val); }, toggleItem(item, val) { if (val === undefined) { val = !(this.expansion.items[item.id].expanded); } this.$emit('expansion', 'items', item.id, val); }, toggleSort(heading) { this.$emit('togglesort', heading); } }, /* TODO: https://css-tricks.com/position-sticky-and-table-headers/ */ template: ` {{text.students}}  {{ p.period.fullname}}{{ p.period.fullname}} {{text.firstname}} / {{text.lastname}} {{text.lastaccess}} `, }); Vue.component('q-groupheading', { props: { group: { type: Object, }, resultcolumns: { type: Number, 'default': 1 }, studentinfocolumns: { type: Number, 'default': 1 }, expanded: { type: Boolean, } }, data() { return { }; }, computed: { }, methods: { toggleGroup() { this.$emit('togglegroup', this.group); } }, template: `  {{group.label}} `, }); Vue.component('q-inforow', { props: { resultcolumns: { type: Number, 'default': 1 }, studentinfocolumns: { type: Number, 'default': 1 }, }, data() { return { }; }, computed: { }, methods: { }, template: ` `, }); Vue.component('q-studentresults', { props: { student: { type: Object, }, structure: { type: Object, }, results: { type: Array, }, loading: { type: Boolean, 'default': false }, expansion: { type: Object, }, even: { type: Boolean, 'default': false, } }, data() { return { text: strings.studentresults, }; }, computed: { lastaccess() { if (this.student.lastaccess) { return formatDatetime(this.student.lastaccess); // Takes date in milliseconds } else { return this.text.never; } } }, methods: { 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; }, conditions(item) { return conditions(item); }, }, /* https://css-tricks.com/position-sticky-and-table-headers/ */ template: ` {{student.firstname}} {{student.lastname}} {{lastaccess}} `, }); 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) { return false; } else { return (course.completion || course.competency || course.grades) ? true : false; } }, completion_icon() { const completion = this.item.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"; } }, }, methods: { }, template: ` `, }); 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() { return conditions(this.item); }, 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); } }, completion_icon() { const completion = this.condition_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"; } }, condition_value() { const course = this.item.course; if (course.competency) { if (this.condition.grade) { // Return grade if possible. return this.condition.grade; } } else if (course.completion) { if (this.condition.grade) { // Return grade if possible. return this.condition.grade; } } else if (course.grades) { return this.condition.grade; } // Fallback to completion icon. const icon = this.completion_icon(); return ``; }, condition_completion() { // Unify completion information const course = this.item.course; if (course.competency) { const competency = this.condition; if (competency.proficient && competency.courseproficient) { return "completed"; } else if (competency.proficient) { return "completed"; } else if (competency.proficient === false) { return "failed"; } else if (competency.progress) { return "progress"; } else { return "incomplete"; } } else if (course.completion) { return this.condition.status; } else if (course.grades) { return this.condition.completion; } else { return "incomplete"; } } }, methods: { }, template: ` `, }); }, };