2023-05-17 21:19:14 +02:00
|
|
|
/*eslint no-var: "error"*/
|
|
|
|
/*eslint no-console: "off"*/
|
2023-07-27 15:07:14 +02:00
|
|
|
/*eslint no-unused-vars: warn */
|
|
|
|
/*eslint max-len: ["error", { "code": 160 }] */
|
2023-05-17 21:19:14 +02:00
|
|
|
/*eslint-disable no-trailing-spaces */
|
|
|
|
/*eslint-env es6*/
|
|
|
|
// Put this file in path/to/plugin/amd/src
|
|
|
|
|
2023-08-19 17:54:40 +02:00
|
|
|
import {SimpleLine} from './simpleline/simpleline';
|
2023-05-17 21:19:14 +02:00
|
|
|
import {get_strings} from 'core/str';
|
2023-09-03 17:12:44 +02:00
|
|
|
import {load_strings} from './util/string-helper';
|
2023-11-13 22:18:28 +01:00
|
|
|
import {format_date,studyplanPageTiming,studyplanTiming} from './util/date-helper';
|
2023-05-17 21:19:14 +02:00
|
|
|
import {call} from 'core/ajax';
|
|
|
|
import notification from 'core/notification';
|
2023-08-22 23:12:33 +02:00
|
|
|
import {svgarcpath} from './util/svgarc';
|
2023-08-19 17:54:40 +02:00
|
|
|
import Debugger from './util/debugger';
|
|
|
|
import Config from 'core/config';
|
2023-11-12 23:49:50 +01:00
|
|
|
import {ProcessStudyplan, ProcessStudyplanPage, objCopy} from './studyplan-processor';
|
2023-09-01 12:27:56 +02:00
|
|
|
import TSComponents from './treestudyplan-components';
|
2023-11-27 23:11:17 +01:00
|
|
|
import {eventTypes as editSwEventTypes} from 'core/edit_switch';
|
2023-09-01 12:27:56 +02:00
|
|
|
|
2023-05-20 17:36:00 +02:00
|
|
|
// Make π available as a constant
|
|
|
|
const π = Math.PI;
|
2023-08-09 14:18:05 +02:00
|
|
|
// Gravity value for arrow lines - determines how much a line is pulled in the direction of the start/end before changing direction
|
2023-08-04 22:54:32 +02:00
|
|
|
const LINE_GRAVITY = 1.3;
|
2023-05-20 17:36:00 +02:00
|
|
|
|
2023-11-12 23:49:50 +01:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
export default {
|
|
|
|
install(Vue/*,options*/){
|
2023-09-01 12:27:56 +02:00
|
|
|
Vue.use(TSComponents);
|
2023-06-16 23:12:17 +02:00
|
|
|
let debug = new Debugger("treestudyplan-viewer");
|
2023-08-04 19:59:28 +02:00
|
|
|
let lastCaller = null;
|
|
|
|
/**
|
|
|
|
* Scroll current period into view
|
|
|
|
* @param {*} handle A key to pass so subsequent calls with the same key won't trigger (always triggers when null or undefined)
|
|
|
|
*/
|
|
|
|
function scrollCurrentIntoView(handle){
|
|
|
|
const elScrollContainer = document.querySelector(".r-studyplan-scrollable");
|
|
|
|
const elCurrentHeader = elScrollContainer.querySelector(".s-studyline-header-period.current");
|
|
|
|
|
|
|
|
if(elCurrentHeader && ((!handle) || (handle != lastCaller))){
|
|
|
|
lastCaller = handle;
|
|
|
|
elCurrentHeader.scrollIntoView({
|
|
|
|
behavior: "smooth",
|
|
|
|
block: "start",
|
|
|
|
inline: "center",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
let strings = load_strings({
|
2023-11-12 23:49:50 +01:00
|
|
|
report: {
|
|
|
|
loading: "loadinghelp@core",
|
|
|
|
studyplan_past: "studyplan_past",
|
|
|
|
studyplan_present: "studyplan_present",
|
|
|
|
studyplan_future: "studyplan_future",
|
|
|
|
back: "back",
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
invalid: {
|
|
|
|
error: 'error',
|
|
|
|
},
|
|
|
|
grading: {
|
|
|
|
ungraded: "ungraded",
|
|
|
|
graded: "graded",
|
|
|
|
allgraded: "allgraded",
|
|
|
|
unsubmitted: "unsubmitted",
|
|
|
|
nogrades: "nogrades",
|
|
|
|
unknown: "unknown",
|
2023-06-03 00:01:18 +02:00
|
|
|
},
|
|
|
|
completion: {
|
2023-06-16 23:12:17 +02:00
|
|
|
completed: "completion_completed",
|
|
|
|
incomplete: "completion_incomplete",
|
|
|
|
completed_pass: "completion_passed",
|
|
|
|
completed_fail: "completion_failed",
|
|
|
|
ungraded: "ungraded",
|
|
|
|
aggregation_all: "aggregation_all",
|
|
|
|
aggregation_any: "aggregation_any",
|
2023-09-01 12:27:56 +02:00
|
|
|
aggregation_one: "aggregation_one",
|
2023-06-16 23:12:17 +02:00
|
|
|
aggregation_overall_all: "aggregation_overall_all",
|
|
|
|
aggregation_overall_any: "aggregation_overall_any",
|
2023-09-01 12:27:56 +02:00
|
|
|
aggregation_overall_one: "aggregation_overall_one",
|
2023-06-16 23:12:17 +02:00
|
|
|
completion_not_configured: "completion_not_configured",
|
|
|
|
configure_completion: "configure_completion",
|
2023-08-08 22:47:36 +02:00
|
|
|
view_completion_report: "view_completion_report",
|
2023-09-01 12:27:56 +02: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",
|
|
|
|
view_feedback: "view_feedback",
|
|
|
|
coursetiming_past: "coursetiming_past",
|
|
|
|
coursetiming_present: "coursetiming_present",
|
|
|
|
coursetiming_future: "coursetiming_future",
|
|
|
|
required_goal: "required_goal",
|
|
|
|
student_not_tracked: "student_not_tracked",
|
|
|
|
completion_not_enabled: "completion_not_enabled",
|
2023-06-03 00:01:18 +02:00
|
|
|
},
|
|
|
|
badge: {
|
|
|
|
share_badge: "share_badge",
|
|
|
|
dateissued: "dateissued",
|
|
|
|
dateexpire: "dateexpire",
|
|
|
|
badgeinfo: "badgeinfo",
|
2023-06-16 23:12:17 +02:00
|
|
|
badgeissuedstats: "badgeissuedstats",
|
2023-11-01 23:47:54 +01:00
|
|
|
completion_incomplete: "completion_incomplete_badge",
|
|
|
|
completion_completed: "completion_completed_badge",
|
2023-11-02 07:18:37 +01:00
|
|
|
completioninfo: "completioninfo",
|
|
|
|
badgedisabled: "badgedisabled"
|
2023-06-16 23:12:17 +02:00
|
|
|
},
|
|
|
|
course: {
|
|
|
|
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",
|
|
|
|
view_feedback: "view_feedback",
|
|
|
|
coursetiming_past: "coursetiming_past",
|
|
|
|
coursetiming_present: "coursetiming_present",
|
|
|
|
coursetiming_future: "coursetiming_future",
|
|
|
|
required_goal: "required_goal",
|
2023-08-31 07:40:55 +02:00
|
|
|
student_not_tracked: "student_not_tracked",
|
|
|
|
not_enrolled: "not_enrolled",
|
2023-06-16 23:12:17 +02:00
|
|
|
},
|
2023-06-30 17:03:10 +02:00
|
|
|
teachercourse: {
|
|
|
|
select_conditions: "select_conditions",
|
|
|
|
select_grades: "select_grades",
|
|
|
|
coursetiming_past: "coursetiming_past",
|
|
|
|
coursetiming_present: "coursetiming_present",
|
2023-09-08 12:47:29 +02:00
|
|
|
coursetiming_future: "coursetiming_future",
|
|
|
|
grade_include: "grade_include",
|
|
|
|
grade_require: "grade_require",
|
2023-06-30 17:03:10 +02:00
|
|
|
required_goal: "required_goal",
|
2023-08-31 07:40:55 +02:00
|
|
|
student_from_plan_enrolled: "student_from_plan_enrolled",
|
2023-09-02 20:38:14 +02:00
|
|
|
students_from_plan_enrolled: "students_from_plan_enrolled",
|
2023-11-05 22:33:57 +01:00
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
competency: {
|
|
|
|
competency_not_configured: "competency_not_configured",
|
|
|
|
configure_competency: "configure_competency",
|
|
|
|
when: "when",
|
|
|
|
required: "required",
|
|
|
|
points: "points@core_grades",
|
|
|
|
heading: "competency_heading",
|
|
|
|
details: "competency_details",
|
|
|
|
results: "results",
|
|
|
|
unrated: "unrated",
|
|
|
|
progress: "completion_progress",
|
|
|
|
view_feedback: "view_feedback",
|
|
|
|
},
|
2023-11-05 22:33:57 +01:00
|
|
|
pageinfo: {
|
|
|
|
edit: 'period_edit',
|
|
|
|
fullname: 'studyplan_name',
|
|
|
|
shortname: 'studyplan_shortname',
|
|
|
|
startdate: 'studyplan_startdate',
|
|
|
|
enddate: 'studyplan_enddate',
|
|
|
|
description: 'studyplan_description',
|
2023-11-12 23:49:50 +01:00
|
|
|
duration: 'studyplan_duration',
|
|
|
|
details: 'studyplan_details',
|
2023-06-30 17:03:10 +02:00
|
|
|
}
|
2023-06-03 00:01:18 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
/************************************
|
|
|
|
* *
|
2023-05-20 17:36:00 +02:00
|
|
|
* Treestudyplan Viewer components *
|
2023-05-17 21:19:14 +02:00
|
|
|
* *
|
|
|
|
************************************/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 );
|
|
|
|
}
|
|
|
|
|
2023-07-27 15:07:14 +02:00
|
|
|
// Create new eventbus for interaction between item components
|
|
|
|
const ItemEventBus = new Vue();
|
|
|
|
|
2023-11-27 23:11:17 +01:00
|
|
|
// Add event listener for the edit mode event so we can react to it, or at the very least ignore it
|
|
|
|
document.addEventListener(editSwEventTypes.editModeSet,(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
ItemEventBus.$emit('editModeSet',e.detail.editMode);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2023-05-20 17:36:00 +02:00
|
|
|
Vue.component('r-progress-circle',{
|
|
|
|
props: {
|
|
|
|
value: {
|
|
|
|
type: Number,
|
|
|
|
},
|
|
|
|
max: {
|
|
|
|
type: Number,
|
|
|
|
default: 100,
|
|
|
|
},
|
|
|
|
min: {
|
|
|
|
type: Number,
|
|
|
|
default: 0,
|
|
|
|
},
|
|
|
|
stroke: {
|
|
|
|
type: Number,
|
|
|
|
default: 0.2,
|
|
|
|
},
|
|
|
|
bgopacity: {
|
|
|
|
type: Number,
|
|
|
|
default: 0.2,
|
|
|
|
},
|
2023-05-20 18:25:22 +02:00
|
|
|
title: {
|
|
|
|
type: String,
|
|
|
|
default: "",
|
|
|
|
},
|
2023-08-09 14:18:05 +02:00
|
|
|
icon: {
|
|
|
|
type: String,
|
|
|
|
}
|
2023-05-20 18:25:22 +02:00
|
|
|
|
2023-05-20 17:36:00 +02:00
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
selectedstudyplan: null,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
range() {
|
|
|
|
return this.max - this.min;
|
|
|
|
},
|
|
|
|
fraction(){
|
|
|
|
if(this.max - this.min == 0){
|
2023-05-20 18:25:22 +02:00
|
|
|
return 0;
|
|
|
|
// 0 size is always empty :)
|
2023-05-20 17:36:00 +02:00
|
|
|
} else {
|
|
|
|
return (this.value - this.min)/(this.max - this.min);
|
|
|
|
}
|
|
|
|
},
|
2023-05-20 18:25:22 +02:00
|
|
|
radius() {
|
|
|
|
return 50 - (50*this.stroke);
|
2023-05-20 17:36:00 +02:00
|
|
|
},
|
2023-05-20 18:25:22 +02:00
|
|
|
arcpath() {
|
2023-09-08 12:47:29 +02:00
|
|
|
let fraction = 0;
|
2023-05-20 18:25:22 +02:00
|
|
|
const r = 50 - (50*this.stroke);
|
2023-05-20 17:36:00 +02:00
|
|
|
if(this.max - this.min != 0){
|
|
|
|
fraction = (this.value - this.min)/(this.max - this.min);
|
|
|
|
}
|
|
|
|
|
|
|
|
const Δ = fraction * 2*π;
|
2023-05-20 18:25:22 +02:00
|
|
|
return svgarcpath([50,50],[r,r],[0,Δ], 1.5*π);
|
2023-05-20 17:36:00 +02:00
|
|
|
},
|
|
|
|
},
|
2023-05-20 18:25:22 +02:00
|
|
|
methods: {
|
|
|
|
},
|
2023-05-20 17:36:00 +02:00
|
|
|
template: `
|
2023-08-09 14:18:05 +02:00
|
|
|
<div style="display: inline-block; width: 1em; height: 1em; position:relative; padding: 0;">
|
|
|
|
<svg width="1em" height="1em" viewBox="0 0 100 100"
|
|
|
|
style="position: absolute;top: 0;left: 0;">
|
|
|
|
<title>{{title}}</title>
|
2023-09-08 12:47:29 +02:00
|
|
|
<circle v-if="fraction >= 1.0" cx="50" cy="50" :r="radius"
|
2023-08-09 14:18:05 +02:00
|
|
|
:style="'opacity: 1; stroke-width: '+ (stroke*100)+'; stroke: currentcolor; fill: none;'"/>
|
|
|
|
<g v-else>
|
2023-09-08 12:47:29 +02:00
|
|
|
<circle cx="50" cy="50" :r="radius"
|
2023-08-09 14:18:05 +02:00
|
|
|
:style="'opacity: ' + bgopacity + ';stroke-width: '+ (stroke*100)+'; stroke: currentcolor; fill: none;'"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
<path :d="arcpath"
|
2023-08-09 14:18:05 +02:00
|
|
|
:style="'stroke-width: ' + (stroke*100) +'; stroke: currentcolor; fill: none;'"/>
|
|
|
|
</g>
|
|
|
|
</svg>
|
2023-09-08 12:47:29 +02:00
|
|
|
<i v-if='icon' :class="'fa fa-'+icon"
|
|
|
|
style=" position: absolute; top: 0.42em; left: 0.43em; text-align:center; display: inline-block;
|
2023-08-09 14:18:05 +02:00
|
|
|
width:1em; font-size: 0.55em; "
|
|
|
|
></i>
|
|
|
|
</div>
|
2023-05-20 17:36:00 +02:00
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
Vue.component('r-report', {
|
|
|
|
props: {
|
2023-11-12 23:49:50 +01:00
|
|
|
invitekey: {
|
|
|
|
type: String,
|
|
|
|
default() { return null;},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
2023-11-12 23:49:50 +01:00
|
|
|
userid: {
|
|
|
|
type: Number,
|
|
|
|
default() { return 0;},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
2023-11-12 23:49:50 +01:00
|
|
|
type: {
|
|
|
|
type: String,
|
|
|
|
default() { return "own";},
|
2023-11-27 23:18:55 +01:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2023-11-12 23:49:50 +01:00
|
|
|
text: strings.report,
|
|
|
|
studyplans: {
|
|
|
|
past: [],
|
|
|
|
present: [],
|
|
|
|
future: [],
|
|
|
|
},
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
selectedstudyplan: null,
|
2023-11-12 23:49:50 +01:00
|
|
|
loadingstudyplan: false,
|
|
|
|
loading: true,
|
2023-05-17 21:19:14 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
2023-11-12 23:49:50 +01:00
|
|
|
teachermode() {
|
|
|
|
return (this.type =="teaching");
|
|
|
|
},
|
|
|
|
guestmode() {
|
|
|
|
return (this.type == "invited");
|
|
|
|
},
|
|
|
|
verified_type() {
|
|
|
|
if (! ["invited","other","teaching","own"].includes(this.type)) {
|
|
|
|
return "own";
|
2023-05-17 21:19:14 +02:00
|
|
|
} else {
|
2023-11-12 23:49:50 +01:00
|
|
|
return this.type;
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
2023-11-12 23:49:50 +01:00
|
|
|
},
|
|
|
|
studyplancount() {
|
|
|
|
return this.studyplans.past.length + this.studyplans.present.length + this.studyplans.future.length;
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
},
|
2023-11-12 23:49:50 +01:00
|
|
|
updated() {
|
|
|
|
|
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
this.loadStudyplans();
|
|
|
|
},
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
methods: {
|
2023-11-12 23:49:50 +01:00
|
|
|
call_args(o) {
|
|
|
|
const args = {};
|
|
|
|
if (typeof o == 'object' && !Array.isArray(o) && o !== null) {
|
|
|
|
objCopy(args,o);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(this.verified_type == "invited") {
|
|
|
|
args["invitekey"] = this.invitekey;
|
|
|
|
} else if(this.verified_type == "other") {
|
|
|
|
args["userid"] = this.userid;
|
|
|
|
}
|
|
|
|
return args;
|
2023-08-15 22:52:43 +02:00
|
|
|
},
|
2023-11-12 23:49:50 +01:00
|
|
|
loadStudyplans() {
|
|
|
|
const self = this;
|
|
|
|
this.loading = true;
|
|
|
|
call([{
|
|
|
|
methodname: `local_treestudyplan_list_${this.verified_type}_studyplans`,
|
|
|
|
args: this.call_args(),
|
|
|
|
}])[0].done(function(response){
|
|
|
|
|
|
|
|
const plans = { future: [], present: [], past: [], };
|
|
|
|
|
|
|
|
for (const ix in response) {
|
|
|
|
const plan = response[ix];
|
2023-11-13 22:18:28 +01:00
|
|
|
const timing = studyplanTiming(plan);
|
2023-11-12 23:49:50 +01:00
|
|
|
plans[timing].push(plan);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const ix in plans) {
|
|
|
|
plans[ix].sort((a,b) => {
|
|
|
|
const t = new Date(b.startdate).getTime() - new Date(a.startdate).getTime();
|
|
|
|
if (t == 0) {
|
|
|
|
// sort by name if timing is equal
|
|
|
|
t = a.name.localeCompare(b.name);
|
|
|
|
}
|
|
|
|
return t;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
self.studyplans = plans;
|
|
|
|
self.loading = false;
|
|
|
|
|
|
|
|
if (self.studyplans.present.length == 1) {
|
|
|
|
// Directly show the current study plan if it's the only one
|
|
|
|
self.selectStudyplan(self.studyplans.present[0]);
|
|
|
|
} else {
|
|
|
|
// If there is but a single studyplan, select it anyway, even if it is not current...
|
|
|
|
if (this.studyplancount == 1) {
|
|
|
|
if(self.studyplans.future.lengh > 0) {
|
|
|
|
self.selectStudyplan(self.studyplans.future[0]);
|
|
|
|
} else {
|
|
|
|
self.selectStudyplan(self.studyplans.past[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}).fail(notification.exception);
|
|
|
|
},
|
|
|
|
selectStudyplan(plan) {
|
|
|
|
const self = this;
|
|
|
|
this.loadingstudyplan = true;
|
|
|
|
call([{
|
|
|
|
methodname: `local_treestudyplan_get_${this.verified_type}_studyplan`,
|
|
|
|
args: this.call_args({
|
|
|
|
studyplanid: plan.id,
|
|
|
|
}),
|
|
|
|
}])[0].done(function(response){
|
|
|
|
self.selectedstudyplan = ProcessStudyplan(response);
|
|
|
|
self.loadingstudyplan = false;
|
|
|
|
}).fail(notification.exception);
|
|
|
|
},
|
|
|
|
deselectStudyplan() {
|
|
|
|
this.selectedstudyplan = null;
|
|
|
|
this.loadStudyplans(); // Reload the list of studyplans.
|
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
template: `
|
2023-09-08 12:47:29 +02:00
|
|
|
<div class='t-studyplan-container r-report-tabs'>
|
2023-11-12 23:49:50 +01:00
|
|
|
<div v-if='loading' class="vue-loader spinner-border text-primary" role="status"></div>
|
|
|
|
<template v-else>
|
|
|
|
<div v-if='!loadingstudyplan && selectedstudyplan'>
|
|
|
|
<div class="mb-2">
|
|
|
|
<a href='#' @click='deselectStudyplan'><h5 class="d-inline"><i class="fa fa-chevron-left"></i> {{ text.back }}</h5></a>
|
|
|
|
<h4 class="d-inline ml-3">{{ selectedstudyplan.name}}
|
|
|
|
<s-studyplan-details
|
|
|
|
class="mb-2"
|
|
|
|
size="sm"
|
|
|
|
v-model="selectedstudyplan"
|
|
|
|
v-if="selectedstudyplan.description"
|
|
|
|
><i class='fa fa-info-circle'></i></s-studyplan-details></h4>
|
|
|
|
</div>
|
|
|
|
<r-studyplan
|
|
|
|
v-model='selectedstudyplan'
|
|
|
|
:guestmode='guestmode'
|
|
|
|
:teachermode='teachermode'
|
|
|
|
></r-studyplan>
|
|
|
|
</div>
|
|
|
|
<div v-else-if='loadingstudyplan' class="vue-loader spinner-border text-primary" role="status">
|
|
|
|
<span class="sr-only">{{ text.loading }}</span>
|
|
|
|
</div>
|
|
|
|
<div v-else class='t-studyplan-notselected'>
|
|
|
|
<template v-for="timing in ['present', 'past', 'future']">
|
|
|
|
<template v-if="studyplans[timing].length > 0">
|
|
|
|
<h4>{{ text["studyplan_"+timing]}}:</h4>
|
|
|
|
<b-card-group deck>
|
|
|
|
<s-studyplan-card
|
|
|
|
v-for='(studyplan, planindex) in studyplans[timing]'
|
|
|
|
:key='studyplan.id'
|
|
|
|
v-model='studyplans[timing][planindex]'
|
|
|
|
open
|
|
|
|
@open='selectStudyplan(studyplan)'
|
|
|
|
></s-studyplan-card>
|
|
|
|
</b-card-group>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
</template>
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('r-studyplan', {
|
|
|
|
props: {
|
|
|
|
value: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
2023-11-27 23:18:55 +01:00
|
|
|
default() { return false;},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
2023-11-27 23:18:55 +01:00
|
|
|
default() { return false;},
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2023-11-13 22:18:28 +01:00
|
|
|
selectedpageindex: -1,
|
2023-11-05 22:33:57 +01:00
|
|
|
text: strings.pageinfo,
|
2023-05-17 21:19:14 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
2023-11-05 22:33:57 +01:00
|
|
|
|
|
|
|
selectedpage() {
|
|
|
|
return this.value.pages[this.selectedpageindex];
|
|
|
|
},
|
2023-11-13 22:18:28 +01:00
|
|
|
startpageindex() {
|
|
|
|
let startpageindex = 0;
|
|
|
|
let firststart = null;
|
|
|
|
for(const ix in this.value.pages) {
|
|
|
|
const page = this.value.pages[ix];
|
|
|
|
debug.info(`Checking page ${ix} - timing ${studyplanPageTiming(page)}`,page);
|
|
|
|
if(studyplanPageTiming(page) == "present") {
|
|
|
|
debug.info(`Found page nr ${ix} to be present`);
|
|
|
|
const s = new Date(page.startdate);
|
|
|
|
if( (!firststart) || firststart > s) {
|
|
|
|
startpageindex = ix;
|
|
|
|
firststart = s;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return startpageindex;
|
|
|
|
}
|
2023-11-05 22:33:57 +01:00
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
pageduration(page){
|
|
|
|
return format_date(page.startdate,false) + " - " + format_date(page.enddate,false);
|
|
|
|
},
|
|
|
|
columns(page) {
|
|
|
|
return 1+ (page.periods * 2);
|
2023-07-27 15:07:14 +02:00
|
|
|
},
|
2023-11-05 22:33:57 +01:00
|
|
|
columns_stylerule(page) {
|
2023-07-27 15:07:14 +02:00
|
|
|
// Uses css variables, so width for slots and filters can be configured in css
|
|
|
|
let s = "grid-template-columns: var(--studyplan-filter-width)"; // use css variable here
|
2023-11-05 22:33:57 +01:00
|
|
|
for(let i=0; i<page.periods;i++){
|
2023-07-27 15:07:14 +02:00
|
|
|
s+= " var(--studyplan-course-width) var(--studyplan-filter-width)";
|
|
|
|
}
|
|
|
|
return s+";";
|
|
|
|
},
|
2023-11-05 22:33:57 +01:00
|
|
|
countLineLayers(line,page){
|
2023-07-27 15:07:14 +02:00
|
|
|
let maxLayer = -1;
|
2023-11-05 22:33:57 +01:00
|
|
|
for(let i = 0; i <= page.periods; i++){
|
2023-07-27 15:07:14 +02:00
|
|
|
const slot = line.slots[i];
|
|
|
|
// Determine the amount of used layers in a studyline slit
|
2023-08-28 11:26:14 +02:00
|
|
|
for(const ix in line.slots[i].courses){
|
|
|
|
const item = line.slots[i].courses[ix];
|
2023-07-27 15:07:14 +02:00
|
|
|
if(item.layer > maxLayer){
|
|
|
|
maxLayer = item.layer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for(const ix in line.slots[i].filters){
|
|
|
|
const item = line.slots[i].filters[ix];
|
|
|
|
if(item.layer > maxLayer){
|
|
|
|
maxLayer = item.layer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-04 14:57:23 +02:00
|
|
|
return (maxLayer >= 0)?(maxLayer+1):1;
|
2023-07-27 15:07:14 +02:00
|
|
|
},
|
2023-11-05 22:33:57 +01:00
|
|
|
showslot(page,line,index, layeridx, type){
|
2023-08-04 14:57:23 +02:00
|
|
|
// check if the slot should be hidden because a previous slot has an item with a span
|
|
|
|
// so big that it hides this slot
|
|
|
|
const forGradable = (type == 'gradable')?true:false;
|
2023-11-05 22:33:57 +01:00
|
|
|
const periods = page.periods;
|
2023-08-04 14:57:23 +02:00
|
|
|
let show = true;
|
|
|
|
for(let i = 0; i < periods; i++){
|
2023-08-28 11:26:14 +02:00
|
|
|
if(line.slots[index-i] && line.slots[index-i].courses){
|
|
|
|
const list = line.slots[index-i].courses;
|
2023-08-04 14:57:23 +02:00
|
|
|
for(const ix in list){ // Really wish that 'for of' would work with the minifier moodle uses
|
|
|
|
const item = list[ix];
|
|
|
|
if(item.layer == layeridx){
|
|
|
|
if(forGradable){
|
|
|
|
if(i > 0 && (item.span - i) > 0){
|
|
|
|
show = false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if((item.span - i) > 1){
|
|
|
|
show = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-08-04 14:57:23 +02:00
|
|
|
return show;
|
2023-11-05 22:33:57 +01:00
|
|
|
},
|
|
|
|
selectedpageChanged(newTabIndex,prevTabIndex) {
|
2023-11-11 20:18:05 +01:00
|
|
|
|
|
|
|
ItemEventBus.$emit('redrawLines', null);
|
2023-11-13 22:18:28 +01:00
|
|
|
scrollCurrentIntoView(this.selectedpage.id);
|
2023-08-04 14:57:23 +02:00
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
2023-08-04 19:59:28 +02:00
|
|
|
mounted() {
|
2023-11-13 22:18:28 +01:00
|
|
|
// scrollCurrentIntoView(this.selectedpage.id);
|
2023-11-11 20:18:05 +01:00
|
|
|
this.$root.$emit('redrawLines');
|
2023-08-04 19:59:28 +02:00
|
|
|
},
|
|
|
|
updated() {
|
2023-11-13 22:18:28 +01:00
|
|
|
|
|
|
|
scrollCurrentIntoView(this.selectedpage.id);
|
2023-08-15 22:52:43 +02:00
|
|
|
ItemEventBus.$emit('lineHeightChange', null);
|
2023-11-11 20:18:05 +01:00
|
|
|
this.$root.$emit('redrawLines');
|
|
|
|
ItemEventBus.$emit('redrawLines');
|
2023-08-04 19:59:28 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
template: `
|
2023-11-05 22:33:57 +01:00
|
|
|
<div>
|
|
|
|
<b-card no-body>
|
|
|
|
<b-tabs
|
|
|
|
v-model='selectedpageindex'
|
|
|
|
@activate-tab='selectedpageChanged'
|
|
|
|
content-class="mt-1">
|
|
|
|
<b-tab
|
|
|
|
v-for="(page,pageindex) in value.pages"
|
2023-11-13 22:18:28 +01:00
|
|
|
:active="(pageindex == startpageindex)"
|
2023-11-05 22:33:57 +01:00
|
|
|
:key="page.id"
|
|
|
|
:title-item-class="'s-studyplanpage-tab '+ page.timing"
|
|
|
|
><template #title>
|
|
|
|
<span v-b-tooltip.hover :title='page.fullname'>{{page.shortname}}</span>
|
2023-11-12 23:49:50 +01:00
|
|
|
<a href="#" v-b-modal="'studyplanpage-info-'+page.id" class='text-info'
|
2023-11-05 22:33:57 +01:00
|
|
|
v-if='pageindex == selectedpageindex'
|
|
|
|
><i class='fa fa-info-circle'></i></a>
|
2023-07-29 12:21:30 +02:00
|
|
|
</template>
|
2023-11-05 22:33:57 +01:00
|
|
|
|
|
|
|
<b-modal
|
|
|
|
:id="'studyplanpage-info-'+page.id"
|
|
|
|
scrollable
|
|
|
|
ok-only
|
|
|
|
>
|
|
|
|
<template #modal-title>
|
|
|
|
{{page.fullname}}
|
|
|
|
</template>
|
|
|
|
<b-container>
|
|
|
|
<b-row>
|
|
|
|
<b-col cols="4"><b>{{ text.shortname}}</b></b-col>
|
|
|
|
<b-col cols="8">
|
|
|
|
{{ page.shortname }}
|
|
|
|
</b-col>
|
|
|
|
</b-row>
|
|
|
|
<b-row>
|
|
|
|
<b-col cols="4"><b>{{ text.duration}}</b></b-col>
|
|
|
|
<b-col cols="8">
|
|
|
|
{{ pageduration(page) }}
|
|
|
|
</b-col>
|
|
|
|
</b-row>
|
|
|
|
<b-row v-if="page.description">
|
|
|
|
<b-col cols="12"><b>{{ text.description}}</b></b-col>
|
|
|
|
</b-row>
|
|
|
|
<b-row v-if="page.description">
|
|
|
|
<b-col cols="12">
|
|
|
|
<span v-html="page.description"></span>
|
|
|
|
</b-col>
|
|
|
|
</b-row>
|
|
|
|
</b-container>
|
|
|
|
</b-modal>
|
|
|
|
<div v-if="page.studylines.length > 0" class='r-studyplan-content'>
|
|
|
|
<!-- First paint the headings-->
|
|
|
|
<div class='r-studyplan-headings'
|
|
|
|
><s-studyline-header-heading :identifier="Number(page.id)"></s-studyline-header-heading>
|
|
|
|
<r-studyline-heading v-for="(line,lineindex) in page.studylines"
|
|
|
|
:key="line.id"
|
|
|
|
v-model="page.studylines[lineindex]"
|
|
|
|
:layers='countLineLayers(page,line)+1'
|
|
|
|
:class=" 't-studyline' + ((lineindex%2==0)?' odd ' :' even ' )
|
|
|
|
+ ((lineindex==0)?' first ':' ')
|
|
|
|
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
|
|
|
|
></r-studyline-heading
|
|
|
|
></div>
|
|
|
|
<!-- Next, paint all the cells in the scrollable -->
|
|
|
|
<div class="r-studyplan-scrollable" >
|
|
|
|
<div class="r-studyplan-timeline" :style="columns_stylerule(page)">
|
|
|
|
<!-- add period information -->
|
|
|
|
<template v-for="(n,index) in (page.periods+1)">
|
|
|
|
<s-studyline-header-period
|
|
|
|
v-if="index > 0"
|
|
|
|
v-model="page.perioddesc[index-1]"
|
|
|
|
:identifier="Number(page.id)"
|
|
|
|
></s-studyline-header-period>
|
|
|
|
<div class="s-studyline-header-filter"></div>
|
|
|
|
</template>
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-11-05 22:33:57 +01:00
|
|
|
<!-- Line by line add the items -->
|
|
|
|
<!-- The grid layout handles putting it in rows and columns -->
|
|
|
|
<template v-for="(line,lineindex) in page.studylines"
|
|
|
|
><template v-for="(layernr,layeridx) in countLineLayers(page,line)"
|
|
|
|
><template v-for="(n,index) in (page.periods+1)"
|
|
|
|
><r-studyline-slot
|
|
|
|
v-if="index > 0 && showslot(page,line, index, layeridx, 'gradable')"
|
|
|
|
type='gradable'
|
|
|
|
v-model="line.slots[index].courses"
|
|
|
|
:key="'c-'+lineindex+'-'+index+'-'+layernr"
|
|
|
|
:slotindex="index"
|
|
|
|
:line="line"
|
|
|
|
:plan="value"
|
|
|
|
:page="page"
|
|
|
|
:period="page.perioddesc[index-1]"
|
|
|
|
:guestmode='guestmode'
|
|
|
|
:teachermode='teachermode'
|
|
|
|
:layer="layeridx"
|
|
|
|
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
|
|
|
+ ((lineindex==0 && layernr==1)?' first ':' ')
|
|
|
|
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
|
|
|
|
></r-studyline-slot
|
|
|
|
><r-studyline-slot
|
|
|
|
v-if="showslot(page,line, index, layeridx, 'gradable')"
|
|
|
|
type='filter'
|
|
|
|
v-model="line.slots[index].filters"
|
|
|
|
:teachermode='teachermode'
|
|
|
|
:key="'f-'+lineindex+'-'+index+'-'+layernr"
|
|
|
|
:slotindex="index"
|
|
|
|
:line="line"
|
|
|
|
:plan="value"
|
|
|
|
:page="page"
|
|
|
|
:layer="layeridx"
|
|
|
|
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
|
|
|
|
+ ((lineindex==0 && layernr==1)?' first ':'')
|
|
|
|
+ ((lineindex==page.studylines.length-1)?' last ':' ')
|
|
|
|
+ ((index==page.periods)?' rightmost':'')"
|
|
|
|
>
|
|
|
|
</r-studyline-slot
|
|
|
|
></template
|
|
|
|
></template
|
2023-07-27 15:07:14 +02:00
|
|
|
></template
|
2023-11-05 22:33:57 +01:00
|
|
|
></div
|
|
|
|
></div
|
|
|
|
></div>
|
|
|
|
</b-tab>
|
|
|
|
</b-tabs>
|
|
|
|
</b-card>
|
|
|
|
</div>
|
2023-05-17 21:19:14 +02:00
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
/*
|
2023-07-27 15:07:14 +02:00
|
|
|
* R-STUDYLINE-HEADER
|
2023-05-17 21:19:14 +02:00
|
|
|
*/
|
2023-07-27 15:07:14 +02:00
|
|
|
Vue.component('r-studyline-heading', {
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object, // Studyline
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
layers: {
|
|
|
|
type: Number,
|
|
|
|
default: 1,
|
|
|
|
},
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
data() {
|
|
|
|
return {
|
2023-07-27 15:07:14 +02:00
|
|
|
layerHeights: {}
|
2023-05-17 21:19:14 +02:00
|
|
|
};
|
|
|
|
},
|
2023-07-27 15:07:14 +02:00
|
|
|
created() {
|
|
|
|
// Listener for the signal that a new connection was made and needs to be drawn
|
|
|
|
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
|
|
|
|
ItemEventBus.$on('lineHeightChange', this.onLineHeightChange);
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
computed: {
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
methods: {
|
2023-08-15 22:52:43 +02:00
|
|
|
onLineHeightChange(lineid){
|
2023-07-27 15:07:14 +02:00
|
|
|
// All layers for this line have the first slot send an update message on layer height change.
|
2023-09-08 12:47:29 +02:00
|
|
|
// When one of those updates is received, record the height and recalculate the total height of the
|
2023-07-27 15:07:14 +02:00
|
|
|
// header
|
2023-08-15 22:52:43 +02:00
|
|
|
if(this.$refs.mainEl && (lineid == this.value.id || lineid === null)){
|
2023-07-27 15:07:14 +02:00
|
|
|
const items = document.querySelectorAll(
|
|
|
|
`.r-studyline-slot-0[data-studyline='${this.value.id}']`);
|
|
|
|
// determine the height of all the lines and add them up.
|
|
|
|
let heightSum = 0;
|
|
|
|
items.forEach((el) => {
|
|
|
|
// getBoundingClientRect() Gets the actual fractional height instead of rounded to integer pixels
|
|
|
|
const r = el.getBoundingClientRect();
|
|
|
|
const height = r.height;
|
|
|
|
heightSum += height;
|
|
|
|
});
|
|
|
|
|
|
|
|
const heightStyle=`${heightSum}px`;
|
|
|
|
this.$refs.mainEl.style.height = heightStyle;
|
|
|
|
}
|
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
template: `
|
2023-09-08 12:47:29 +02:00
|
|
|
<div class="r-studyline r-studyline-heading "
|
2023-07-27 15:07:14 +02:00
|
|
|
:data-studyline="value.id" ref="mainEl"
|
|
|
|
><div class="r-studyline-handle" :style="'background-color: ' + value.color"></div>
|
|
|
|
<div class="r-studyline-title">
|
|
|
|
<abbr v-b-tooltip.hover :title="value.name">{{ value.shortname }}</abbr>
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('r-studyline-slot', {
|
|
|
|
props: {
|
2023-07-27 15:07:14 +02:00
|
|
|
value: {
|
|
|
|
type: Array, // item to display
|
|
|
|
default(){ return [];},
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
type : {
|
|
|
|
type: String,
|
2023-07-27 15:07:14 +02:00
|
|
|
default: 'gradable',
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
slotindex : {
|
|
|
|
type: Number,
|
|
|
|
default: 0,
|
|
|
|
},
|
2023-07-27 15:07:14 +02:00
|
|
|
line : {
|
|
|
|
type: Object,
|
|
|
|
default(){ return null;},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
2023-07-27 15:07:14 +02:00
|
|
|
layer : {
|
|
|
|
type: Number,
|
2023-09-08 12:47:29 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
plan: {
|
|
|
|
type: Object,
|
2023-07-27 15:07:14 +02:00
|
|
|
default(){ return null;},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
2023-08-07 16:11:14 +02:00
|
|
|
page: {
|
|
|
|
type: Object,
|
|
|
|
default(){ return null;},
|
2023-09-08 12:47:29 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
2023-08-03 18:44:57 +02:00
|
|
|
},
|
2023-09-08 12:47:29 +02:00
|
|
|
period: {
|
2023-08-03 18:44:57 +02:00
|
|
|
type: Object,
|
|
|
|
default(){ return null;},
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
},
|
2023-07-27 15:07:14 +02:00
|
|
|
mounted() {
|
|
|
|
const self=this;
|
|
|
|
if(self.type == "gradable" && self.slotindex == 1){
|
|
|
|
self.resizeListener = new ResizeObserver(() => {
|
|
|
|
if(self.$refs.sizeElement){
|
|
|
|
const height = self.$refs.sizeElement.getBoundingClientRect().height;
|
|
|
|
ItemEventBus.$emit('lineHeightChange', self.line.id, self.layer, height);
|
|
|
|
}
|
|
|
|
}).observe(self.$refs.sizeElement);
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
},
|
2023-07-27 15:07:14 +02:00
|
|
|
computed: {
|
|
|
|
item(){
|
|
|
|
for(const ix in this.value){
|
|
|
|
const itm = this.value[ix];
|
|
|
|
if(itm.layer == this.layer){
|
|
|
|
return itm;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
2023-08-03 18:44:57 +02:00
|
|
|
current(){
|
|
|
|
if( this.period && this.period.startdate && this.period.enddate){
|
|
|
|
const now = new Date();
|
|
|
|
const pstart = new Date(this.period.startdate);
|
|
|
|
const pend = new Date(this.period.enddate);
|
|
|
|
return (now >= pstart && now < pend);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return false;
|
|
|
|
}
|
2023-08-04 14:57:23 +02:00
|
|
|
},
|
|
|
|
spanCss(){
|
|
|
|
if(this.item && this.item.span > 1){
|
|
|
|
const span = (2 * this.item.span) - 1;
|
|
|
|
return `width: 100%; grid-column: span ${span};`;
|
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
2023-08-03 18:44:57 +02:00
|
|
|
}
|
2023-07-27 15:07:14 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
};
|
|
|
|
},
|
|
|
|
methods: {
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
template: `
|
2023-09-08 12:47:29 +02:00
|
|
|
<div :class=" 'r-studyline-slot ' + type + ' ' + (current?'current ':' ')
|
|
|
|
+ 'r-studyline-slot-' + slotindex + ' '
|
2023-07-27 15:07:14 +02:00
|
|
|
+ ((slotindex==0)?'r-studyline-firstcolumn ':' ')"
|
|
|
|
:data-studyline="line.id" ref="sizeElement"
|
2023-08-04 14:57:23 +02:00
|
|
|
:style='spanCss'
|
|
|
|
><div class="r-slot-item" v-if="item"
|
2023-09-08 12:47:29 +02:00
|
|
|
><r-item
|
|
|
|
v-model="item"
|
2023-07-27 15:07:14 +02:00
|
|
|
:plan="plan"
|
|
|
|
:guestmode='guestmode'
|
|
|
|
:teachermode='teachermode'></r-item
|
|
|
|
></div
|
|
|
|
></r-item
|
|
|
|
></div>
|
2023-05-17 21:19:14 +02:00
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Vue.component('r-item', {
|
|
|
|
props: {
|
|
|
|
value :{
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return null;},
|
2023-09-08 12:47:29 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
plan: {
|
|
|
|
type: Object,
|
|
|
|
default(){ return null;}
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
lines: [],
|
|
|
|
};
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
lineColor(){
|
|
|
|
if(this.teachermode){
|
2023-07-27 15:07:14 +02:00
|
|
|
return "var(--gray)";
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
else{
|
|
|
|
switch(this.value.completion){
|
|
|
|
default: // "incomplete"
|
2023-07-27 15:07:14 +02:00
|
|
|
return "var(--gray)";
|
2023-05-17 21:19:14 +02:00
|
|
|
case "failed":
|
2023-07-27 15:07:14 +02:00
|
|
|
return "var(--danger)";
|
2023-05-17 21:19:14 +02:00
|
|
|
case "progress":
|
2023-07-27 15:07:14 +02:00
|
|
|
return "var(--warning)";
|
2023-05-17 21:19:14 +02:00
|
|
|
case "completed":
|
2023-07-27 15:07:14 +02:00
|
|
|
return "var(--success)";
|
2023-05-17 21:19:14 +02:00
|
|
|
case "good":
|
2023-07-27 15:07:14 +02:00
|
|
|
return "var(--info)";
|
2023-05-17 21:19:14 +02:00
|
|
|
case "excellent":
|
2023-07-27 15:07:14 +02:00
|
|
|
return "var(--blue)";
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
redrawLine(conn){
|
|
|
|
let lineColor = this.lineColor();
|
|
|
|
// draw new line...
|
|
|
|
let start = document.getElementById('studyitem-'+conn.from_id);
|
|
|
|
let end= document.getElementById('studyitem-'+conn.to_id);
|
2023-11-11 20:18:05 +01:00
|
|
|
|
|
|
|
// delete old line
|
|
|
|
if(this.lines[conn.to_id]){
|
|
|
|
this.lines[conn.to_id].remove();
|
|
|
|
delete this.lines[conn.to_id];
|
|
|
|
}
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
if(start !== null && end !== null && isVisible(start) && isVisible(end)){
|
2023-11-11 20:18:05 +01:00
|
|
|
this.lines[conn.to_id] = new SimpleLine(start,end,{
|
2023-05-17 21:19:14 +02:00
|
|
|
color: lineColor,
|
2023-08-04 22:54:32 +02:00
|
|
|
gravity: {
|
|
|
|
start: LINE_GRAVITY,
|
|
|
|
end: LINE_GRAVITY,
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
redrawLines(){
|
2023-08-15 22:52:43 +02:00
|
|
|
// Clean all old lines
|
|
|
|
for(let ix in this.lines){
|
|
|
|
let lineinfo = this.lines[ix];
|
|
|
|
if(lineinfo && lineinfo.line){
|
|
|
|
lineinfo.line.remove();
|
|
|
|
lineinfo.line = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create new lines
|
2023-05-17 21:19:14 +02:00
|
|
|
for(let i in this.value.connections.out){
|
|
|
|
let conn = this.value.connections.out[i];
|
|
|
|
this.redrawLine(conn);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onWindowResize(){
|
|
|
|
this.redrawLines();
|
2023-11-11 20:18:05 +01:00
|
|
|
},
|
|
|
|
onRedrawLines(){
|
|
|
|
this.redrawLines();
|
|
|
|
},
|
2023-11-11 21:13:07 +01:00
|
|
|
removeLine(conn){
|
|
|
|
if(this.lines[conn.to_id]){
|
|
|
|
this.lines[conn.to_id].remove();
|
|
|
|
delete this.lines[conn.to_id];
|
|
|
|
}
|
|
|
|
},
|
2023-11-11 20:18:05 +01:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
hasConnectionsOut() {
|
|
|
|
return !(["finish",].includes(this.value.type));
|
|
|
|
},
|
|
|
|
hasConnectionsIn() {
|
|
|
|
return !(["start",].includes(this.value.type));
|
|
|
|
},
|
|
|
|
hasContext() {
|
|
|
|
return ['start','junction','finish'].includes(this.value.type);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
created(){
|
2023-11-11 20:18:05 +01:00
|
|
|
ItemEventBus.$on('redrawLines', this.onRedrawLines);
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
mounted(){
|
|
|
|
// Initialize connection lines when mounting
|
|
|
|
|
|
|
|
this.redrawLines();
|
|
|
|
setTimeout(()=>{
|
|
|
|
this.redrawLines();
|
|
|
|
},50);
|
|
|
|
|
|
|
|
// Add resize event listener
|
|
|
|
window.addEventListener('resize',this.onWindowResize);
|
|
|
|
},
|
|
|
|
beforeDestroy(){
|
|
|
|
for(let i in this.value.connections.out){
|
|
|
|
let conn = this.value.connections.out[i];
|
2023-11-11 20:18:05 +01:00
|
|
|
this.removeLine(conn);
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
// Remove resize event listener
|
|
|
|
window.removeEventListener('resize',this.onWindowResize);
|
2023-11-11 20:18:05 +01:00
|
|
|
ItemEventBus.$off('redrawLines', this.onRedrawLines);
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
beforeUpdate(){
|
|
|
|
},
|
|
|
|
updated(){
|
|
|
|
if(!this.dummy) {
|
|
|
|
this.redrawLines();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<div class="r-item-base" :id="'studyitem-'+value.id" :data-x='value.type'>
|
|
|
|
<r-item-competency v-if="value.type == 'competency'"
|
|
|
|
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-competency>
|
|
|
|
<r-item-course v-if="value.type == 'course' && !teachermode" :plan="plan"
|
|
|
|
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-course>
|
|
|
|
<r-item-teachercourse v-if="value.type == 'course' && teachermode" :plan="plan"
|
|
|
|
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-teachercourse>
|
|
|
|
<r-item-junction v-if="value.type == 'junction'"
|
|
|
|
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-junction>
|
|
|
|
<r-item-start v-if="value.type == 'start'"
|
|
|
|
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-start>
|
|
|
|
<r-item-finish v-if="value.type == 'finish'"
|
|
|
|
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-finish>
|
|
|
|
<r-item-badge v-if="value.type == 'badge'"
|
|
|
|
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-badge>
|
2023-09-08 12:47:29 +02:00
|
|
|
<r-item-invalid v-if="value.type == 'invalid' && teachermode"
|
2023-05-17 21:19:14 +02:00
|
|
|
v-model="value" ></r-item-invalid>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('r-item-invalid', {
|
|
|
|
props: {
|
|
|
|
'value' :{
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return null;},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.invalid,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
},
|
|
|
|
template: `
|
2023-08-22 16:47:37 +02:00
|
|
|
<div class="r-item-invalid">
|
2023-05-17 21:19:14 +02:00
|
|
|
<b-card no-body class="r-item-invalid">
|
|
|
|
<b-row no-gutters>
|
|
|
|
<b-col md="1">
|
|
|
|
<span class="r-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>
|
2023-08-22 16:47:37 +02:00
|
|
|
</div>
|
2023-05-17 21:19:14 +02:00
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-05-29 23:00:45 +02:00
|
|
|
//TAG: Item Course
|
2023-05-17 21:19:14 +02:00
|
|
|
Vue.component('r-item-course', {
|
|
|
|
props: {
|
|
|
|
value :{
|
|
|
|
type: Object,
|
|
|
|
default(){ return null;},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default(){ return false;}
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default(){ return false;}
|
2023-09-08 12:47:29 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
plan: {
|
|
|
|
type: Object,
|
|
|
|
default(){ return null;}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2023-06-16 23:12:17 +02:00
|
|
|
text: strings.course,
|
2023-05-17 21:19:14 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
2023-07-28 23:43:11 +02:00
|
|
|
startdate(){
|
|
|
|
return format_date(this.value.course.startdate);
|
|
|
|
},
|
|
|
|
enddate(){
|
|
|
|
if(this.value.course.enddate){
|
|
|
|
return format_date(this.value.course.enddate);
|
2023-09-08 12:47:29 +02:00
|
|
|
}
|
2023-07-28 23:43:11 +02:00
|
|
|
else {
|
|
|
|
return this.text.noenddate;
|
|
|
|
}
|
2023-12-11 22:30:30 +01:00
|
|
|
},
|
|
|
|
courseprogress() {
|
|
|
|
if (!this.value.course.enrolled) {
|
|
|
|
return 0;
|
|
|
|
} else if(this.value.course.completion) {
|
|
|
|
return (this.value.course.completion.progress / this.value.course.completion.count);
|
|
|
|
} else if(this.value.course.competency) {
|
|
|
|
return (this.value.course.competency.progress / this.value.course.competency.count);
|
|
|
|
} else if(this.value.course.grades) {
|
|
|
|
return (this.gradeprogress(this.value.course.grades) / this.value.course.grades.length);
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
hasprogressinfo() {
|
|
|
|
if (!this.value.course.enrolled) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
return (this.value.course.completion || this.value.course.competency || this.value.course.grades);
|
|
|
|
}
|
2023-07-28 23:43:11 +02:00
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
created(){
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
completion_icon(completion) {
|
|
|
|
switch(completion){
|
|
|
|
default: // case "incomplete"
|
|
|
|
return "circle-o";
|
|
|
|
case "pending":
|
|
|
|
return "question-circle";
|
|
|
|
case "failed":
|
2023-09-08 12:47:29 +02:00
|
|
|
return "times-circle";
|
2023-05-17 21:19:14 +02:00
|
|
|
case "progress":
|
|
|
|
return "exclamation-circle";
|
|
|
|
case "completed":
|
|
|
|
return "check-circle";
|
|
|
|
case "good":
|
|
|
|
return "check-circle";
|
|
|
|
case "excellent":
|
|
|
|
return "check-circle";
|
|
|
|
}
|
|
|
|
},
|
2023-08-09 14:18:05 +02:00
|
|
|
circle_icon(completion) {
|
|
|
|
switch(completion){
|
|
|
|
default: // case "incomplete"
|
|
|
|
return null;
|
|
|
|
case "failed":
|
2023-09-08 12:47:29 +02:00
|
|
|
return "times";
|
2023-08-09 14:18:05 +02:00
|
|
|
case "progress":
|
2023-12-11 22:56:59 +01:00
|
|
|
return "";
|
2023-08-09 14:18:05 +02:00
|
|
|
case "completed":
|
|
|
|
return "check";
|
|
|
|
case "good":
|
|
|
|
return "check";
|
|
|
|
case "excellent":
|
|
|
|
return "check";
|
|
|
|
}
|
|
|
|
},
|
2023-12-11 22:30:30 +01:00
|
|
|
gradeprogress(grades) {
|
|
|
|
let progress = 0;
|
|
|
|
for (const ix in grades) {
|
|
|
|
const g = grades[ix];
|
|
|
|
if (["completed","excellent","good"].includes(g.completion)) {
|
|
|
|
progress++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return progress;
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
template: `
|
2023-08-22 16:47:37 +02:00
|
|
|
<div :class="'r-item-competency completion-'+value.completion">
|
|
|
|
<b-card no-body >
|
2023-05-17 21:19:14 +02:00
|
|
|
<b-row no-gutters>
|
|
|
|
<b-col md="1">
|
2023-09-08 12:47:29 +02:00
|
|
|
<span
|
|
|
|
:title="text['coursetiming_'+value.course.timing]"
|
|
|
|
v-b-popover.hover.top="startdate+' - '+enddate"
|
2023-05-17 21:19:14 +02:00
|
|
|
:class="'r-timing-indicator timing-'+value.course.timing"></span>
|
|
|
|
</b-col>
|
2023-08-22 16:47:37 +02:00
|
|
|
<b-col md="11" class="align-items-center">
|
|
|
|
<b-card-body >
|
2023-08-31 07:40:55 +02:00
|
|
|
<template v-if='!value.course.enrolled'>
|
2023-09-08 12:47:29 +02:00
|
|
|
<i v-b-popover.top
|
2023-08-31 07:40:55 +02:00
|
|
|
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
|
|
|
|
:title="text.student_not_tracked"></i>
|
|
|
|
</template>
|
2023-12-11 22:30:30 +01:00
|
|
|
<template v-else-if='hasprogressinfo'>
|
2023-11-26 22:58:26 +01:00
|
|
|
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
|
2023-12-11 22:30:30 +01:00
|
|
|
:value='courseprogress'
|
|
|
|
:max='1'
|
2023-11-26 22:58:26 +01:00
|
|
|
:min='0'
|
|
|
|
:class="'r-course-result r-completion-'+value.completion"
|
|
|
|
:icon='circle_icon(value.completion)'
|
|
|
|
:title="text['completion_'+value.completion]"
|
|
|
|
></r-progress-circle>
|
|
|
|
<i v-else v-b-popover.top
|
|
|
|
:class="'r-course-result fa fa-'+completion_icon(value.completion)+
|
|
|
|
' r-completion-'+value.completion"
|
|
|
|
:title="text['completion_'+value.completion]"></i>
|
|
|
|
</template>
|
2023-07-04 23:38:08 +02:00
|
|
|
<template v-else>
|
2023-09-08 12:47:29 +02:00
|
|
|
<i v-b-popover.top
|
2023-07-04 23:38:08 +02:00
|
|
|
:class="'r-course-result fa fa-'+completion_icon(value.completion)+
|
|
|
|
' r-completion-'+value.completion"
|
|
|
|
:title="text['completion_'+value.completion]"></i>
|
|
|
|
</template>
|
2023-09-08 12:47:29 +02:00
|
|
|
<a v-b-modal="'r-item-course-details-'+value.id"
|
2023-05-17 21:19:14 +02:00
|
|
|
:href="(!guestmode)?('/course/view.php?id='+value.course.id):'#'"
|
|
|
|
@click.prevent.stop=''
|
|
|
|
>{{ value.course.displayname }}</i></a>
|
|
|
|
</b-card-body>
|
|
|
|
</b-col>
|
|
|
|
</b-row>
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
<b-modal
|
2023-09-08 12:47:29 +02:00
|
|
|
:id="'r-item-course-details-'+value.id"
|
|
|
|
:title="value.course.displayname + ' - ' + value.course.fullname"
|
2023-05-17 21:19:14 +02:00
|
|
|
size="lg"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
2023-12-11 22:30:30 +01:00
|
|
|
header-class="r-item-course-header"
|
2023-05-17 21:19:14 +02:00
|
|
|
>
|
2023-12-11 22:30:30 +01:00
|
|
|
<template #modal-header >
|
|
|
|
<div class="r-item-course-header-details">
|
|
|
|
<div>
|
|
|
|
<h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank"
|
|
|
|
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a></h1>
|
|
|
|
{{ value.course.context.path.join(" / ")}}
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
2023-12-11 22:30:30 +01:00
|
|
|
<div class="r-course-detail-header-right">
|
|
|
|
<div class="r-completion-detail-header">
|
|
|
|
<template v-if='!value.course.enrolled'>
|
|
|
|
{{text.not_enrolled}}
|
|
|
|
<i v-b-popover.top
|
2023-12-11 22:56:59 +01:00
|
|
|
class="fa fa-exclamation-triangle t-not-enrolled-alert"
|
2023-12-11 22:30:30 +01:00
|
|
|
:title="text.student_not_tracked"></i>
|
|
|
|
</template>
|
2023-12-11 22:56:59 +01:00
|
|
|
<template v-else-if='hasprogressinfo && !value.course.showprogressbar'>
|
|
|
|
{{text['completion_'+value.completion]}}
|
2023-12-11 22:30:30 +01:00
|
|
|
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
|
|
|
|
:value='courseprogress'
|
|
|
|
:max='1'
|
|
|
|
:min='0'
|
2023-12-11 22:56:59 +01:00
|
|
|
:class="'r-progress-circle-popup r-completion-'+value.completion"
|
2023-12-11 22:30:30 +01:00
|
|
|
:icon='circle_icon(value.completion)'
|
|
|
|
:title="text['completion_'+value.completion]"
|
|
|
|
></r-progress-circle>
|
|
|
|
<i v-else v-b-popover.top
|
2023-12-11 22:56:59 +01:00
|
|
|
:class="'fa fa-'+completion_icon(value.completion)+
|
2023-12-11 22:30:30 +01:00
|
|
|
' r-completion-'+value.completion"
|
|
|
|
:title="text['completion_'+value.completion]"></i>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
{{text['completion_'+value.completion]}}
|
|
|
|
<i :class="'fa fa-'+completion_icon(value.completion)+' r-completion-'+value.completion"
|
|
|
|
:title="text['completion_'+value.completion]"></i>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
<div :class="'r-timing-'+value.course.timing">
|
|
|
|
{{text['coursetiming_'+value.course.timing]}}<br>
|
|
|
|
{{ startdate }} - {{ enddate }}
|
|
|
|
</div>
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-12-11 22:30:30 +01:00
|
|
|
<s-progress-bar
|
|
|
|
v-if='value.course.showprogressbar && hasprogressinfo'
|
|
|
|
v-model="courseprogress"
|
|
|
|
></s-progress-bar>
|
2023-05-17 21:19:14 +02:00
|
|
|
</template>
|
2023-12-11 22:30:30 +01:00
|
|
|
<s-course-extrafields
|
|
|
|
v-if="value.course.extrafields"
|
|
|
|
v-model="value.course.extrafields"
|
|
|
|
position="above"
|
|
|
|
></s-course-extrafields>
|
2023-09-08 12:47:29 +02:00
|
|
|
<r-item-studentgrades
|
|
|
|
v-if='!!value.course.grades && value.course.grades.length > 0'
|
2023-05-17 21:19:14 +02:00
|
|
|
v-model='value'
|
|
|
|
:guestmode='guestmode'></r-item-studentgrades>
|
2023-05-19 19:24:10 +02:00
|
|
|
<r-item-studentcompletion
|
2023-09-08 12:47:29 +02:00
|
|
|
v-if='!!value.course.completion'
|
2023-05-17 21:19:14 +02:00
|
|
|
v-model='value.course.completion'
|
|
|
|
:course='value.course'
|
|
|
|
:guestmode='guestmode'></r-item-studentcompletion>
|
2023-11-26 22:58:26 +01:00
|
|
|
<r-item-student-course-competency
|
|
|
|
v-if='!!value.course.competency'
|
|
|
|
v-model='value.course.competency'
|
|
|
|
:item='value'
|
|
|
|
></r-item-student-course-competency>
|
2023-12-11 22:30:30 +01:00
|
|
|
<s-course-extrafields
|
|
|
|
v-if="value.course.extrafields"
|
|
|
|
v-model="value.course.extrafields"
|
|
|
|
position="below"
|
|
|
|
></s-course-extrafields>
|
2023-09-08 12:47:29 +02:00
|
|
|
</b-modal>
|
2023-08-22 16:47:37 +02:00
|
|
|
</b-card></div>
|
2023-05-17 21:19:14 +02:00
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-11-26 22:58:26 +01:00
|
|
|
//Selected activities dispaly
|
2023-05-17 21:19:14 +02:00
|
|
|
Vue.component('r-item-studentgrades',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2023-06-16 23:12:17 +02:00
|
|
|
text: strings.course,
|
2023-05-17 21:19:14 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
pendingsubmission(){
|
|
|
|
let result = false;
|
|
|
|
for(const ix in this.value.course.grades){
|
|
|
|
const g = this.value.course.grades[ix];
|
|
|
|
if(g.pendingsubmission){
|
|
|
|
result = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
useRequiredGrades() {
|
|
|
|
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
|
|
|
|
return this.plan.aggregation_info.useRequiredGrades;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return false;
|
|
|
|
}
|
2023-09-08 12:47:29 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
completion_icon(completion) {
|
|
|
|
switch(completion){
|
|
|
|
default: // case "incomplete"
|
|
|
|
return "circle-o";
|
|
|
|
case "pending":
|
|
|
|
return "question-circle";
|
|
|
|
case "failed":
|
2023-09-08 12:47:29 +02:00
|
|
|
return "times-circle";
|
2023-05-17 21:19:14 +02:00
|
|
|
case "progress":
|
|
|
|
return "exclamation-circle";
|
|
|
|
case "completed":
|
|
|
|
return "check-circle";
|
|
|
|
case "good":
|
|
|
|
return "check-circle";
|
|
|
|
case "excellent":
|
|
|
|
return "check-circle";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<table class="r-item-course-grade-details">
|
|
|
|
<tr v-for="g in value.course.grades">
|
|
|
|
<td><span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span
|
|
|
|
><a
|
2023-10-07 23:19:54 +02:00
|
|
|
:href="(!guestmode)?(g.link):undefined" target="_blank"><span v-html="g.name"></span></a>
|
2023-05-17 21:19:14 +02:00
|
|
|
<abbr v-if="useRequiredGrades && g.required" :title="text.required_goal"
|
|
|
|
:class="'s-required ' + g.completion"
|
|
|
|
><i class='fa fa-asterisk' ></i
|
|
|
|
></abbr>
|
|
|
|
</td>
|
|
|
|
<td><span :class="' r-completion-'+g.completion">{{g.grade}}</span></td>
|
|
|
|
<td><i :class="'fa fa-'+completion_icon(g.completion)+' r-completion-'+g.completion"
|
|
|
|
:title="text['completion_'+g.completion]"></i>
|
2023-09-08 12:47:29 +02:00
|
|
|
<i v-if='g.pendingsubmission' :title="text['completion_pending']"
|
2023-05-17 21:19:14 +02:00
|
|
|
class="r-pendingsubmission fa fa-clock-o"></i></td>
|
|
|
|
<td v-if="g.feedback">
|
2023-09-08 12:47:29 +02:00
|
|
|
<a v-b-modal="'r-grade-feedback-'+g.id"
|
|
|
|
href="#"
|
2023-05-17 21:19:14 +02:00
|
|
|
>{{ text["view_feedback"]}}</a>
|
|
|
|
<b-modal
|
2023-09-08 12:47:29 +02:00
|
|
|
:id="'r-grade-feedback-'+g.id"
|
2023-05-17 21:19:14 +02:00
|
|
|
size="sm"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<h2><i class="fa fa-graduation-cap"></i>{{ value.course.fullname }}</h2><br>
|
|
|
|
<span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span>{{g.name}}
|
2023-09-08 12:47:29 +02:00
|
|
|
</template>
|
2023-05-17 21:19:14 +02:00
|
|
|
<span v-html="g.feedback"></span>
|
|
|
|
</b-modal>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-11-26 22:58:26 +01:00
|
|
|
// Core completion version of student course info
|
2023-05-17 21:19:14 +02:00
|
|
|
Vue.component('r-item-studentcompletion',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
course: {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2023-09-01 12:27:56 +02:00
|
|
|
text: strings.completion,
|
2023-05-17 21:19:14 +02:00
|
|
|
};
|
|
|
|
},
|
2023-05-19 19:24:10 +02:00
|
|
|
created(){
|
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
computed: {
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
completion_icon(completion) {
|
|
|
|
switch(completion){
|
|
|
|
case "progress":
|
|
|
|
return "exclamation-circle";
|
2023-05-19 22:12:18 +02:00
|
|
|
case "complete":
|
2023-05-17 21:19:14 +02:00
|
|
|
return "check-circle";
|
2023-05-19 22:12:18 +02:00
|
|
|
case "complete-pass":
|
2023-05-17 21:19:14 +02:00
|
|
|
return "check-circle";
|
2023-05-19 22:12:18 +02:00
|
|
|
case "complete-fail":
|
|
|
|
return "times-circle";
|
|
|
|
default: // case "incomplete"
|
|
|
|
return "circle-o";
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
completion_tag(cgroup){
|
|
|
|
return cgroup.completion?'completed':'incomplete';
|
2023-09-01 12:27:56 +02:00
|
|
|
},
|
|
|
|
hasCompletions() {
|
|
|
|
if(this.value.conditions) {
|
|
|
|
for(const cgroup of this.value.conditions){
|
|
|
|
if(cgroup.items && cgroup.items.length > 0){
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
2023-09-01 23:13:50 +02:00
|
|
|
requirementHTML(requirements) {
|
|
|
|
const rqs = requirements.split(/, */);
|
|
|
|
let html = "";
|
|
|
|
for(const ix in rqs) {
|
|
|
|
const rq = rqs[ix];
|
|
|
|
html += `${rq}<br>`;
|
|
|
|
}
|
|
|
|
return html;
|
|
|
|
},
|
|
|
|
addTargetBlank(html) {
|
|
|
|
const m = /^([^<]*\< *a +)(.*)/.exec(html);
|
|
|
|
if(m){
|
|
|
|
return `${m[1]} target="_blank" ${m[2]}`;
|
|
|
|
} else {
|
|
|
|
return html;
|
|
|
|
}
|
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<table class="r-item-course-grade-details">
|
2023-09-01 12:27:56 +02:00
|
|
|
<tr v-if="hasCompletions">
|
|
|
|
<td colspan='2'
|
|
|
|
><span v-if="value.conditions.length <= 1">{{ text.aggregation_overall_one }}</span
|
|
|
|
><span v-else 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}}!
|
|
|
|
</td>
|
|
|
|
</tr>
|
2023-08-31 07:40:55 +02:00
|
|
|
<template v-for='cgroup in value.conditions' v-if='value.enabled && value.tracked'>
|
2023-05-17 21:19:14 +02:00
|
|
|
<tr>
|
2023-09-01 12:27:56 +02:00
|
|
|
<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
|
|
|
|
><span v-else>{{ text.aggregation_one }}</span>
|
|
|
|
{{ cgroup.title.toLowerCase() }}:
|
|
|
|
</th>
|
2023-07-04 23:38:08 +02:00
|
|
|
<th><r-progress-circle v-if="cgroup.progress < cgroup.count"
|
2023-05-20 18:25:22 +02:00
|
|
|
:value='cgroup.progress'
|
|
|
|
:max='cgroup.count'
|
|
|
|
:class="'r-completion-'+cgroup.status"
|
|
|
|
:title="text['completion_'+cgroup.status]"
|
2023-07-04 23:38:08 +02:00
|
|
|
></r-progress-circle>
|
|
|
|
<i v-else :class="'fa fa-check-circle r-completion-'+cgroup.status"></i>
|
|
|
|
</th>
|
2023-05-17 21:19:14 +02:00
|
|
|
</tr>
|
|
|
|
<tr v-for='ci in cgroup.items'>
|
|
|
|
<td><span v-if='guestmode'>{{ci.title}}</span>
|
2023-09-01 23:13:50 +02:00
|
|
|
<span v-else v-html='addTargetBlank(ci.details.criteria)'></span>
|
|
|
|
<a href="#" v-b-tooltip.click.hover.right.html="{ customClass: 'r-tooltip ' + ci.status}"
|
|
|
|
:title="requirementHTML(ci.details.requirement)"
|
2023-09-08 12:47:29 +02:00
|
|
|
class="text-primary"><i v-if="ci.details.requirement"
|
|
|
|
class='fa fa-question-circle'
|
2023-06-16 23:12:17 +02:00
|
|
|
></i></a>
|
2023-09-01 23:13:50 +02:00
|
|
|
<td
|
|
|
|
><span :class="' r-completion-'+ci.status">{{ci.grade}}</span>
|
|
|
|
<span v-if="ci.warning"
|
2023-09-08 12:47:29 +02:00
|
|
|
><i class="text-primary fa fa-exclamation-triangle"
|
2023-09-01 23:13:50 +02:00
|
|
|
v-b-tooltip.hover.right.click="{ customClass: 'r-tooltip info' }"
|
|
|
|
:title="ci.warning"
|
|
|
|
></i
|
|
|
|
></span
|
|
|
|
></td>
|
2023-05-17 21:19:14 +02:00
|
|
|
<td><i :class="'fa fa-'+completion_icon(ci.status)+' r-completion-'+ci.status"
|
2023-05-19 19:24:10 +02:00
|
|
|
:title="text['completion_'+ci.status]"></i>
|
2023-09-08 12:47:29 +02:00
|
|
|
<i v-if='ci.pending' :title="text['completion_pending']"
|
2023-05-17 21:19:14 +02:00
|
|
|
class="r-pendingsubmission fa fa-clock-o"></i>
|
|
|
|
</td>
|
|
|
|
<td v-if="ci.feedback">
|
2023-09-08 12:47:29 +02:00
|
|
|
<a v-b-modal="'r-grade-feedback-'+ci.id"
|
|
|
|
href="#"
|
2023-05-17 21:19:14 +02:00
|
|
|
>{{ text["view_feedback"]}}</a>
|
|
|
|
<b-modal
|
2023-09-08 12:47:29 +02:00
|
|
|
:id="'r-grade-feedback-'+ci.id"
|
2023-05-17 21:19:14 +02:00
|
|
|
size="sm"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<h2><i class="fa fa-graduation-cap"></i>{{ course.fullname }}</h2><br>
|
2023-05-19 19:24:10 +02:00
|
|
|
<span class="r-activity-icon" :title="ci.typename" v-html="ci.icon"></span>{{ci.name}}
|
2023-05-17 21:19:14 +02:00
|
|
|
</template>
|
2023-05-19 19:24:10 +02:00
|
|
|
<span v-html="ci.feedback"></span>
|
2023-05-17 21:19:14 +02:00
|
|
|
</b-modal>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</template>
|
2023-08-31 07:40:55 +02:00
|
|
|
<template v-else>
|
|
|
|
<tr v-if='! value.enabled'>
|
|
|
|
<td colspan='4'>{{text.completion_not_enabled}}</td>
|
|
|
|
</tr>
|
|
|
|
<tr v-else>
|
|
|
|
<td colspan='4'>{{text.student_not_tracked}}</td>
|
|
|
|
</tr>
|
|
|
|
</template>
|
2023-05-17 21:19:14 +02:00
|
|
|
</table>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-11-26 22:58:26 +01:00
|
|
|
//TAG: STUDENT Course competency
|
|
|
|
Vue.component('r-item-student-course-competency',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
item: {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return { id: null};},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.competency,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
created(){
|
|
|
|
|
|
|
|
},
|
|
|
|
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(competency) {
|
|
|
|
if (competency.proficient && competency.courseproficient) {
|
|
|
|
return "check-circle";
|
|
|
|
} else if (competency.proficient) {
|
|
|
|
return "check";
|
|
|
|
} else if (competency.proficient === false) {
|
|
|
|
return "times-circle";
|
|
|
|
} else {
|
|
|
|
return "circle-o";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
completion_tag(competency){
|
|
|
|
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";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
pathtags(competency) {
|
|
|
|
const path = competency.path;
|
|
|
|
let s = "";
|
|
|
|
for (const ix in path) {
|
|
|
|
const p = path[ix];
|
|
|
|
if ( ix > 0) {
|
|
|
|
s += " / ";
|
|
|
|
}
|
|
|
|
let url;
|
|
|
|
if (p.type =='competency') {
|
|
|
|
url = `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${p.id}`;
|
|
|
|
} else {
|
|
|
|
url = this.competencyurl(p);
|
|
|
|
}
|
|
|
|
|
|
|
|
s += `<a href="${url}" target="_blank">${p.title}</a>`;
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
},
|
|
|
|
competencyurl(c) {
|
|
|
|
return `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${c.id}`;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<table class="r-item-course-competency-list">
|
|
|
|
<tr v-if="value.competencies.length == 0">
|
|
|
|
<td colspan='2'>{{text.competencies_not_configured}}!
|
|
|
|
<br><a :href="'/admin/tool/lp/coursecompetencies.php?courseid='+item.course.id" target='_blank'>{{text.configure_competencies}}</a>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
<template v-else>
|
|
|
|
<tr v-for='c in value.competencies'>
|
2023-11-27 23:11:17 +01:00
|
|
|
<td>
|
2023-11-26 22:58:26 +01:00
|
|
|
<a href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a>
|
|
|
|
</td>
|
2023-11-27 23:11:17 +01:00
|
|
|
<td class='details' >
|
|
|
|
<a v-if="c.details" href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.details'></span></a>
|
|
|
|
<abbr v-if="c.required" :title="text.required"
|
|
|
|
:class="'s-required ' + + completion_tag(c)"
|
|
|
|
><i class='fa fa-asterisk' ></i
|
|
|
|
></abbr>
|
2023-11-26 22:58:26 +01:00
|
|
|
</td>
|
|
|
|
<td :colspan="(c.required)?1:2">
|
|
|
|
<span :class="'r-completion-'+completion_tag(c)">
|
|
|
|
<template v-if="!c.progress && !c.count">
|
|
|
|
<i :class="'fa fa-'+completion_icon(c)" :title="text['completion_'+completion_tag(c)]"></i>
|
|
|
|
{{ (c.proficient === null)?text.unrated:c.grade }}
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<r-progress-circle
|
|
|
|
:value='c.progress'
|
|
|
|
:max='c.count'
|
|
|
|
:min='0'
|
|
|
|
:class="'r-completion-'+completion_tag(c)"
|
|
|
|
:title="text['completion_'+completion_tag(c)]"
|
|
|
|
></r-progress-circle>
|
|
|
|
{{ (c.proficient === null)?((c.progress)?text.progress:text.unrated):c.grade }}
|
|
|
|
</template>
|
|
|
|
</span>
|
|
|
|
</td>
|
|
|
|
<td v-if="c.feedback">
|
|
|
|
<a v-b-modal="'r-competency-feedback-'+c.id"
|
|
|
|
href="#"
|
|
|
|
>{{ text["view_feedback"]}}</a>
|
|
|
|
<b-modal
|
|
|
|
:id="'r-competency-feedback-'+c.id"
|
|
|
|
size="sm"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<h2><i class="fa fa-puzzle-piece"></i>{{ c.title }}</h2><br>
|
|
|
|
</template>
|
|
|
|
<span v-html="c.feedback"></span>
|
|
|
|
</b-modal>
|
|
|
|
</td>
|
|
|
|
<b-modal :id="'modal-competency-id-'+c.id"
|
|
|
|
size="lg"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<div>
|
|
|
|
<h1><i class="fa fa-puzzle-piece"></i>
|
|
|
|
<a :href="'/admin/tool/lp/competencies.php?competencyid='+c.id" target="_blank"
|
|
|
|
>{{c.title}} {{c.details}} </a
|
|
|
|
></h1>
|
|
|
|
<div><span v-html="pathtags(c)"></span></div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
<div class="mb-2" v-if="c.description"><span v-html='c.description'></span></div>
|
|
|
|
|
|
|
|
<template v-if="c.rule && c.children">
|
|
|
|
<div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div>
|
|
|
|
<table v-if="c.children" class='r-item-course-competency-list'>
|
|
|
|
<tr class='t-item-course-competency-headers'>
|
|
|
|
<th colspan="2">{{text.heading}}</th>
|
|
|
|
<th colspan="3">{{text.results}}</th>
|
|
|
|
</tr>
|
|
|
|
<tr v-for="cc in c.children">
|
2023-11-27 23:11:17 +01:00
|
|
|
<td >
|
2023-11-26 22:58:26 +01:00
|
|
|
<a :href='competencyurl(c)' target="_blank"><span v-html='cc.title'></span></a>
|
|
|
|
</td>
|
2023-11-27 23:11:17 +01:00
|
|
|
<td class='details'>
|
|
|
|
<a v-if="cc.details" :href='competencyurl(c)' target="_blank"><span v-html='cc.details'></span></a>
|
|
|
|
<abbr v-if="c.required" :title="text.required"
|
|
|
|
:class="'s-required ' + + completion_tag(cc)"
|
|
|
|
><i class='fa fa-asterisk' ></i
|
|
|
|
></abbr>
|
2023-11-26 22:58:26 +01:00
|
|
|
</td>
|
|
|
|
<td><span :class="'r-completion-'+completion_tag(cc)"
|
|
|
|
><i :class="'fa fa-'+completion_icon(cc)" :title="text['completion_'+completion_tag(cc)]"></i>
|
|
|
|
{{ (cc.proficient === null)?text.unrated:cc.grade }}</span></td>
|
|
|
|
<td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td>
|
2023-11-27 23:11:17 +01:00
|
|
|
<td>
|
|
|
|
</td>
|
2023-11-26 22:58:26 +01:00
|
|
|
<td v-if="cc.feedback">
|
|
|
|
<a v-b-modal="'r-competency-feedback-'+cc.id"
|
|
|
|
href="#"
|
|
|
|
>{{ text["view_feedback"]}}</a>
|
|
|
|
<b-modal
|
|
|
|
:id="'r-competency-feedback-'+cc.id"
|
|
|
|
size="sm"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<h2><i class="fa fa-puzzle-piece"></i>{{ cc.title }}</h2><br>
|
|
|
|
</template>
|
|
|
|
<span v-html="cc.feedback"></span>
|
|
|
|
</b-modal>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</template>
|
|
|
|
</b-modal>
|
|
|
|
</tr>
|
|
|
|
</template>
|
|
|
|
</table>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-06-16 23:12:17 +02:00
|
|
|
//TAG: Teacher course
|
2023-05-17 21:19:14 +02:00
|
|
|
Vue.component('r-item-teachercourse', {
|
|
|
|
props: {
|
|
|
|
value :{
|
|
|
|
type: Object,
|
|
|
|
default(){ return null;}
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default(){ return false;}
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default(){ return false;}
|
|
|
|
},
|
|
|
|
plan: {
|
|
|
|
type: Object,
|
|
|
|
default(){ return null;}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2023-06-30 17:03:10 +02:00
|
|
|
text: strings.teachercourse,
|
2023-05-17 21:19:14 +02:00
|
|
|
txt: {
|
|
|
|
grading: strings.grading,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
course_grading_needed(){
|
|
|
|
return this.course_grading_state();
|
|
|
|
},
|
|
|
|
course_grading_icon(){
|
|
|
|
return this.determine_grading_icon(this.course_grading_state());
|
|
|
|
},
|
|
|
|
filtered_grades(){
|
|
|
|
return this.value.course.grades.filter(g => g.selected);
|
|
|
|
},
|
|
|
|
useRequiredGrades() {
|
|
|
|
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
|
|
|
|
return this.plan.aggregation_info.useRequiredGrades;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
2023-06-16 23:12:17 +02:00
|
|
|
isCompletable() {
|
|
|
|
let completable = false;
|
|
|
|
if(this.value.course.completion){
|
|
|
|
if(this.value.course.completion.conditions.length > 0){
|
|
|
|
completable = true;
|
|
|
|
}
|
|
|
|
} else if (this.value.course.grades){
|
|
|
|
if(this.value.course.grades.length > 0){
|
|
|
|
completable = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return completable;
|
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
progress_circle() {
|
2023-06-16 23:12:17 +02:00
|
|
|
const status = {
|
|
|
|
students: 0,
|
|
|
|
completed: 0,
|
|
|
|
completed_pass: 0,
|
|
|
|
completed_fail: 0,
|
|
|
|
ungraded: 0,
|
|
|
|
};
|
|
|
|
|
2023-11-27 23:11:17 +01:00
|
|
|
if(this.value.course.completion) {
|
|
|
|
for(const cond of this.value.course.completion.conditions) {
|
|
|
|
for(const itm of cond.items) {
|
|
|
|
if(itm.progress) {
|
2023-06-16 23:12:17 +02:00
|
|
|
status.students += itm.progress.students;
|
|
|
|
status.completed += itm.progress.completed;
|
|
|
|
status.completed_pass += itm.progress.completed_pass;
|
|
|
|
status.completed_fail += itm.progress.completed_fail;
|
2023-09-01 23:13:50 +02:00
|
|
|
status.ungraded += itm.progress.ungraded;
|
2023-06-16 23:12:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-27 23:11:17 +01:00
|
|
|
} else if (this.value.course.competency) {
|
|
|
|
status.students = this.value.course.competency.total;
|
|
|
|
status.completed = this.value.course.competency.proficient;
|
|
|
|
} else if (this.value.course.grades) {
|
|
|
|
for( const g of this.value.course.grades) {
|
|
|
|
if(g.grading) {
|
2023-06-16 23:12:17 +02:00
|
|
|
status.students += g.grading.students;
|
|
|
|
status.completed += g.grading.completed;
|
|
|
|
status.completed_pass += g.grading.completed_pass;
|
|
|
|
status.completed_fail += g.grading.completed_fail;
|
|
|
|
status.ungraded += g.grading.ungraded;
|
|
|
|
}
|
|
|
|
}
|
2023-11-27 23:11:17 +01:00
|
|
|
}
|
2023-06-16 23:12:17 +02:00
|
|
|
|
|
|
|
return status;
|
2023-07-28 23:43:11 +02:00
|
|
|
},
|
|
|
|
startdate(){
|
|
|
|
return format_date(this.value.course.startdate);
|
|
|
|
},
|
|
|
|
enddate(){
|
|
|
|
if(this.value.course.enddate){
|
|
|
|
return format_date(this.value.course.enddate);
|
2023-09-08 12:47:29 +02:00
|
|
|
}
|
2023-07-28 23:43:11 +02:00
|
|
|
else {
|
|
|
|
return this.text.noenddate;
|
|
|
|
}
|
2023-06-16 23:12:17 +02:00
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
created(){
|
2023-09-01 12:27:56 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
course_grading_state(){
|
|
|
|
let ungraded = 0;
|
|
|
|
let unknown = 0;
|
|
|
|
let graded = 0;
|
|
|
|
let allgraded = 0;
|
|
|
|
const grades = this.filtered_grades;
|
|
|
|
|
|
|
|
if(!Array.isArray(grades) || grades == 0){
|
|
|
|
return 'nogrades';
|
|
|
|
}
|
|
|
|
|
|
|
|
for(const ix in grades){
|
|
|
|
const grade = grades[ix];
|
|
|
|
if(grade.grading){
|
|
|
|
if(Number(grade.grading.ungraded) > 0){
|
|
|
|
ungraded++;
|
|
|
|
}
|
|
|
|
else if(Number(grade.grading.graded) > 0) {
|
|
|
|
if(Number(grade.grading.graded) == Number(grade.grading.students)){
|
2023-09-08 12:47:29 +02:00
|
|
|
allgraded++;
|
2023-05-17 21:19:14 +02:00
|
|
|
} else {
|
|
|
|
graded++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
unknown = true;
|
|
|
|
}
|
|
|
|
}
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
if(ungraded > 0){
|
|
|
|
return 'ungraded';
|
|
|
|
} else if(unknown) {
|
|
|
|
return 'unknown';
|
|
|
|
} else if(graded){
|
|
|
|
return 'graded';
|
|
|
|
} else if(allgraded){
|
|
|
|
return 'allgraded';
|
|
|
|
} else {
|
|
|
|
return 'unsubmitted';
|
|
|
|
}
|
|
|
|
},
|
|
|
|
determine_grading_icon(gradingstate){
|
|
|
|
switch(gradingstate){
|
|
|
|
default: // "nogrades":
|
|
|
|
return "circle-o";
|
|
|
|
case "ungraded":
|
|
|
|
return "exclamation-circle";
|
|
|
|
case "unknown":
|
|
|
|
return "question-circle-o";
|
|
|
|
case "graded":
|
|
|
|
return "check";
|
|
|
|
case "allgraded":
|
|
|
|
return "check";
|
|
|
|
case "unsubmitted":
|
|
|
|
return "dot-circle-o";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
template: `
|
2023-08-22 16:47:37 +02:00
|
|
|
<div :class="'r-item-competency '+ (value.course.amteacher?'r-course-am-teacher':'')">
|
|
|
|
<b-card no-body>
|
2023-08-04 16:54:41 +02:00
|
|
|
<div class='d-flex flex-wrap mr-0 ml-0'>
|
|
|
|
<div>
|
2023-09-08 12:47:29 +02:00
|
|
|
<span
|
|
|
|
:title="text['coursetiming_'+value.course.timing]"
|
|
|
|
v-b-popover.hover.top="startdate+' - '+enddate"
|
2023-05-17 21:19:14 +02:00
|
|
|
:class="'r-timing-indicator timing-'+value.course.timing"></span>
|
2023-08-04 16:54:41 +02:00
|
|
|
</div>
|
2023-08-22 16:47:37 +02:00
|
|
|
<div class="flex-fill align-items-center">
|
|
|
|
<b-card-body>
|
2023-09-08 12:47:29 +02:00
|
|
|
<a v-b-modal="'r-item-course-details-'+value.id"
|
2023-05-17 21:19:14 +02:00
|
|
|
:href="(!guestmode)?('/course/view.php?id='+value.course.id):'#'"
|
|
|
|
@click.prevent.stop=''
|
|
|
|
>{{ value.course.displayname }}</i></a>
|
2023-06-16 23:12:17 +02:00
|
|
|
<r-completion-circle class="r-course-graded" :disabled="!isCompletable"
|
|
|
|
v-model="progress_circle"></r-completion-circle>
|
2023-05-17 21:19:14 +02:00
|
|
|
</b-card-body>
|
2023-08-04 16:54:41 +02:00
|
|
|
</div>
|
2023-08-04 17:46:34 +02:00
|
|
|
</div>
|
2023-05-17 21:19:14 +02:00
|
|
|
<b-modal
|
|
|
|
v-if="true"
|
2023-09-08 12:47:29 +02:00
|
|
|
:id="'r-item-course-details-'+value.id"
|
|
|
|
:title="value.course.displayname + ' - ' + value.course.fullname"
|
2023-05-17 21:19:14 +02:00
|
|
|
size="lg"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<div>
|
|
|
|
<h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank"
|
|
|
|
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a>
|
2023-08-18 19:22:57 +02:00
|
|
|
<r-item-teacher-gradepicker v-model="value"
|
2023-05-29 23:00:45 +02:00
|
|
|
v-if="value.course.grades && value.course.grades.length > 0"
|
|
|
|
:useRequiredGrades="useRequiredGrades"
|
|
|
|
:plan="plan"
|
|
|
|
></r-item-teacher-gradepicker>
|
2023-09-08 12:47:29 +02:00
|
|
|
<a v-if='!!value.course.completion && value.course.amteacher'
|
2023-06-16 23:12:17 +02:00
|
|
|
:href="'/course/completion.php?id='+value.course.id" target="_blank"
|
|
|
|
:title="text.configure_completion"><i class="fa fa-gear"></i></a>
|
2023-05-17 21:19:14 +02:00
|
|
|
</h1>
|
2023-08-31 21:09:57 +02:00
|
|
|
{{ value.course.context.path.join(" / ") }}
|
2023-08-31 07:40:55 +02:00
|
|
|
<div class='mt-1 text-info'>
|
|
|
|
<span v-if='value.course.numenrolled != 1'>{{ value.course.numenrolled }} {{ text.students_from_plan_enrolled }}</span>
|
|
|
|
<span v-else> 1 {{ text.student_from_plan_enrolled }} </span>
|
|
|
|
</div>
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
|
|
|
<div class="r-course-detail-header-right">
|
|
|
|
<div class="r-completion-detail-header">
|
2023-06-16 23:12:17 +02:00
|
|
|
<r-completion-circle class="r-progress-circle-popup" :disabled="!isCompletable"
|
|
|
|
v-model="progress_circle"></r-completion-circle>
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
|
|
|
<div :class="'r-timing-'+value.course.timing">
|
|
|
|
{{ text['coursetiming_'+value.course.timing] }}<br>
|
2023-07-28 23:43:11 +02:00
|
|
|
{{ startdate }} - {{ enddate }}
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
2023-12-11 22:30:30 +01:00
|
|
|
<s-course-extrafields
|
|
|
|
v-if="value.course.extrafields"
|
|
|
|
v-model="value.course.extrafields"
|
|
|
|
position="above"
|
|
|
|
></s-course-extrafields>
|
2023-09-08 12:47:29 +02:00
|
|
|
<r-item-teachergrades
|
|
|
|
v-if='!!value.course.grades && value.course.grades.length > 0'
|
2023-05-29 23:00:45 +02:00
|
|
|
v-model='value.course'
|
|
|
|
:useRequiredGrades="useRequiredGrades"
|
|
|
|
></r-item-teachergrades>
|
|
|
|
<r-item-teachercompletion
|
2023-09-08 12:47:29 +02:00
|
|
|
v-if='!!value.course.completion'
|
2023-05-29 23:00:45 +02:00
|
|
|
v-model='value.course.completion'
|
|
|
|
:course='value.course'
|
|
|
|
></r-item-teachercompletion>
|
2023-11-26 22:58:26 +01:00
|
|
|
<r-item-teacher-course-competency
|
|
|
|
v-if='!!value.course.competency'
|
|
|
|
v-model='value.course.competency'
|
|
|
|
:item='value'
|
|
|
|
></r-item-teacher-course-competency>
|
2023-12-11 22:30:30 +01:00
|
|
|
<s-course-extrafields
|
|
|
|
v-if="value.course.extrafields"
|
|
|
|
v-model="value.course.extrafields"
|
|
|
|
position="below"
|
|
|
|
></s-course-extrafields>
|
2023-05-17 21:19:14 +02:00
|
|
|
</b-modal>
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-05-29 23:00:45 +02:00
|
|
|
</b-card>
|
2023-08-22 16:47:37 +02:00
|
|
|
</div>
|
2023-05-29 23:00:45 +02:00
|
|
|
`,
|
|
|
|
});
|
2023-05-17 21:19:14 +02:00
|
|
|
|
2023-05-29 23:00:45 +02:00
|
|
|
|
2023-11-26 22:58:26 +01:00
|
|
|
//Select activities to use in grade overview
|
|
|
|
Vue.component('r-item-teacher-gradepicker', {
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object, // Item
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
useRequiredGrades: {
|
|
|
|
type: Boolean,
|
|
|
|
default(){ return null;}
|
|
|
|
}
|
2023-05-29 23:00:45 +02:00
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.teachercourse,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
},
|
|
|
|
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);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<a v-if="value.course.canselectgradables" href='#'
|
|
|
|
v-b-modal="'r-item-course-config-'+value.id"
|
|
|
|
@click.prevent.stop=''
|
|
|
|
><i class='fa fa-cog'></i>
|
|
|
|
<b-modal v-if='value.course.canselectgradables'
|
|
|
|
:id="'r-item-course-config-'+value.id"
|
|
|
|
:title="value.course.displayname + ' - ' + value.course.fullname"
|
|
|
|
ok-only
|
|
|
|
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></h1>
|
|
|
|
{{ value.course.context.path.join(" / ")}} / {{value.course.displayname}}
|
2023-05-17 21:19:14 +02:00
|
|
|
</div>
|
2023-11-26 22:58:26 +01:00
|
|
|
<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>
|
2023-12-11 22:30:30 +01:00
|
|
|
<s-
|
2023-11-26 22:58:26 +01:00
|
|
|
</template>
|
|
|
|
<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><a
|
|
|
|
:href="g.link" target="_blank">{{g.name}}</a>
|
|
|
|
<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></s-edit-mod>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</b-form-group>
|
|
|
|
</b-modal>
|
|
|
|
</a>
|
|
|
|
`,
|
|
|
|
});
|
2023-05-29 23:00:45 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
2023-11-26 22:58:26 +01:00
|
|
|
//Selected activities dispaly
|
|
|
|
Vue.component('r-item-teachergrades',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
useRequiredGrades: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
2023-05-29 23:00:45 +02:00
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.teachercourse,
|
|
|
|
txt: {
|
|
|
|
grading: strings.grading,
|
2023-05-29 23:00:45 +02:00
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
};
|
2023-05-29 23:00:45 +02:00
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
computed: {
|
|
|
|
pendingsubmission(){
|
|
|
|
let result = false;
|
|
|
|
for(const ix in this.value.grades){
|
|
|
|
const g = this.value.grades[ix];
|
|
|
|
if(g.pendingsubmission){
|
|
|
|
result = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
filtered_grades(){
|
|
|
|
return this.value.grades.filter(g => g.selected);
|
|
|
|
},
|
2023-05-29 23:00:45 +02:00
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
methods: {
|
|
|
|
determine_grading_icon(gradingstate){
|
|
|
|
switch(gradingstate){
|
|
|
|
default: // "nogrades":
|
|
|
|
return "circle-o";
|
|
|
|
case "ungraded":
|
|
|
|
return "exclamation-circle";
|
|
|
|
case "unknown":
|
|
|
|
return "question-circle-o";
|
|
|
|
case "graded":
|
|
|
|
return "check";
|
|
|
|
case "allgraded":
|
|
|
|
return "check";
|
|
|
|
case "unsubmitted":
|
|
|
|
return "dot-circle-o";
|
2023-09-08 12:47:29 +02:00
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
},
|
|
|
|
grading_icon(grade){
|
|
|
|
return this.determine_grading_icon(this.is_grading_needed(grade));
|
|
|
|
},
|
|
|
|
is_grading_needed(grade){
|
|
|
|
if(grade.grading){
|
|
|
|
if(grade.grading.ungraded){
|
|
|
|
return 'ungraded';
|
|
|
|
}
|
|
|
|
else if(grade.grading.completed_pass || grade.grading.completed || grade.grading.completed_fail) {
|
|
|
|
if(Number(grade.grading.completed) + Number(grade.grading.completed_pass)
|
|
|
|
+ Number(grade.grading.completed_fail)
|
|
|
|
== Number(grade.grading.students)){
|
|
|
|
return 'allgraded';
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return 'graded';
|
|
|
|
}
|
2023-05-29 23:00:45 +02:00
|
|
|
}
|
|
|
|
else {
|
2023-11-26 22:58:26 +01:00
|
|
|
return 'unsubmitted';
|
2023-05-29 23:00:45 +02:00
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
} else {
|
|
|
|
return 'unknown';
|
2023-05-29 23:00:45 +02:00
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<div>
|
|
|
|
<table class="r-item-course-grade-details">
|
|
|
|
<tr v-for="g in filtered_grades">
|
|
|
|
<td><span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span
|
|
|
|
><a
|
|
|
|
:href="g.gradinglink"
|
|
|
|
target="_blank" :title="g.name"><span v-html="g.name"></span></a>
|
|
|
|
<s-edit-mod
|
|
|
|
:title="value.fullname"
|
|
|
|
@saved="(fd) => g.name = fd.get('name')"
|
|
|
|
v-if="g.cmid > 0"
|
|
|
|
:cmid="g.cmid"
|
|
|
|
:coursectxid="value.ctxid"
|
|
|
|
genericonly></s-edit-mod>
|
|
|
|
<abbr v-if="useRequiredGrades && g.required" :title="text.required_goal"
|
|
|
|
:class="'s-required ' + is_grading_needed(g)"
|
|
|
|
><i class='fa fa-asterisk' ></i
|
|
|
|
></abbr>
|
|
|
|
</td>
|
|
|
|
<td v-if='g.grading'
|
|
|
|
><i :class="'r-course-grading fa fa-'+grading_icon(g)+' r-graded-'+is_grading_needed(g)"
|
|
|
|
:title="txt.grading[is_grading_needed(g)]"></i>
|
|
|
|
</td>
|
|
|
|
<td v-if='g.grading'>
|
|
|
|
<r-completion-bar v-model="g.grading" :width="150" :height="15"></r-completion-bar>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Core completion version of student course info
|
|
|
|
Vue.component('r-item-teachercompletion',{
|
|
|
|
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++;
|
2023-05-29 23:00:45 +02:00
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
});
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
completionreport(){
|
|
|
|
return `${Config.wwwroot}/report/completion/index.php?course=${this.course.id}`;
|
2023-05-29 23:00:45 +02:00
|
|
|
}
|
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
methods: {
|
|
|
|
hasCompletions() {
|
|
|
|
if(this.value.conditions) {
|
|
|
|
for(const cgroup of this.value.conditions){
|
|
|
|
if(cgroup.items && cgroup.items.length > 0){
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
template: `
|
2023-05-29 23:00:45 +02:00
|
|
|
<table class="r-item-course-grade-details">
|
2023-11-26 22:58:26 +01:00
|
|
|
<tr v-if="hasCompletions">
|
|
|
|
<td colspan='2'
|
|
|
|
><span v-if="value.conditions.length <= 1">{{ text.aggregation_overall_one }}</span
|
|
|
|
><span v-else 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}}!
|
|
|
|
<span v-if="course.amteacher">
|
|
|
|
<br><a :href="'/course/completion.php?id='+course.id" target='_blank'>{{text.configure_completion}}</a>
|
|
|
|
</span>
|
2023-05-29 23:00:45 +02:00
|
|
|
</td>
|
|
|
|
</tr>
|
2023-11-26 22:58:26 +01:00
|
|
|
<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
|
|
|
|
><span v-else>{{ text.aggregation_one }}</span>
|
|
|
|
{{ cgroup.title.toLowerCase() }}:
|
|
|
|
</th>
|
|
|
|
</tr>
|
|
|
|
<tr v-for='ci in cgroup.items'>
|
|
|
|
<td><span v-html='ci.details.criteria'></span>
|
|
|
|
<a href="#" v-b-tooltip.click
|
|
|
|
:title="ci.details.requirement"
|
|
|
|
class='text-info'><i v-if="ci.details.requirement"
|
|
|
|
class='fa fa-question-circle'
|
|
|
|
></i></a>
|
|
|
|
</td>
|
|
|
|
<td>
|
|
|
|
<r-completion-bar v-model="ci.progress" :width="150" :height="15"></r-completion-bar>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</template>
|
|
|
|
<tr><td colspan='2' class='pt-2'>
|
|
|
|
<a target="_blank" :href='completionreport'>{{ text.view_completion_report}}
|
|
|
|
<i class='fa fa-external-link'></i></a></td></tr>
|
2023-05-29 23:00:45 +02:00
|
|
|
</table>
|
2023-11-26 22:58:26 +01:00
|
|
|
`,
|
|
|
|
});
|
2023-05-29 23:00:45 +02:00
|
|
|
|
2023-11-26 22:58:26 +01:00
|
|
|
|
2023-11-27 23:11:17 +01:00
|
|
|
//TAG: Teacher Course competency
|
2023-11-26 22:58:26 +01:00
|
|
|
Vue.component('r-item-teacher-course-competency',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
item: {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return { id: null};},
|
|
|
|
}
|
2023-05-29 23:00:45 +02:00
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.competency,
|
|
|
|
};
|
2023-05-29 23:00:45 +02:00
|
|
|
},
|
2023-11-26 22:58:26 +01:00
|
|
|
created(){
|
|
|
|
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
hasCompletions() {
|
|
|
|
if(this.value.conditions) {
|
|
|
|
for(const cgroup of this.value.conditions){
|
|
|
|
if(cgroup.items && cgroup.items.length > 0){
|
|
|
|
return true;
|
|
|
|
}
|
2023-06-16 23:12:17 +02:00
|
|
|
}
|
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
return false;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
methods: {
|
2023-11-27 23:11:17 +01:00
|
|
|
completion_icon(competency) {
|
|
|
|
if (competency.proficient && competency.courseproficient) {
|
|
|
|
return "check-circle";
|
|
|
|
} else if (competency.proficient) {
|
|
|
|
return "check";
|
|
|
|
} else if (competency.proficient === false) {
|
|
|
|
return "times-circle";
|
|
|
|
} else {
|
|
|
|
return "circle-o";
|
2023-11-26 22:58:26 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-11-27 23:11:17 +01:00
|
|
|
completion_tag(competency){
|
|
|
|
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";
|
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
pathtags(competency) {
|
|
|
|
const path = competency.path;
|
|
|
|
let s = "";
|
|
|
|
for (const ix in path) {
|
|
|
|
const p = path[ix];
|
|
|
|
if ( ix > 0) {
|
|
|
|
s += " / ";
|
|
|
|
}
|
|
|
|
let url;
|
|
|
|
if (p.type =='competency') {
|
2023-11-27 23:11:17 +01:00
|
|
|
url = `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${p.id}`;
|
2023-11-26 22:58:26 +01:00
|
|
|
} else {
|
2023-11-27 23:11:17 +01:00
|
|
|
url = this.competencyurl(p);
|
2023-11-26 22:58:26 +01:00
|
|
|
}
|
|
|
|
|
2023-11-27 23:11:17 +01:00
|
|
|
s += `<a href="${url}" target="_blank">${p.title}</a>`;
|
2023-11-26 22:58:26 +01:00
|
|
|
}
|
|
|
|
return s;
|
|
|
|
},
|
2023-11-27 23:11:17 +01:00
|
|
|
competencyurl(c) {
|
|
|
|
return `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${c.id}`;
|
|
|
|
}
|
2023-11-26 22:58:26 +01:00
|
|
|
},
|
|
|
|
template: `
|
2023-11-27 23:11:17 +01:00
|
|
|
<table class="r-item-course-competency-list">
|
2023-11-26 22:58:26 +01:00
|
|
|
<tr v-if="value.competencies.length == 0">
|
|
|
|
<td colspan='2'>{{text.competencies_not_configured}}!
|
|
|
|
<br><a :href="'/admin/tool/lp/coursecompetencies.php?courseid='+item.course.id" target='_blank'>{{text.configure_competencies}}</a>
|
2023-05-29 23:00:45 +02:00
|
|
|
</td>
|
|
|
|
</tr>
|
2023-11-26 22:58:26 +01:00
|
|
|
<template v-else>
|
|
|
|
<tr v-for='c in value.competencies'>
|
|
|
|
<td>
|
2023-11-27 23:11:17 +01:00
|
|
|
<a href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a>
|
|
|
|
</td>
|
|
|
|
<td class='details'>
|
|
|
|
<a v-if="c.details" href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.details'></span></a>
|
|
|
|
<abbr v-if="c.required" :title="text.required"
|
|
|
|
:class="'s-required ' + + completion_tag(c)"
|
|
|
|
><i class='fa fa-asterisk' ></i
|
|
|
|
></abbr>
|
|
|
|
</td>
|
|
|
|
<td><r-completion-bar v-model="c.completionstats" :width="150" :height="15"></td>
|
|
|
|
<td v-if="c.feedback">
|
|
|
|
<a v-b-modal="'r-competency-feedback-'+c.id"
|
|
|
|
href="#"
|
|
|
|
>{{ text["view_feedback"]}}</a>
|
|
|
|
<b-modal
|
|
|
|
:id="'r-competency-feedback-'+c.id"
|
|
|
|
size="sm"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<h2><i class="fa fa-puzzle-piece"></i>{{ c.title }}</h2><br>
|
|
|
|
</template>
|
|
|
|
<span v-html="c.feedback"></span>
|
|
|
|
</b-modal>
|
2023-11-26 22:58:26 +01:00
|
|
|
</td>
|
|
|
|
<b-modal :id="'modal-competency-id-'+c.id"
|
|
|
|
size="lg"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<div>
|
|
|
|
<h1><i class="fa fa-puzzle-piece"></i>
|
|
|
|
<a :href="'/admin/tool/lp/competencies.php?competencyid='+c.id" target="_blank"
|
|
|
|
>{{c.title}} {{c.details}} </a
|
|
|
|
></h1>
|
|
|
|
<div><span v-html="pathtags(c)"></span></div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
<div class="mb-2" v-if="c.description"><span v-html='c.description'></span></div>
|
|
|
|
|
|
|
|
<template v-if="c.rule && c.children">
|
|
|
|
<div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div>
|
2023-11-27 23:11:17 +01:00
|
|
|
<table v-if="c.children" class='r-item-course-competency-list'>
|
2023-11-26 22:58:26 +01:00
|
|
|
<tr class='t-item-course-competency-headers'>
|
2023-11-27 23:11:17 +01:00
|
|
|
<th colspan="2">{{text.heading}}</th>
|
|
|
|
<th colspan="3">{{text.results}}</th>
|
2023-11-26 22:58:26 +01:00
|
|
|
</tr>
|
|
|
|
<tr v-for="cc in c.children">
|
2023-11-27 23:11:17 +01:00
|
|
|
<td>
|
|
|
|
<a :href='competencyurl(c)' target="_blank"><span v-html='cc.title'></span></a>
|
|
|
|
</td>
|
|
|
|
<td class='details'>
|
|
|
|
<a v-if="cc.details" :href='competencyurl(c)' target="_blank"><span v-html='cc.details'></span></a>
|
|
|
|
<abbr v-if="c.required" :title="text.required"
|
|
|
|
:class="'s-required ' + completion_tag(cc)"
|
|
|
|
><i class='fa fa-asterisk' ></i
|
|
|
|
></abbr>
|
|
|
|
</td>
|
|
|
|
<td><r-completion-bar v-model="cc.completionstats" :width="150" :height="15"></r-completion-bar></td>
|
2023-11-26 22:58:26 +01:00
|
|
|
<td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td>
|
2023-11-27 23:11:17 +01:00
|
|
|
<td v-if="cc.feedback">
|
|
|
|
<a v-b-modal="'r-competency-feedback-'+cc.id"
|
|
|
|
href="#"
|
|
|
|
>{{ text["view_feedback"]}}</a>
|
|
|
|
<b-modal
|
|
|
|
:id="'r-competency-feedback-'+cc.id"
|
|
|
|
size="sm"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<h2><i class="fa fa-puzzle-piece"></i>{{ cc.title }}</h2><br>
|
|
|
|
</template>
|
|
|
|
<span v-html="cc.feedback"></span>
|
|
|
|
</b-modal>
|
|
|
|
</td>
|
2023-11-26 22:58:26 +01:00
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</template>
|
|
|
|
</b-modal>
|
|
|
|
</tr>
|
|
|
|
</template>
|
|
|
|
</table>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
|
|
|
|
Vue.component('r-grading-bar',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
width: {
|
|
|
|
type: Number,
|
|
|
|
default: 150,
|
|
|
|
},
|
|
|
|
height: {
|
|
|
|
type: Number,
|
|
|
|
default: 15,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.grading,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
width_unsubmitted() {
|
|
|
|
return this.width * this.fraction_unsubmitted();
|
|
|
|
},
|
|
|
|
width_graded() {
|
|
|
|
return this.width * this.fraction_graded();
|
|
|
|
},
|
|
|
|
width_ungraded() {
|
|
|
|
return this.width * this.fraction_ungraded();
|
|
|
|
},
|
|
|
|
count_unsubmitted(){
|
|
|
|
return (this.value.students - this.value.graded - this.value.ungraded);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
fraction_unsubmitted() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return 1 - ((this.value.graded + this.value.ungraded) / this.value.students);
|
|
|
|
} else {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_graded() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.graded / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_ungraded() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.ungraded / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<span class="r-grading-bar" :style="{height: height+'px'}"
|
|
|
|
><span :style="{height: height+'px', width: width_ungraded+'px'}"
|
|
|
|
class='r-grading-bar-segment r-grading-bar-ungraded'
|
|
|
|
:title="text.ungraded + ' (' + this.value.ungraded + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
><span :style="{height: height+'px', width: width_graded+'px'}"
|
|
|
|
class='r-grading-bar-segment r-grading-bar-graded'
|
|
|
|
:title="text.graded+ ' (' + this.value.graded + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
><span :style="{height: height+'px', width: width_unsubmitted+'px'}"
|
|
|
|
class='r-grading-bar-segment r-grading-bar-unsubmitted'
|
|
|
|
:title="text.unsubmitted + ' (' + count_unsubmitted + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
></span>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-06-16 23:12:17 +02:00
|
|
|
Vue.component('r-completion-bar',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {
|
|
|
|
students: 0,
|
|
|
|
completed: 0,
|
|
|
|
completed_pass: 0,
|
|
|
|
completed_fail: 0,
|
|
|
|
ungraded: 0,
|
|
|
|
};},
|
|
|
|
},
|
|
|
|
width: {
|
|
|
|
type: Number,
|
|
|
|
default: 150,
|
|
|
|
},
|
|
|
|
height: {
|
|
|
|
type: Number,
|
|
|
|
default: 15,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
text: strings.completion,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
width_incomplete() {
|
|
|
|
return this.width * this.fraction_incomplete();
|
|
|
|
},
|
|
|
|
width_completed() {
|
|
|
|
return this.width * this.fraction_completed();
|
|
|
|
},
|
|
|
|
width_completed_pass() {
|
|
|
|
return this.width * this.fraction_completed_pass();
|
|
|
|
},
|
|
|
|
width_completed_fail() {
|
|
|
|
return this.width * this.fraction_completed_fail();
|
|
|
|
},
|
|
|
|
width_ungraded() {
|
|
|
|
return this.width * this.fraction_ungraded();
|
|
|
|
},
|
|
|
|
count_incomplete(){
|
|
|
|
return (this.value.students - this.value.completed - this.value.completed_pass
|
|
|
|
- this.value.completed_fail - this.value.ungraded);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
fraction_incomplete() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return 1 - (
|
|
|
|
(this.value.completed + this.value.completed_pass +
|
|
|
|
this.value.completed_fail + this.value.ungraded) / this.value.students);
|
|
|
|
} else {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_completed() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.completed / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_completed_pass() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.completed_pass / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_completed_fail() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.completed_fail / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_ungraded() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.ungraded / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<span class="r-grading-bar" :style="{height: height+'px'}"
|
|
|
|
><span :style="{height: height+'px', width: width_ungraded+'px'}"
|
|
|
|
class='r-grading-bar-segment r-completion-bar-ungraded'
|
|
|
|
:title="text.ungraded + ' (' + this.value.ungraded + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
><span :style="{height: height+'px', width: width_completed+'px'}"
|
|
|
|
class='r-grading-bar-segment r-completion-bar-completed'
|
|
|
|
:title="text.completed + ' (' + this.value.completed + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
><span :style="{height: height+'px', width: width_completed_pass+'px'}"
|
|
|
|
class='r-grading-bar-segment r-completion-bar-completed-pass'
|
|
|
|
:title="text.completed_pass + ' (' + this.value.completed_pass + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
><span :style="{height: height+'px', width: width_completed_fail+'px'}"
|
|
|
|
class='r-grading-bar-segment r-completion-bar-completed-fail'
|
|
|
|
:title="text.completed_fail + ' (' + this.value.completed_fail + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
><span :style="{height: height+'px', width: width_incomplete+'px'}"
|
|
|
|
class='r-grading-bar-segment r-completion-bar-incomplete'
|
|
|
|
:title="text.incomplete + ' (' + count_incomplete + ')'" v-b-popover.hover.top
|
|
|
|
></span
|
|
|
|
></span>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('r-completion-circle',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {
|
|
|
|
students: 10,
|
|
|
|
completed: 2,
|
|
|
|
completed_pass: 2,
|
|
|
|
completed_fail: 2,
|
|
|
|
ungraded: 2,
|
|
|
|
};},
|
|
|
|
},
|
|
|
|
stroke: {
|
|
|
|
type: Number,
|
|
|
|
default: 0.2,
|
|
|
|
},
|
|
|
|
disabled: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
title: {
|
|
|
|
type: String,
|
|
|
|
default: "",
|
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
radius() {
|
|
|
|
return 50 - (50*this.stroke);
|
|
|
|
},
|
|
|
|
|
|
|
|
arcpath_ungraded() {
|
|
|
|
const begin = 0;
|
|
|
|
return this.arcpath(begin,this.fraction_ungraded());
|
|
|
|
},
|
|
|
|
arcpath_completed() {
|
|
|
|
const begin = this.fraction_ungraded();
|
|
|
|
return this.arcpath(begin,this.fraction_completed());
|
|
|
|
},
|
|
|
|
arcpath_completed_pass() {
|
|
|
|
const begin = this.fraction_ungraded()
|
|
|
|
+ this.fraction_completed();
|
|
|
|
return this.arcpath(begin,this.fraction_completed_pass());
|
|
|
|
},
|
|
|
|
arcpath_completed_fail() {
|
|
|
|
const begin = this.fraction_ungraded()
|
|
|
|
+ this.fraction_completed()
|
|
|
|
+ this.fraction_completed_pass();
|
|
|
|
return this.arcpath(begin,this.fraction_completed_fail());
|
|
|
|
},
|
|
|
|
arcpath_incomplete() {
|
2023-09-08 12:47:29 +02:00
|
|
|
const begin = this.fraction_ungraded()
|
2023-06-16 23:12:17 +02:00
|
|
|
+ this.fraction_completed()
|
|
|
|
+ this.fraction_completed_pass()
|
|
|
|
+ this.fraction_completed_fail();
|
|
|
|
return this.arcpath(begin,this.fraction_incomplete());
|
|
|
|
},
|
|
|
|
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
arcpath(start, end) {
|
|
|
|
const r = 50 - (50*this.stroke);
|
|
|
|
|
|
|
|
const t1 = start * 2*π;
|
|
|
|
const Δ = end * 2*π;
|
|
|
|
return svgarcpath([50,50],[r,r],[t1,Δ], 1.5*π);
|
|
|
|
},
|
|
|
|
fraction_incomplete() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return 1 - (
|
|
|
|
(this.value.completed + this.value.completed_pass +
|
|
|
|
this.value.completed_fail + this.value.ungraded) / this.value.students);
|
|
|
|
} else {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_completed() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.completed / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_completed_pass() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.completed_pass / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_completed_fail() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.completed_fail / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
fraction_ungraded() {
|
|
|
|
if(this.value.students > 0){
|
|
|
|
return this.value.ungraded / this.value.students;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<svg width="1em" height="1em" viewBox="0 0 100 100">
|
|
|
|
<title>{{title}}</title>
|
2023-09-08 12:47:29 +02:00
|
|
|
<circle cx="50" cy="50" :r="radius"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'stroke-width: ' + (stroke*100)+'; stroke: #ccc; fill: none;'"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
<path :d="arcpath_ungraded"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--warning); fill: none;'"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
<path :d="arcpath_completed"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--info); fill: none;'"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
<path :d="arcpath_completed_pass"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--success); fill: none;'"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
<path :d="arcpath_completed_fail"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--danger); fill: none;'"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
|
|
|
|
<circle v-if="disabled" cx="50" cy="50" :r="radius/2"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'fill: var(--dark);'"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
<circle v-else-if="value.ungraded > 0" cx="50" cy="50" :r="radius/2"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'fill: var(--warning);'"/>
|
|
|
|
</g>
|
|
|
|
</svg>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
Vue.component('r-item-junction',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
completion(){
|
|
|
|
if(this.value.completion){
|
|
|
|
return this.value.completion;
|
|
|
|
} else {
|
|
|
|
return "incomplete";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
|
|
|
|
},
|
|
|
|
template: `
|
2023-09-08 12:47:29 +02:00
|
|
|
<div :class="'r-item-junction r-item-filter completion-'+completion">
|
2023-05-17 21:19:14 +02:00
|
|
|
<i v-if="value.completion=='incomplete'" class="fa fa-circle-o"></i>
|
|
|
|
<i v-else-if="value.completion=='failed'" class="fa fa-times-circle"></i>
|
|
|
|
<i v-else-if="value.completion=='progress'" class="fa fa-exclamation-circle"></i>
|
|
|
|
<i v-else class="fa fa-check-circle"></i>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('r-item-finish',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
completion(){
|
|
|
|
if(this.value.completion){
|
|
|
|
return this.value.completion;
|
|
|
|
} else {
|
|
|
|
return "incomplete";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
},
|
|
|
|
template: `
|
2023-09-08 12:47:29 +02:00
|
|
|
<div :class="'r-item-finish r-item-filter completion-'+completion">
|
2023-05-17 21:19:14 +02:00
|
|
|
<i class="fa fa-stop-circle"></i>
|
2023-09-08 12:47:29 +02:00
|
|
|
</div>
|
2023-05-17 21:19:14 +02:00
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('r-item-start',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
completion(){
|
|
|
|
if(this.value.completion){
|
|
|
|
return this.value.completion;
|
|
|
|
} else {
|
|
|
|
return "incomplete";
|
|
|
|
}
|
|
|
|
}
|
2023-09-08 12:47:29 +02:00
|
|
|
},
|
2023-05-17 21:19:14 +02:00
|
|
|
created(){
|
2023-09-08 12:47:29 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
},
|
|
|
|
template: `
|
|
|
|
<div :class="'r-item-start r-item-filter completion-'+completion">
|
|
|
|
<i class="fa fa-play-circle"></i>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
Vue.component('r-item-badge',{
|
|
|
|
props: {
|
|
|
|
value : {
|
|
|
|
type: Object,
|
|
|
|
default: function(){ return {};},
|
|
|
|
},
|
|
|
|
guestmode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
teachermode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2023-11-01 23:47:54 +01:00
|
|
|
text: strings.badge,
|
2023-05-17 21:19:14 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
completion() {
|
|
|
|
return this.value.badge.issued?"completed":"incomplete";
|
|
|
|
},
|
2023-06-03 00:01:18 +02:00
|
|
|
issued_icon(){
|
|
|
|
switch(this.value.badge.issued){
|
|
|
|
default: // "nogrades":
|
|
|
|
return "circle-o";
|
|
|
|
case true:
|
|
|
|
return "check";
|
|
|
|
}
|
|
|
|
},
|
2023-06-16 23:12:17 +02:00
|
|
|
issuestats(){
|
|
|
|
// so the r-completion-bar can be used to show issuing stats
|
|
|
|
return {
|
|
|
|
students: (this.value.badge.studentcount)?this.value.badge.studentcount:0,
|
|
|
|
completed: (this.value.badge.issuedcount)?this.value.badge.issuedcount:0,
|
|
|
|
completed_pass: 0,
|
|
|
|
completed_fail: 0,
|
|
|
|
ungraded: 0,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
arcpath_issued(){
|
|
|
|
if(this.value.badge.studentcount){
|
|
|
|
const fraction = this.value.badge.issuedcount/this.value.badge.studentcount;
|
|
|
|
return this.arcpath(0,fraction);
|
|
|
|
} else {
|
|
|
|
return ""; // no path
|
|
|
|
}
|
2023-11-01 23:47:54 +01:00
|
|
|
},
|
|
|
|
arcpath_progress(){
|
|
|
|
if(this.value.badge.completion){
|
|
|
|
const fraction = this.value.badge.completion.progress/this.value.badge.completion.count;
|
|
|
|
return this.arcpath(0,fraction);
|
|
|
|
} else {
|
|
|
|
return ""; // no path
|
|
|
|
}
|
|
|
|
},
|
|
|
|
badgeinprogress(){
|
|
|
|
return ( this.value.badge.issued
|
|
|
|
|| this.teachermode
|
|
|
|
|| (this.value.badge.completion
|
|
|
|
&& this.value.badge.completion.progress >= this.value.badge.completion.count)
|
|
|
|
);
|
2023-06-16 23:12:17 +02:00
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
methods: {
|
2023-06-16 23:12:17 +02:00
|
|
|
arcpath(start, end) {
|
|
|
|
const r = 44;
|
|
|
|
|
|
|
|
const t1 = start * 2*π;
|
2023-11-01 23:47:54 +01:00
|
|
|
const Δ = (end * 2*π-0.01);
|
2023-06-16 23:12:17 +02:00
|
|
|
return svgarcpath([50,50],[r,r],[t1,Δ], 1.5*π);
|
|
|
|
},
|
2023-11-01 23:47:54 +01:00
|
|
|
addTargetBlank(html) {
|
|
|
|
const m = /^([^<]*\< *a +)(.*)/.exec(html);
|
|
|
|
if(m){
|
|
|
|
return `${m[1]} target="_blank" ${m[2]}`;
|
|
|
|
} else {
|
|
|
|
return html;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
completion_icon_rq(complete) {
|
|
|
|
if (complete) {
|
|
|
|
return "check-square-o";
|
|
|
|
} else {
|
|
|
|
return "square-o";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
completion_icon(complete) {
|
|
|
|
if (complete) {
|
|
|
|
return "check-circle";
|
|
|
|
} else {
|
|
|
|
return "times-circle";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
status(complete) {
|
|
|
|
if (complete) {
|
|
|
|
return "complete";
|
|
|
|
} else {
|
|
|
|
return "incomplete";
|
|
|
|
}
|
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
},
|
|
|
|
template: `
|
2023-09-08 12:47:29 +02:00
|
|
|
<div :class="'r-item-badge r-item-filter r-completion-'+completion" v-b-tooltip.hover :title="value.badge.name">
|
|
|
|
<a v-b-modal="'r-item-badge-details-'+value.id"
|
2023-06-03 00:01:18 +02:00
|
|
|
><svg class="r-badge-backdrop " width='50px' height='50px' viewBox="0 0 100 100">
|
|
|
|
<title>{{value.badge.name}}</title>
|
2023-06-16 23:12:17 +02:00
|
|
|
<template v-if="teachermode">
|
2023-09-08 12:47:29 +02:00
|
|
|
<circle cx="50" cy="50" r="44"
|
2023-06-16 23:12:17 +02:00
|
|
|
style="stroke: #ccc; stroke-width: 8; fill: #ddd; fill-opacity: 0.8;"/>
|
2023-09-08 12:47:29 +02:00
|
|
|
<path :d="arcpath_issued"
|
2023-06-16 23:12:17 +02:00
|
|
|
:style="'stroke-width: 8; stroke: var(--info); fill: none;'"/>
|
|
|
|
</template>
|
2023-09-08 12:47:29 +02:00
|
|
|
<circle v-else-if="value.badge.issued" cx="50" cy="50" r="46"
|
2023-06-03 00:01:18 +02:00
|
|
|
style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/>
|
2023-11-01 23:47:54 +01:00
|
|
|
<template v-else-if="value.badge.completion">
|
|
|
|
<circle cx="50" cy="50" r="44"
|
|
|
|
style="stroke: #ccc; stroke-width: 8; fill: #ddd; fill-opacity: 0.8;"/>
|
|
|
|
<path :d="arcpath_progress"
|
|
|
|
:style="'stroke-width: 8; stroke: var(--info); fill: none;'"/>
|
|
|
|
</template>
|
2023-09-08 12:47:29 +02:00
|
|
|
<circle v-else cx="50" cy="50" r="46"
|
2023-06-03 00:01:18 +02:00
|
|
|
stroke-dasharray="6 9"
|
|
|
|
style="stroke: #999; stroke-width: 6; fill: #ddd; fill-opacity: 0.8;"/>
|
|
|
|
<image class="badge-image" clip-path="circle() fill-box"
|
|
|
|
:href="value.badge.imageurl" x="12" y="12" width="76" height="76"
|
2023-11-01 23:47:54 +01:00
|
|
|
:style="(badgeinprogress)?'':'opacity: 0.4;'" />
|
2023-06-03 00:01:18 +02:00
|
|
|
</svg></a>
|
|
|
|
|
|
|
|
<b-modal
|
2023-09-08 12:47:29 +02:00
|
|
|
:id="'r-item-badge-details-'+value.id"
|
|
|
|
:title="value.badge.name"
|
2023-06-03 00:01:18 +02:00
|
|
|
size="lg"
|
|
|
|
ok-only
|
|
|
|
centered
|
|
|
|
scrollable
|
|
|
|
>
|
|
|
|
<template #modal-header>
|
|
|
|
<div>
|
|
|
|
<h1><i class="fa fa-certificate"></i>
|
|
|
|
<a :href="(!guestmode)?(value.badge.infolink):undefined" target="_blank"
|
|
|
|
>{{ value.badge.name }}</a
|
|
|
|
></h1>
|
|
|
|
</div>
|
2023-06-16 23:12:17 +02:00
|
|
|
<div class="r-course-detail-header-right" v-if="!teachermode">
|
2023-06-03 00:01:18 +02:00
|
|
|
<div class="r-completion-detail-header">
|
2023-11-01 23:47:54 +01:00
|
|
|
{{ text['completion_'+completion] }}
|
2023-06-16 23:12:17 +02:00
|
|
|
<i v-b-popover.hover :class="'fa fa-'+issued_icon+' r-completion-'+completion"
|
2023-11-01 23:47:54 +01:00
|
|
|
:title="text['completion_'+completion]"></i>
|
2023-06-03 00:01:18 +02:00
|
|
|
</div>
|
|
|
|
</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 v-if="value.badge.issued" class="list-unstyled pt-1 mb-1 border-grey border-top">
|
2023-11-01 23:47:54 +01:00
|
|
|
<li><strong><i class="fa fa-calendar-check-o r-completion-complete-pass"></i>
|
|
|
|
{{text.dateissued}}:</strong> {{ value.badge.dateissued }}</li>
|
|
|
|
<li v-if='value.badge.dateexpired'
|
|
|
|
><strong><i class="fa fa-calendar-times-o r-completion-complete"></i>
|
|
|
|
{{text.dateexpired}}:</strong> {{ value.badge.dateexpired }}</li>
|
|
|
|
<li><strong><i class="fa fa-share-alt r-completion-complete-pass"></i>
|
|
|
|
<a :href="value.badge.issuedlink">{{text.share_badge}}</a></strong> </li>
|
2023-06-03 00:01:18 +02:00
|
|
|
</ul>
|
2023-11-01 23:47:54 +01:00
|
|
|
<table v-if='value.badge.completion && !value.badge.issued' class="r-item-course-grade-details mb-2">
|
|
|
|
<tr v-if="value.badge.completion.types.length > 1">
|
|
|
|
<th colspan="2"><span v-html="value.badge.completion.title"></span></th>
|
|
|
|
</tr>
|
|
|
|
<template v-for='cgroup in value.badge.completion.types' >
|
|
|
|
<tr>
|
|
|
|
<td colspan="2" v-if="value.badge.completion.types.length > 1"
|
|
|
|
><span v-html="cgroup.title"></span></td>
|
|
|
|
<th colspan="2" v-else><span v-html="cgroup.title"></span></th>
|
|
|
|
</tr>
|
|
|
|
<template v-for='ci in cgroup.criteria'>
|
|
|
|
<tr>
|
|
|
|
<td class="pl-3"><span v-if='guestmode'><span v-html="ci.title"></span></span>
|
|
|
|
<a target='_blank' v-else-if='ci.link' :href='ci.link'
|
|
|
|
><span v-html="ci.title"></span></a>
|
|
|
|
<span v-else><span v-html="ci.title"></span></span>
|
|
|
|
<td><i :class="'fa fa-'+completion_icon(ci.completed)+' r-completion-'+status(ci.completed)"
|
|
|
|
:title="text['completion_'+status(ci.completed)]"></i>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
<template v-if="ci.requirements.length > 1">
|
|
|
|
<tr v-for="rq in ci.requirements">
|
|
|
|
<td class="pl-4" colspan="2"
|
|
|
|
><i :class="'fa fa-'+completion_icon_rq(rq.completed)+' r-completion-incomplete'"
|
|
|
|
:title="text['completion_'+status(rq.completed)]"></i>
|
|
|
|
<span class="t-badge-criteria-requirement"><span v-html="rq.title"></span></span></td>
|
|
|
|
</tr>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
</template>
|
2023-11-02 07:18:37 +01:00
|
|
|
<tr v-if="!value.badge.active" class="mt-1">
|
|
|
|
<td colspan="2" class="alert alert-warning alert-block">{{text.badgedisabled}}</td>
|
|
|
|
</tr>
|
2023-11-01 23:47:54 +01:00
|
|
|
</table>
|
2023-06-03 00:01:18 +02:00
|
|
|
<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 v-if="(!guestmode)"><strong><i class="fa fa-link"></i>
|
2023-06-16 23:12:17 +02:00
|
|
|
<a :href="value.badge.infolink" target="_blank"
|
2023-11-01 23:47:54 +01:00
|
|
|
>{{ text.badgeinfo }}</a></strong></p>
|
2023-06-16 23:12:17 +02:00
|
|
|
<p v-if="teachermode && !guestmode"
|
2023-11-01 23:47:54 +01:00
|
|
|
>{{text.badgeissuedstats}}:<br>
|
2023-06-16 23:12:17 +02:00
|
|
|
<r-completion-bar v-model="issuestats" :width="150" :height="15"></r-completion-bar>
|
|
|
|
</p>
|
2023-06-03 00:01:18 +02:00
|
|
|
</b-col></b-row>
|
|
|
|
</b-container>
|
|
|
|
</b-modal>
|
2023-09-08 12:47:29 +02:00
|
|
|
</div>
|
2023-05-17 21:19:14 +02:00
|
|
|
`,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
};
|