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

3754 lines
167 KiB
JavaScript

/* eslint no-var: "error"*/
/* eslint no-console: "off"*/
/* eslint no-unused-vars: warn */
/* eslint max-len: ["error", { "code": 160 }] */
/* eslint promise/no-nesting: "off" */
/* eslint max-depth: ["error", 6] */
/* eslint no-trailing-spaces: warn */
/* eslint-env es6*/
import {SimpleLine} from './simpleline/simpleline';
import {loadStrings} from './util/string-helper';
import {formatDate, formatDatetime, studyplanPageTiming, studyplanTiming} from './util/date-helper';
import {addBrowserButtonEvent} from './util/browserbuttonevents';
import {call} from 'core/ajax';
import notification from 'core/notification';
import {svgarcpath} from './util/svgarc';
import Debugger from './util/debugger';
import Config from 'core/config';
import {processStudyplan, objCopy} from './studyplan-processor';
import TSComponents from './treestudyplan-components';
import FitTextVue from './util/fittext-vue';
// Make π available as a constant
const π = Math.PI;
// Gravity value for arrow lines - determines how much a line is pulled in the direction of the start/end before changing direction
const LINE_GRAVITY = 1.3;
/**
* Studyline is not enrollable
* @var int
*/
const ENROLLABLE_NONE = 0;
/**
* Studyline can be enrolled into by the student
* @var int
*/
const ENROLLABLE_SELF = 1;
/**
* Studyline can be enrolled into by specific role(s)
* @var int
*/
const ENROLLABLE_ROLE = 2;
/**
* Studyline can be enrolled by user and/or role
* @var int
*/
const ENROLLABLE_SELF_ROLE = 3;
export default {
install(Vue/* ,options */) {
Vue.use(TSComponents);
Vue.use(FitTextVue);
let debug = new Debugger("treestudyplan-viewer");
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",
});
}
}
let strings = loadStrings({
report: {
loading: "loadinghelp@core",
'studyplan_past': "studyplan_past",
'studyplan_present': "studyplan_present",
'studyplan_future': "studyplan_future",
back: "back",
},
invalid: {
error: 'error',
},
grading: {
ungraded: "ungraded",
graded: "graded",
allgraded: "allgraded",
unsubmitted: "unsubmitted",
nogrades: "nogrades",
unknown: "unknown",
},
completion: {
completed: "completion_completed",
incomplete: "completion_incomplete",
'completed_pass': "completion_passed",
'completed_fail': "completion_failed",
ungraded: "ungraded",
'aggregation_all': "aggregation_all",
'aggregation_any': "aggregation_any",
'aggregation_one': "aggregation_one",
'aggregation_overall_all': "aggregation_overall_all",
'aggregation_overall_any': "aggregation_overall_any",
'aggregation_overall_one': "aggregation_overall_one",
'completion_not_configured': "completion_not_configured",
'configure_completion': "configure_completion",
'view_completion_report': "view_completion_report",
'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",
},
badge: {
'share_badge': "share_badge",
dateissued: "dateissued",
dateexpire: "dateexpire",
badgeinfo: "badgeinfo",
badgeissuedstats: "badgeissuedstats",
'completion_incomplete': "completion_incomplete_badge",
'completion_completed': "completion_completed_badge",
badgedisabled: "badgedisabled"
},
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",
'student_not_tracked': "student_not_tracked",
'not_enrolled': "not_enrolled",
noenddate: "noenddate",
},
teachercourse: {
'select_conditions': "select_conditions",
'select_grades': "select_grades",
'coursetiming_past': "coursetiming_past",
'coursetiming_present': "coursetiming_present",
'coursetiming_future': "coursetiming_future",
'grade_include': "grade_include",
'grade_require': "grade_require",
'required_goal': "required_goal",
'student_from_plan_enrolled': "student_from_plan_enrolled",
'students_from_plan_enrolled': "students_from_plan_enrolled",
noenddate: "noenddate",
},
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",
},
pageinfo: {
edit: 'period_edit',
fullname: 'studyplan_name',
shortname: 'studyplan_shortname',
startdate: 'studyplan_startdate',
enddate: 'studyplan_enddate',
description: 'studyplan_description',
duration: 'studyplan_duration',
details: 'studyplan_details',
overview: 'overviewreport:all',
oveviewperiod: 'overviewreport:period'
},
lineheader: {
'cannot_enrol': 'line_cannot_enrol',
'can_enrol': 'line_can_enrol',
'is_enrolled': 'line_is_enrolled',
enrol: 'line_enrol',
unenrol: 'line_unenrol',
enrolled: 'line_enrolled',
notenrolled: 'line_notenrolled',
'enrol_question': 'line_enrol_question',
enrollments: 'line_enrollments',
enrollment: 'line_enrollment',
info: 'info@core',
confirm: 'confirm@core',
yes: 'yes@core',
no: 'no@core',
'enrolled_in': 'line_enrolled_in',
since: 'since@core',
byname: 'byname@core',
students: 'students@core',
firstname: 'firstname@core',
lastname: 'lastname@core',
email: 'email@core',
'enrol_student_question': 'line_enrol_student_question',
'unenrol_student_question': 'line_unenrol_student_question',
}
});
/* **********************************
* *
* Treestudyplan Viewer components *
* *
************************************/
/**
* Check if element is visible
* @param {Object} elem The element to check
* @returns {boolean} True if visible
*/
function isVisible(elem) {
return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
}
// Create new eventbus for interaction between item components
const ItemEventBus = new Vue();
/*
// 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);
});
*/
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,
},
title: {
type: String,
'default': "",
},
icon: {
type: String,
}
},
data() {
return {
selectedstudyplan: null,
};
},
computed: {
range() {
return this.max - this.min;
},
fraction() {
if (this.max - this.min == 0) {
return 0;
// 0 size is always empty :)
} else {
return (this.value - this.min) / (this.max - this.min);
}
},
radius() {
return 50 - (50 * this.stroke);
},
arcpath() {
let fraction = 0;
const r = 50 - (50 * this.stroke);
if (this.max - this.min != 0) {
fraction = (this.value - this.min) / (this.max - this.min);
}
const Δ = fraction * 2 * π;
return svgarcpath([50, 50], [r, r], [0, Δ], 1.5 * π);
},
},
methods: {
},
template: `
<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>
<circle v-if="fraction >= 1.0" cx="50" cy="50" :r="radius"
:style="'opacity: 1; stroke-width: '+ (stroke*100)+'; stroke: currentcolor; fill: none;'"/>
<g v-else>
<circle cx="50" cy="50" :r="radius"
:style="'opacity: ' + bgopacity + ';stroke-width: '+ (stroke*100)+'; stroke: currentcolor; fill: none;'"/>
<path :d="arcpath"
:style="'stroke-width: ' + (stroke*100) +'; stroke: currentcolor; fill: none;'"/>
</g>
</svg>
<i v-if='icon' :class="'fa fa-'+icon"
style=" position: absolute; top: 0.42em; left: 0.43em; text-align:center; display: inline-block;
width:1em; font-size: 0.55em; "
></i>
</div>
`,
});
Vue.component('r-report', {
props: {
invitekey: {
type: String,
default() {
return null;
},
},
userid: {
type: Number,
default() {
return 0;
},
},
type: {
type: String,
default() {
return "own";
},
},
},
data() {
return {
text: strings.report,
studyplans: {
past: [],
present: [],
future: [],
},
selectedstudyplan: null,
loadingstudyplan: false,
loading: true,
};
},
computed: {
teachermode() {
return (this.type == "teaching");
},
guestmode() {
return (this.type == "invited");
},
verifiedType() {
if (!["invited", "other", "teaching", "own"].includes(this.type)) {
return "own";
} else {
return this.type;
}
},
studyplancount() {
return this.studyplans.past.length + this.studyplans.present.length + this.studyplans.future.length;
}
},
mounted() {
this.loadStudyplans();
addBrowserButtonEvent(this.backPressed);
},
methods: {
backPressed() {
debug.log("Back button pressed");
if (this.selectedstudyplan) {
debug.log("Closing studyplan");
this.deselectStudyplan();
}
},
callArgs(o) {
const args = {};
if (typeof o == 'object' && !Array.isArray(o) && o !== null) {
objCopy(args, o);
}
if (this.verifiedType == "invited") {
args.invitekey = this.invitekey;
} else if (this.verifiedType == "other") {
args.userid = this.userid;
}
return args;
},
loadStudyplans() {
const self = this;
this.loading = true;
call([{
methodname: `local_treestudyplan_list_${this.verifiedType}_studyplans`,
args: this.callArgs(),
}])[0].then(function(response) {
console.info("Loaded: plans", response);
const plans = {future: [], present: [], past: []};
for (const ix in response) {
const plan = response[ix];
const timing = studyplanTiming(plan);
plans[timing].push(plan);
}
for (const ix in plans) {
plans[ix].sort((a, b) => {
let 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;
// Load studyplan from hash if applicable
const hash = window.location.hash.replace('#', '');
const parts = hash.split("-");
if (!!parts && parts.length > 0) {
for (const k in self.studyplans) {
const list = self.studyplans[k];
for (const idx in list) {
const plan = list[idx];
if (plan.id == parts[0] && !plan.suspended) {
self.selectStudyplan(plan);
return;
}
}
}
}
// If there is but a single studyplan, select it anyway, even if it is not current...
if (this.studyplancount == 1) {
if (self.studyplans.present.length > 0) {
// Directly show the current study plan if it's the only current one
const plan = self.studyplans.present[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
} else if (self.studyplans.future.lengh > 0) {
const plan = self.studyplans.future[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
} else {
const plan = self.studyplans.past[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
}
}
return;
}).catch(notification.exception);
},
selectStudyplan(plan) {
const self = this;
this.loadingstudyplan = true;
call([{
methodname: `local_treestudyplan_get_${this.verifiedType}_studyplan`,
args: this.callArgs({
studyplanid: plan.id,
}),
}])[0].then(function(response) {
self.selectedstudyplan = processStudyplan(response);
self.loadingstudyplan = false;
window.location.hash = self.selectedstudyplan.id;
return;
}).catch(notification.exception);
},
deselectStudyplan() {
this.selectedstudyplan = null;
this.loadStudyplans(); // Reload the list of studyplans.
window.location.hash = '';
}
},
template: `
<div class='t-studyplan-container r-report-tabs'>
<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>
</div>
`,
});
Vue.component('r-studyplan', {
props: {
value: {
type: Object,
},
guestmode: {
type: Boolean,
'default': false,
},
teachermode: {
type: Boolean,
'default': false,
},
coaching: {
type: Boolean,
'default': false,
},
},
data() {
return {
selectedpageindex: -1,
text: strings.pageinfo,
};
},
computed: {
selectedpage() {
return this.value.pages[this.selectedpageindex];
},
startpageindex() {
let startpageindex = 0;
let firststart = null;
for (const ix in this.value.pages) {
const page = this.value.pages[ix];
if (studyplanPageTiming(page) == "present") {
const s = new Date(page.startdate);
if ((!firststart) || firststart > s) {
startpageindex = ix;
firststart = s;
}
}
}
return startpageindex;
},
wwwroot() {
return Config.wwwroot;
},
},
methods: {
pageduration(page) {
return formatDate(page.startdate, false) + " - " + formatDate(page.enddate, false);
},
columns(page) {
return 1 + (page.periods * 2);
},
columnsStylerule(page) {
// Uses css variables, so width for slots and filters can be configured in css
let s = "grid-template-columns: var(--studyplan-filter-width)"; // Ese css variable here
for (let i = 0; i < page.periods; i++) {
s += " var(--studyplan-course-width) var(--studyplan-filter-width)";
}
return s + ";";
},
countLineLayers(line, page) {
let maxLayer = -1;
for (let i = 0; i <= page.periods; i++) {
// Determine the amount of used layers in a studyline slot
for (const ix in line.slots[i].courses) {
const item = line.slots[i].courses[ix];
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;
}
}
}
return (maxLayer >= 0) ? (maxLayer + 1) : 1;
},
showslot(page, line, index, layeridx, type) {
// 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;
const periods = page.periods;
let show = true;
for (let i = 0; i < periods; i++) {
if (line.slots[index - i] && line.slots[index - i].courses) {
const list = line.slots[index - i].courses;
for (const ix in list) {
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;
}
}
}
}
}
}
return show;
},
selectedpageChanged(/* Params: newTabIndex, prevTabIndex */) {
ItemEventBus.$emit('redrawLines', null);
scrollCurrentIntoView(this.selectedpage.id);
},
},
mounted() {
this.$root.$emit('redrawLines');
},
updated() {
scrollCurrentIntoView(this.selectedpage.id);
ItemEventBus.$emit('lineHeightChange', null);
this.$root.$emit('redrawLines');
ItemEventBus.$emit('redrawLines');
},
template: `
<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"
:active="(pageindex == startpageindex)"
:key="page.id"
:title-item-class="'s-studyplanpage-tab '+ page.timing"
><template #title>
<span v-b-tooltip.hover :title='page.fullname'>{{page.shortname}}</span>
<a href="#" v-b-modal="'studyplanpage-info-'+page.id" class='text-info'
v-if='pageindex == selectedpageindex'
><i class='fa fa-info-circle'></i></a>
</template>
<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 v-if="!page.timeless">
<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)"
><a v-if="teachermode && selectedpage && !coaching" class="ml-2"
:href="wwwroot+'//local/treestudyplan/result-overview.php?page='+selectedpage.id"
target='_blank'><i class='fa fa-list-ul'></i>&nbsp;{{text.overview}}</a
></s-studyline-header-heading>
<r-studyline-heading v-for="(line,lineindex) in page.studylines"
:key="line.id"
:teachermode="teachermode"
:guestmode="guestmode"
v-model="page.studylines[lineindex]"
:layers='countLineLayers(line,page)+1'
:studentid="value.userid"
@enrolupdate="page.studylines[lineindex].enrol = $event"
: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="columnsStylerule(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)"
><a v-if="teachermode && selectedpage && !coaching"
v-b-tooltip.hover
:href="wwwroot+'//local/treestudyplan/result-overview.php?page='+selectedpage.id
+'&firstperiod='+index+'&lastperiod='+index"
target='_blank'
:title="text.overviewperiod"><i class='fa fa-list-ul'></i></a
></s-studyline-header-period>
<div class="s-studyline-header-filter"></div>
</template>
<!-- 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(line,page)"
><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 ':' ')
+ ((layernr == countLineLayers(line,page))?' lastlyr ':' ')"
></r-studyline-slot
><r-studyline-slot
v-if="showslot(page,line, index, layeridx, 'filter')"
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':'')
+ ((layernr == countLineLayers(line,page))?' lastlyr ':' ')"
>
</r-studyline-slot
></template
></template
></template
></div
></div
></div>
</b-tab>
</b-tabs>
</b-card>
</div>
`,
});
/*
* R-STUDYLINE-HEADER
*/
Vue.component('r-studyline-heading', {
props: {
value: {
type: Object, // Studyline
default() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
teachermode: {
type: Boolean,
'default': false,
},
layers: {
type: Number,
'default': 1,
},
studentid: {
type: Number,
},
},
data() {
return {
layerHeights: {},
text: strings.lineheader,
students: null,
canUnenrol: false,
sorting: {
asc: false,
field: 'enrolled_time'
}
};
},
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);
},
computed: {
enrollable() {
return this.value.enrol.enrollable > ENROLLABLE_NONE;
},
enrollableSelf() {
return [ENROLLABLE_SELF, ENROLLABLE_SELF_ROLE].includes(this.value.enrol.enrollable);
},
enrollableRole() {
return [ENROLLABLE_ROLE, ENROLLABLE_SELF_ROLE].includes(this.value.enrol.enrollable);
},
enrolled() {
return this.value.enrol.enrolled ? true : false;
},
canEnrol() {
return this.value.enrol.can_enrol ? true : false;
},
enrolQuestion() {
return this.text.enrol_question.replace('{$a}', this.value.name);
},
enrolledIn() {
return this.text.enrolled_in.replace('{$a}', this.value.name);
},
by() {
return this.text.byname.replace('{$a}', '');
},
enrolldate() {
return formatDatetime(this.value.enrol.enrolled_time);
},
sortedStudents() {
const self = this;
const list = Array.isArray(this.students) ? this.students : [];
list.sort((a, b) => {
let d = a;
let e = b;
if (!self.sorting.asc) {
d = b;
e = a;
}
let df = d;
let ef = e;
const field = self.sorting.field;
if (d.user && d.user.hasOwnProperty(field)) {
df = d.user;
ef = e.user;
} else if (d.enrol && d.enrol.hasOwnProperty(field)) {
df = d.enrol;
ef = e.enrol;
}
if (field == 'enrolled') {
return ((df.enrolled) ? 1 : 0) - ((ef.enrolled) ? 1 : 0);
} else if (field == "enrolled_time") {
const dvalue = (df[field] && d.enrol.enrolled) ? df[field] : 0;
const evalue = (ef[field] && e.enrol.enrolled) ? ef[field] : 0;
return dvalue - evalue;
} else {
return String(df[this.sorting.field]).localeCompare(String(ef[this.sorting.field]));
}
});
return list;
}
},
methods: {
formatDatetime,
onLineHeightChange(lineid) {
// All layers for this line have the first slot send an update message on layer height change.
// When one of those updates is received, record the height and recalculate the total height of the
// header
if (this.$refs.mainEl && (lineid == this.value.id || lineid === null)) {
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) => {
// Function 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;
}
},
enrolSelf() {
const self = this;
call([{
methodname: 'local_treestudyplan_line_enrol_self',
args: {
id: self.value.id,
},
}])[0].then(function(response) {
self.$emit('enrolupdate', response);
return;
}).catch(notification.exception);
},
enrolStudent(student) {
const self = this;
const user = student.user;
let question = self.text.enrol_student_question.replace('{$a}', `${user.firstname} ${user.lastname}`);
const options = {
okTitle: self.text.yes,
cancelTitle: self.text.no,
okVariant: "success",
cancelVariant: "danger",
};
this.$bvModal.msgBoxConfirm(question, options).then(reply => {
if (reply) {
call([{
methodname: 'local_treestudyplan_line_enrol_students',
args: {
id: self.value.id,
users: [user.id],
},
}])[0].then(function(response) {
student.enrol = response[0].enrol;
return;
}).catch(notification.exception);
}
return;
}).catch(notification.exception);
},
unenrolStudent(student) {
const self = this;
const user = student.user;
let question = self.text.unenrol_student_question.replace('{$a}', `${user.firstname} ${user.lastname}`);
const options = {
okTitle: self.text.yes,
cancelTitle: self.text.no,
okVariant: "success",
cancelVariant: "danger",
};
this.$bvModal.msgBoxConfirm(question, options).then(reply => {
if (reply) {
call([{
methodname: 'local_treestudyplan_line_unenrol_students',
args: {
id: self.value.id,
users: [user.id],
},
}])[0].then(function(response) {
student.enrol = response[0].enrol;
return;
}).catch(notification.exception);
}
return;
}).catch(notification.exception);
},
loadStudents() {
const self = this;
self.students = null;
self.canUnenrol = false;
call([{
methodname: 'local_treestudyplan_list_line_enrolled_students',
args: {
id: self.value.id,
},
}])[0].then(function(response) {
self.students = response.userinfo;
self.canUnenrol = response.can_unenrol;
return;
}).catch(notification.exception);
},
toggleSort(header) {
if (this.sorting.field == header) {
this.sorting.asc = !this.sorting.asc;
} else {
this.sorting.field = header;
this.sorting.asc = true;
}
},
},
template: `
<div class="r-studyline r-studyline-heading "
:data-studyline="value.id" ref="mainEl"
><div class="r-studyline-handle" :style="'background-color: ' + value.color"></div>
<div class="r-studyline-title"><div>
<abbr v-b-tooltip.hover :title="value.name">{{ value.shortname }}</abbr>
<template v-if="enrollable">
<template v-if="teachermode">
<br>
<a v-if="!canEnrol"
href='#' @click.prevent=""
v-b-modal="'r-enrollments-'+value.id"
:title="text.cannot_enrol"
><i class='fa fa-lock text-danger'></i>&nbsp;{{text.enrollments}}</a>
<a v-else
href='#' @click.prevent=""
v-b-modal="'r-enrollments-'+value.id"
:title="text.can_enrol"
><i class='fa fa-unlock-alt text-success'></i>&nbsp;{{text.enrollments}}</a>
<b-modal
:id="'r-enrollments-'+value.id"
@show="loadStudents"
size="xl"
ok-only
scrollable
:title="value.name"
>
<table class="r-line-enroll-userlist">
<thead>
<th><a href='#' @click.prevent="toggleSort('firstname')"
>{{text.firstname}}</a>
<i v-if="sorting.field=='firstname' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.field=='firstname' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i>
</th>
<th><a href='#' @click.prevent="toggleSort('lastname')"
>{{text.lastname}}</a>
<i v-if="sorting.field=='lastname' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.field=='lastname' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i>
</th>
<th><a href='#' @click.prevent="toggleSort('enrolled')"
>{{text.enrolled}}</a>
<i v-if="sorting.field=='enrolled' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.field=='enrolled' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i>
</th>
<th><a href='#' @click.prevent="toggleSort('enrolled_time')"
>{{text.since}}</a>
<i v-if="sorting.field=='enrolled_time' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.field=='enrolled_time' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i>
</th>
<th><a href='#' @click.prevent="toggleSort('enrolled_by')"
>{{by}}</a>
<i v-if="sorting.field=='enrolled_by' && sorting.asc" class='fa fa-sort-asc fa-fw'></i
><i v-else-if="sorting.field=='enrolled_by' && !sorting.asc" class='fa fa-sort-desc fa-fw'></i>
</th>
<th v-if="canEnrol || canUnenrol">{{text.enrol}}</th>
</thead>
<tbody>
<tr v-if="students == null"><td
:colspan="4+(canEnrol?1:0)">
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
</td></tr>
<template v-else>
<tr v-for="student in sortedStudents">
<td>{{student.user.firstname}}</td>
<td>{{student.user.lastname}}</td>
<td><span v-if="student.enrol.enrolled" class="text-success">{{text.enrolled}}</span
><span v-else class="text-danger">{{text.notenrolled}}</span></td>
<td><span v-if="student.enrol.enrolled"
>{{formatDatetime(student.enrol.enrolled_time)}}</span></td>
<td><span v-if="student.enrol.enrolled"
>{{student.enrol.enrolled_by}}</span></td>
<td ><b-button v-if="!student.enrol.enrolled && (canEnrol || canUnenrol)"
variant="success"
size="sm"
@click="enrolStudent(student)"
>{{text.enrol}}</b-button
><b-button v-else-if="student.enrol.enrolled && canUnenrol"
variant="danger"
size="sm"
@click="unenrolStudent(student)"
>{{text.unenrol}}</b-button>
</td>
</tr>
</template>
</tbody>
</table>
</b-modal>
</template>
<template v-else-if="value.enrol.selfview">
<br>
<a v-if="!enrolled && !canEnrol"
@click.prevent=""
href='#'
v-b-tooltip.focus
:title="text.cannot_enrol"
><fittext maxsize="12pt"><i class='fa fa-lock text-danger'></i>&nbsp;{{text.cannot_enrol}}</fittext></a>
<a v-else-if="!enrolled && canEnrol"
@click.prevent=""
href='#'
v-b-modal="'r-enrol-'+value.id"
:title="text.can_enrol"
><fittext maxsize="12pt"><i class='fa fa-unlock-alt text-info'></i>&nbsp;{{text.enrol}}</fittext></a>
<a v-else-if="enrolled"
@click.prevent=""
href='#'
v-b-modal="'r-enrollment-'+value.id"
:title="text.enrolled"
><fittext maxsize="12pt"><i class='fa fa-unlock text-success'></i>&nbsp;{{text.enrolled}}</fittext></a>
<b-modal
:id="'r-enrol-'+value.id"
:title="text.confirm"
:ok-title="text.yes"
:cancel-title="text.no"
ok-variant="success"
cancel-variant="danger"
@ok="enrolSelf"
>
<p>{{enrolQuestion}}</p>
</b-modal>
<b-modal
:id="'r-enrollment-'+value.id"
ok-only
:title="text.enrollment"
>
<p>{{enrolledIn}}<p>
<p><b>{{text.since}}</b> {{enrolldate}}<br>
<b>{{by}}</b> {{this.value.enrol.enrolled_by}}</p>
</b-modal>
</template>
<template v-else>
&nbsp;
<i v-if="!enrolled" class='fa fa-lock text-danger'></i>
<i v-else class='fa fa-unlock text-success'></i>
</template>
</template>
</div></div>
</div>
`,
});
Vue.component('r-studyline-slot', {
props: {
value: {
type: Array, // Item to display
default() {
return [];
},
},
type: {
type: String,
'default': 'gradable',
},
slotindex: {
type: Number,
'default': 0,
},
line: {
type: Object,
default() {
return null;
},
},
layer: {
type: Number,
},
plan: {
type: Object,
default() {
return null;
},
},
page: {
type: Object,
default() {
return null;
},
},
guestmode: {
type: Boolean,
'default': false,
},
teachermode: {
type: Boolean,
'default': false,
},
period: {
type: Object,
default() {
return null;
},
}
},
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);
}
},
computed: {
item() {
for (const ix in this.value) {
const itm = this.value[ix];
if (itm.layer == this.layer) {
return itm;
}
}
return null;
},
spanCss() {
if (this.item && this.item.span > 1) {
const span = (2 * this.item.span) - 1;
return `width: 100%; grid-column: span ${span};`;
} else {
return "";
}
},
cloud() {
const enrol = this.line.enrol;
return (!this.teachermode) && (enrol.enrollable > 0) && (!enrol.enrolled);
}
},
data() {
return {
};
},
methods: {
},
template: `
<div :class=" 'r-studyline-slot ' + type + ' '
+ 'r-studyline-slot-' + slotindex + ' '
+ 'periodcount-' + page.periods + ' '
+ ((slotindex==0)?'r-studyline-firstcolumn ':' ')"
:data-studyline="line.id" ref="sizeElement"
:style='spanCss'
><div class="r-slot-item" v-if="item"
><r-item
v-model="item"
:cloud="cloud"
:plan="plan"
:guestmode='guestmode'
:teachermode='teachermode'></r-item
></div
></r-item
></div>
`,
});
Vue.component('r-item', {
props: {
value: {
type: Object,
default() {
return null;
},
},
plan: {
type: Object,
default() {
return null;
}
},
guestmode: {
type: Boolean,
'default': false,
},
teachermode: {
type: Boolean,
'default': false,
},
cloud: {
type: Boolean,
'default': false,
}
},
data() {
return {
lines: [],
};
},
methods: {
lineColor() {
if (this.teachermode) {
return "var(--gray)";
} else if (this.cloud) {
return "#ccc";
} else {
switch (this.value.completion) {
default: // "incomplete"
return "var(--gray)";
case "failed":
return "var(--danger)";
case "progress":
return "var(--warning)";
case "completed":
return "var(--success)";
case "good":
return "var(--info)";
case "excellent":
return "var(--blue)";
}
}
},
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);
// Delete old line
if (this.lines[conn.to_id]) {
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
}
if (start !== null && end !== null && isVisible(start) && isVisible(end)) {
this.lines[conn.to_id] = new SimpleLine(start, end, {
color: lineColor,
gravity: {
start: LINE_GRAVITY,
end: LINE_GRAVITY,
},
'class': (this.cloud ? "r-dummy-line" : ""),
});
}
},
redrawLines() {
// 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
for (let i in this.value.connections.out) {
let conn = this.value.connections.out[i];
this.redrawLine(conn);
}
},
onWindowResize() {
this.redrawLines();
},
onRedrawLines() {
this.redrawLines();
},
removeLine(conn) {
if (this.lines[conn.to_id]) {
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
}
},
},
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() {
ItemEventBus.$on('redrawLines', this.onRedrawLines);
},
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];
this.removeLine(conn);
}
// Remove resize event listener
window.removeEventListener('resize', this.onWindowResize);
ItemEventBus.$off('redrawLines', this.onRedrawLines);
},
updated() {
if (!this.dummy) {
this.redrawLines();
}
},
template: `
<div class="r-item-base" :id="'studyitem-'+value.id" :data-x='value.type'>
<template v-if="cloud">
<r-item-dummy-course v-if="value.type == 'course'"></r-item-dummy-course>
<r-item-dummy-badge v-else-if="value.type == 'badge'"></r-item-dummy-badge>
<r-item-dummy-filter v-else></r-item-dummy-filter>
</template>
<template v-else>
<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>
<r-item-invalid v-if="value.type == 'invalid' && teachermode"
v-model="value" ></r-item-invalid>
</template>
</div>
`,
});
Vue.component('r-item-invalid', {
props: {
'value': {
type: Object,
default() {
return null;
},
},
},
data() {
return {
text: strings.invalid,
};
},
methods: {
},
template: `
<div class="r-item-invalid">
<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>
</div>
`,
});
// TAG: Item Course
Vue.component('r-item-course', {
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 {
text: strings.course,
};
},
computed: {
startdate() {
return formatDate(this.value.course.startdate);
},
enddate() {
if (this.value.course.enddate > 0) {
return formatDate(this.value.course.enddate);
} else {
return this.text.noenddate;
}
},
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);
}
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
completionIcon(completion) {
switch (completion) {
default: // Case "incomplete"
return "circle-o";
case "pending":
return "question-circle";
case "failed":
return "times-circle";
case "progress":
return "exclamation-circle";
case "completed":
return "check-circle";
case "good":
return "check-circle";
case "excellent":
return "check-circle";
}
},
circleIcon(completion) {
switch (completion) {
default: // Case "incomplete"
return null;
case "failed":
return "times";
case "progress":
return "";
case "completed":
return "check";
case "good":
return "check";
case "excellent":
return "check";
}
},
gradeprogress(grades) {
let progress = 0;
for (const ix in grades) {
const g = grades[ix];
if (["completed", "excellent", "good"].includes(g.completion)) {
progress++;
}
}
return progress;
},
},
template: `
<div :class="'r-item-course card completion-'+value.completion">
<div class='r-item-course-cardwrapper mr-0 ml-0 h-100 '>
<div
:title="text['coursetiming_'+value.course.timing]"
v-b-popover.hover.top="startdate+' - '+enddate"
:class="'r-timing-indicator timing-'+value.course.timing"
></div>
<div class="r-item-course-title card-body h-100">
<fittext maxsize="12pt" minsize="9pt">
<a v-b-modal="'r-item-course-details-'+value.id"
:href="(!guestmode)?(wwwroot+'/course/view.php?id='+value.course.id):'#'"
@click.prevent.stop=''
>{{ value.course.displayname }}</a>
</fittext>
</div>
<div class="h-100 r-item-course-indicator ">
<template v-if='!value.course.enrolled'>
<i v-b-popover.top
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
:title="text.student_not_tracked"></i>
</template>
<template v-else-if='hasprogressinfo'>
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
:value='courseprogress'
:max='1'
:min='0'
:class="'r-course-result r-completion-'+value.completion"
:icon='circleIcon(value.completion)'
:title="text['completion_'+value.completion]"
></r-progress-circle>
<i v-else v-b-popover.top
:class="'r-course-result fa fa-'+completionIcon(value.completion)+
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
<template v-else>
<i v-b-popover.top
:class="'r-course-result fa fa-'+completionIcon(value.completion)+
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
</div>
</div>
<b-modal
:id="'r-item-course-details-'+value.id"
:title="value.course.displayname + ' - ' + value.course.fullname"
size="lg"
ok-only
centered
scrollable
header-class="r-item-course-header"
>
<template #modal-header >
<div class="r-item-course-header-details">
<div>
<h1><a :href="(!guestmode)?(wwwroot+'/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(" / ")}}
</div>
<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
class="fa fa-exclamation-triangle t-not-enrolled-alert"
:title="text.student_not_tracked"></i>
</template>
<template v-else-if='hasprogressinfo && !value.course.showprogressbar'>
{{text['completion_'+value.completion]}}
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
:value='courseprogress'
:max='1'
:min='0'
:class="'r-progress-circle-popup r-completion-'+value.completion"
:icon='circleIcon(value.completion)'
:title="text['completion_'+value.completion]"
></r-progress-circle>
<i v-else v-b-popover.top
:class="'fa fa-'+completionIcon(value.completion)+
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
<template v-else>
{{text['completion_'+value.completion]}}
<i :class="'fa fa-'+completionIcon(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>
</div>
</div>
<s-progress-bar
v-if='value.course.showprogressbar && hasprogressinfo'
v-model="courseprogress"
></s-progress-bar>
</template>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="above"
></s-course-extrafields>
<r-item-studentgrades
v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value'
:guestmode='guestmode'></r-item-studentgrades>
<r-item-studentcompletion
v-if='!!value.course.completion'
v-model='value.course.completion'
:course='value.course'
:guestmode='guestmode'></r-item-studentcompletion>
<r-item-student-course-competency
v-if='!!value.course.competency'
v-model='value.course.competency'
:item='value'
></r-item-student-course-competency>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="below"
></s-course-extrafields>
</b-modal>
</div>
`,
});
// Selected activities dispaly
Vue.component('r-item-studentgrades', {
props: {
value: {
type: Object,
default() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
},
data() {
return {
text: strings.course,
};
},
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;
}
},
},
methods: {
completionIcon(completion) {
switch (completion) {
default: // Case "incomplete"
return "circle-o";
case "pending":
return "question-circle";
case "failed":
return "times-circle";
case "progress":
return "exclamation-circle";
case "completed":
return "check-circle";
case "good":
return "check-circle";
case "excellent":
return "check-circle";
}
},
},
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
:href="(!guestmode)?(g.link):undefined" target="_blank"><span v-html="g.name"></span></a>
<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-'+completionIcon(g.completion)+' r-completion-'+g.completion"
:title="text['completion_'+g.completion]"></i>
<i v-if='g.pendingsubmission' :title="text['completion_pending']"
class="r-pendingsubmission fa fa-clock-o"></i></td>
<td v-if="g.feedback">
<a v-b-modal="'r-grade-feedback-'+g.id"
href="#"
@click.prevent.stop=""
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-grade-feedback-'+g.id"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h3><i class="fa fa-graduation-cap"></i>{{ value.course.fullname }}</h3><br>
<span class="r-activity-icon" :title="g.typename" v-html="g.icon + g.name"></span>
</div>
</template>
<span v-html="g.feedback"></span>
</b-modal>
</td>
</tr>
</table>
`,
});
// Core completion version of student course info
Vue.component('r-item-studentcompletion', {
props: {
value: {
type: Object,
default() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
course: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
text: strings.completion,
};
},
computed: {
},
methods: {
completionIcon(completion) {
switch (completion) {
case "progress":
return "exclamation-circle";
case "complete":
return "check-circle";
case "complete-pass":
return "check-circle";
case "complete-fail":
return "times-circle";
default: // Case "incomplete"
return "circle-o";
}
},
completionTag(cgroup) {
return cgroup.completion ? 'completed' : 'incomplete';
},
hasCompletions() {
if (this.value.conditions) {
for (const cgroup of this.value.conditions) {
if (cgroup.items && cgroup.items.length > 0) {
return true;
}
}
}
return false;
},
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;
}
}
},
template: `
<table class="r-item-course-grade-details">
<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>
<template v-for='cgroup in value.conditions' v-if='value.enabled && value.tracked'>
<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>
<th><r-progress-circle v-if="cgroup.progress < cgroup.count"
:value='cgroup.progress'
:max='cgroup.count'
:class="'r-completion-'+cgroup.status"
:title="text['completion_'+cgroup.status]"
></r-progress-circle>
<i v-else :class="'fa fa-check-circle r-completion-'+cgroup.status"></i>
</th>
</tr>
<tr v-for='ci in cgroup.items'>
<td><span v-if='guestmode'><span v-html="ci.title"></span></span>
<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)"
class="text-primary"><i v-if="ci.details.requirement"
class='fa fa-question-circle'
></i></a>
<td
><span :class="' r-completion-'+ci.status">{{ci.grade}}</span>
<span v-if="ci.warning"
><i class="text-primary fa fa-exclamation-triangle"
v-b-tooltip.hover.right.click="{ customClass: 'r-tooltip info' }"
:title="ci.warning"
></i
></span
></td>
<td><i :class="'fa fa-'+completionIcon(ci.status)+' r-completion-'+ci.status"
:title="text['completion_'+ci.status]"></i>
<i v-if='ci.pending' :title="text['completion_pending']"
class="r-pendingsubmission fa fa-clock-o"></i>
</td>
<td v-if="ci.feedback">
<a v-b-modal="'r-grade-feedback-'+ci.id"
href="#"
@click.prevent.stop=""
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-grade-feedback-'+ci.id"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h3><i class="fa fa-graduation-cap"></i>{{ course.fullname }}</h3>
<span class="r-activity-icon" :title="ci.typename" v-html="ci.icon + ci.name"></span>
</div>
</template>
<span v-html="ci.feedback"></span>
</b-modal>
</td>
</tr>
</template>
<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>
</table>
`,
});
// TAG: STUDENT Course competency
Vue.component('r-item-student-course-competency', {
props: {
value: {
type: Object,
default() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
item: {
type: Object,
default() {
return {id: null};
},
}
},
data() {
return {
text: strings.competency,
};
},
computed: {
hasCompletions() {
if (this.value.conditions) {
for (const cgroup of this.value.conditions) {
if (cgroup.items && cgroup.items.length > 0) {
return true;
}
}
}
return false;
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
completionIcon(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";
}
},
completionTag(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 = Config.wwwroot + `/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 Config.wwwroot + `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${c.id}`;
},
usercompetencyurl(c) {
return Config.wwwroot + `/admin/tool/lp/user_competency.php?id=${c.ucid}`;
}
},
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="wwwroot+'/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'>
<td>
<a href="#" v-b-modal="'modal-competency-id-'+c.id" @click.prevent.stop=''><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" @click.prevent.stop='' ><span v-html='c.details'></span></a>
<abbr v-if="c.required" :title="text.required"
:class="'s-required ' + + completionTag(c)"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td :colspan="(c.required)?1:2">
<span :class="'r-completion-'+completionTag(c)">
<template v-if="!c.progress && !c.count">
<i :class="'fa fa-'+completionIcon(c)" :title="text['completion_'+completionTag(c)]"></i>
{{ (c.proficient === null)?text.unrated:c.grade }}
</template>
<template v-else>
<r-progress-circle v-if='!c.proficient'
:value='c.progress'
:max='c.count'
:min='0'
:class="'r-completion-'+completionTag(c)"
:title="text['completion_'+completionTag(c)]"
></r-progress-circle>
<i v-else :class="'fa fa-'+completionIcon(c)" :title="text['completion_'+completionTag(c)]"></i>
{{ (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"
@click.prevent.stop=''
href="#"
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-competency-feedback-'+c.id"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h3><i class="fa fa-puzzle-piece"></i>{{ c.title }}</h3>
</div>
</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="wwwroot+'/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">
<td >
<a :href='usercompetencyurl(cc)' target="_blank"><span v-html='cc.title'></span></a>
</td>
<td class='details'>
<a v-if="cc.details" :href='usercompetencyurl(cc)' target="_blank"><span v-html='cc.details'></span></a>
<abbr v-if="c.required" :title="text.required"
:class="'s-required ' + + completionTag(cc)"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td><span :class="'r-completion-'+completionTag(cc)"
><i :class="'fa fa-'+completionIcon(cc)" :title="text['completion_'+completionTag(cc)]"></i>
{{ (cc.proficient === null)?text.unrated:cc.grade }}</span></td>
<td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td>
<td>
</td>
<td v-if="cc.feedback">
<a v-b-modal="'r-competency-feedback-'+cc.id"
href="#"
@click.prevent.stop=""
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-competency-feedback-'+cc.id"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h3><i class="fa fa-puzzle-piece"></i>{{ cc.title }}</h3><br>
</div>
</template>
<span v-html="cc.feedback"></span>
</b-modal>
</td>
</tr>
</table>
</template>
</b-modal>
</tr>
</template>
</table>
`,
});
// TAG: Teacher course
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 {
text: strings.teachercourse,
txt: {
grading: strings.grading,
}
};
},
computed: {
courseGradingNeeded() {
return this.courseGradingState();
},
courseGradingIcon() {
return this.determineGradingIcon(this.courseGradingState());
},
filteredGrades() {
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;
}
},
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;
},
progressCircle() {
const status = {
students: 0,
completed: 0,
completedPass: 0,
completedFail: 0,
ungraded: 0,
};
if (this.value.course.completion) {
for (const cond of this.value.course.completion.conditions) {
for (const itm of cond.items) {
if (itm.progress) {
status.students += itm.progress.students;
status.completed += itm.progress.completed;
status.completedPass += itm.progress.completed_pass;
status.completedFail += itm.progress.completed_fail;
status.ungraded += itm.progress.ungraded;
}
}
}
} 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) {
status.students += g.grading.students;
status.completed += g.grading.completed;
status.completedPass += g.grading.completed_pass;
status.completedFail += g.grading.completed_fail;
status.ungraded += g.grading.ungraded;
}
}
}
return status;
},
startdate() {
return formatDate(this.value.course.startdate);
},
enddate() {
if (this.value.course.enddate > 0) {
return formatDate(this.value.course.enddate);
} else {
return this.text.noenddate;
}
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
courseGradingState() {
let ungraded = 0;
let unknown = 0;
let graded = 0;
let allgraded = 0;
const grades = this.filteredGrades;
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)) {
allgraded++;
} else {
graded++;
}
}
} else {
unknown = true;
}
}
if (ungraded > 0) {
return 'ungraded';
} else if (unknown) {
return 'unknown';
} else if (graded) {
return 'graded';
} else if (allgraded) {
return 'allgraded';
} else {
return 'unsubmitted';
}
},
determineGradingIcon(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: `
<div :class="'r-item-course card '+ (value.course.amteacher?'r-course-am-teacher':'')">
<div class='r-item-course-cardwrapper mr-0 ml-0 h-100 '>
<div
:title="text['coursetiming_'+value.course.timing]"
v-b-popover.hover.top="startdate+' - '+enddate"
:class="'r-timing-indicator timing-'+value.course.timing"
></div>
<div class="r-item-course-title card-body h-100">
<fittext maxsize="12pt" minsize="9pt">
<a v-b-modal="'r-item-course-details-'+value.id"
:href="(!guestmode)?(wwwroot+'/course/view.php?id='+value.course.id):'#'"
@click.prevent.stop=''
>{{ value.course.displayname }}</a>
</fittext>
</div>
<div class="h-100 r-item-course-indicator ">
<r-completion-circle class="r-course-graded" :disabled="!isCompletable"
v-model="progressCircle"></r-completion-circle>
</div>
</div>
<b-modal
v-if="true"
:id="'r-item-course-details-'+value.id"
:title="value.course.displayname + ' - ' + value.course.fullname"
size="lg"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h1><a :href="(!guestmode)?(wwwroot+'/course/view.php?id='+value.course.id):undefined" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a>
<r-item-teacher-gradepicker v-model="value"
v-if="value.course.grades && value.course.grades.length > 0"
:useRequiredGrades="useRequiredGrades"
:plan="plan"
></r-item-teacher-gradepicker>
<a v-if='!!value.course.completion && value.course.amteacher'
:href="wwwroot+'/course/completion.php?id='+value.course.id" target="_blank"
:title="text.configure_completion"><i class="fa fa-gear"></i></a>
</h1>
{{ value.course.context.path.join(" / ") }}
<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>
</div>
<div class="r-course-detail-header-right">
<div class="r-completion-detail-header">
<r-completion-circle class="r-progress-circle-popup" :disabled="!isCompletable"
v-model="progressCircle"></r-completion-circle>
</div>
<div :class="'r-timing-'+value.course.timing">
{{ text['coursetiming_'+value.course.timing] }}<br>
{{ startdate }} - {{ enddate }}
</div>
</div>
</template>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="above"
></s-course-extrafields>
<r-item-teachergrades
v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value.course'
:useRequiredGrades="useRequiredGrades"
></r-item-teachergrades>
<r-item-teachercompletion
v-if='!!value.course.completion'
v-model='value.course.completion'
:course='value.course'
></r-item-teachercompletion>
<r-item-teacher-course-competency
v-if='!!value.course.competency'
v-model='value.course.competency'
:item='value'
></r-item-teacher-course-competency>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="below"
></s-course-extrafields>
</b-modal>
</b-card>
</div>
`,
});
// Select activities to use in grade overview
Vue.component('r-item-teacher-gradepicker', {
props: {
value: {
type: Object, // Item
default() {
return {};
},
},
useRequiredGrades: {
type: Boolean,
default() {
return null;
}
}
},
data() {
return {
text: strings.teachercourse,
};
},
computed: {
startdate() {
return formatDate(this.value.course.startdate);
},
enddate() {
if (this.value.course.enddate > 0) {
return formatDate(this.value.course.enddate);
} else {
return this.text.noenddate;
}
},
wwwroot() {
return Config.wwwroot;
}
},
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].catch(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].catch(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="wwwroot+'/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}}
</div>
<div class="r-course-detail-header-right">
<div :class="'r-timing-'+value.course.timing">
{{text['coursetiming_'+value.course.timing]}}<br>
{{ startdate }} - {{ enddate }}
</div>
</div>
</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>
`,
});
// Selected activities dispaly
Vue.component('r-item-teachergrades', {
props: {
value: {
type: Object,
default() {
return {};
},
},
useRequiredGrades: {
type: Boolean,
'default': false,
},
},
data() {
return {
text: strings.teachercourse,
txt: {
grading: strings.grading,
}
};
},
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;
},
filteredGrades() {
return this.value.grades.filter(g => g.selected);
},
},
methods: {
determineGradingIcon(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";
}
},
gradingIcon(grade) {
return this.determineGradingIcon(this.isGradingNeeded(grade));
},
isGradingNeeded(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';
}
} else {
return 'unsubmitted';
}
} else {
return 'unknown';
}
},
},
template: `
<div>
<table class="r-item-course-grade-details">
<tr v-for="g in filteredGrades">
<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 ' + isGradingNeeded(g)"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td v-if='g.grading'
><i :class="'r-course-grading fa fa-'+gradingIcon(g)+' r-graded-'+isGradingNeeded(g)"
:title="txt.grading[isGradingNeeded(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() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
course: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
text: strings.completion,
};
},
computed: {
completionreport() {
return `${Config.wwwroot}/report/completion/index.php?course=${this.course.id}`;
},
wwwroot() {
return Config.wwwroot;
}
},
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: `
<table class="r-item-course-grade-details">
<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="wwwroot+'/course/completion.php?id='+course.id" target='_blank'>{{text.configure_completion}}</a>
</span>
</td>
</tr>
<template v-for='cgroup in value.conditions'>
<tr>
<th colspan='2'><span v-if="cgroup.items.length > 1"
><span v-if="cgroup.aggregation == 'all'">{{ text.aggregation_all}}</span
><span v-else>{{ text.aggregation_any}}</span></span
><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>
</table>
`,
});
// TAG: Teacher Course competency
Vue.component('r-item-teacher-course-competency', {
props: {
value: {
type: Object,
default() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
item: {
type: Object,
default() {
return {id: null};
},
}
},
data() {
return {
text: strings.competency,
};
},
computed: {
hasCompletions() {
if (this.value.conditions) {
for (const cgroup of this.value.conditions) {
if (cgroup.items && cgroup.items.length > 0) {
return true;
}
}
}
return false;
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
completionIcon(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";
}
},
completionTag(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 = Config.wwwroot + `/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 Config.wwwroot + `/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="wwwroot+'/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'>
<td>
<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 ' + + completionTag(c)"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td><r-completion-bar v-model="c.completionstats" :width="150" :height="15"></r-completion-bar></td>
<td v-if="c.feedback">
<a v-b-modal="'r-competency-feedback-'+c.id"
href="#"
@click.prevent.stop=""
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-competency-feedback-'+c.id"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h3><i class="fa fa-puzzle-piece"></i>{{ c.title }}</h3><br>
</div>
</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="wwwroot+'/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">
<td>
<a :href='competencyurl(cc)' target="_blank"><span v-html='cc.title'></span></a>
</td>
<td class='details'>
<a v-if="cc.details" :href='competencyurl(cc)' target="_blank"><span v-html='cc.details'></span></a>
<abbr v-if="c.required" :title="text.required"
:class="'s-required ' + completionTag(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>
<td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td>
<td v-if="cc.feedback">
<a v-b-modal="'r-competency-feedback-'+cc.id"
href="#"
@click.prevent.stop=""
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-competency-feedback-'+cc.id"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h3><i class="fa fa-puzzle-piece"></i>{{ cc.title }}</h3><br>
</div>
</template>
<span v-html="cc.feedback"></span>
</b-modal>
</td>
</tr>
</table>
</template>
</b-modal>
</tr>
</template>
</table>
`,
});
Vue.component('r-grading-bar', {
props: {
value: {
type: Object,
default() {
return {};
},
},
width: {
type: Number,
'default': 150,
},
height: {
type: Number,
'default': 15,
}
},
data() {
return {
text: strings.grading,
};
},
computed: {
widthUnsubmitted() {
return this.width * this.fractionUnsubmitted();
},
widthGraded() {
return this.width * this.fractionGraded();
},
widthUngraded() {
return this.width * this.fractionUngraded();
},
countUnsubmitted() {
return (this.value.students - this.value.graded - this.value.ungraded);
}
},
methods: {
fractionUnsubmitted() {
if (this.value.students > 0) {
return 1 - ((this.value.graded + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fractionGraded() {
if (this.value.students > 0) {
return this.value.graded / this.value.students;
} else {
return 0;
}
},
fractionUngraded() {
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: widthUngraded+'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: widthGraded+'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: widthUnsubmitted+'px'}"
class='r-grading-bar-segment r-grading-bar-unsubmitted'
:title="text.unsubmitted + ' (' + countUnsubmitted + ')'" v-b-popover.hover.top
></span
></span>
`,
});
Vue.component('r-completion-bar', {
props: {
value: {
type: Object,
default() {
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: {
widthIncomplete() {
return this.width * this.fractionIncomplete();
},
widthCompleted() {
return this.width * this.fractionCompleted();
},
widthCompletedPass() {
return this.width * this.fractionCompletedPass();
},
widthCompletedFail() {
return this.width * this.fractionCompletedFail();
},
widthUngraded() {
return this.width * this.fractionUngraded();
},
countIncomplete() {
return (this.value.students - this.value.completed - this.value.completed_pass
- this.value.completed_fail - this.value.ungraded);
}
},
methods: {
fractionIncomplete() {
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;
}
},
fractionCompleted() {
if (this.value.students > 0) {
return this.value.completed / this.value.students;
} else {
return 0;
}
},
fractionCompletedPass() {
if (this.value.students > 0) {
return this.value.completed_pass / this.value.students;
} else {
return 0;
}
},
fractionCompletedFail() {
if (this.value.students > 0) {
return this.value.completed_fail / this.value.students;
} else {
return 0;
}
},
fractionUngraded() {
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: widthUngraded+'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: widthCompleted+'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: widthCompletedPass+'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: widthCompletedFail+'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: widthIncomplete+'px'}"
class='r-grading-bar-segment r-completion-bar-incomplete'
:title="text.incomplete + ' (' + countIncomplete + ')'" v-b-popover.hover.top
></span
></span>
`,
});
Vue.component('r-completion-circle', {
props: {
value: {
type: Object,
default() {
return {
students: 10,
completed: 2,
completedPass: 2,
completedFail: 2,
ungraded: 2,
};
},
},
stroke: {
type: Number,
'default': 0.2,
},
disabled: {
type: Boolean,
'default': false,
},
title: {
type: String,
'default': "",
}
},
computed: {
completedPass() {
if (this.value.completed_pass) {
return this.value.completed_pass;
} else {
return this.value.completedPass;
}
},
completedFail() {
if (this.value.completed_fail) {
return this.value.completed_fail;
} else {
return this.value.completedFail;
}
},
radius() {
return 50 - (50 * this.stroke);
},
arcpathUngraded() {
const begin = 0;
return this.arcpath(begin, this.fractionUngraded());
},
arcpathCompleted() {
const begin = this.fractionUngraded();
return this.arcpath(begin, this.fractionCompleted());
},
arcpathCompletedPass() {
const begin = this.fractionUngraded()
+ this.fractionCompleted();
return this.arcpath(begin, this.fractionCompletedPass());
},
arcpathCompletedFail() {
const begin = this.fractionUngraded()
+ this.fractionCompleted()
+ this.fractionCompletedPass();
return this.arcpath(begin, this.fractionCompletedFail());
},
arcpathIncomplete() {
const begin = this.fractionUngraded()
+ this.fractionCompleted()
+ this.fractionCompletedPass()
+ this.fractionCompletedFail();
return this.arcpath(begin, this.fractionIncomplete());
},
},
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 * π);
},
fractionIncomplete() {
if (this.value.students > 0) {
return 1 - (
(this.value.completed + this.completedPass +
this.completedFail + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fractionCompleted() {
if (this.value.students > 0) {
return this.value.completed / this.value.students;
} else {
return 0;
}
},
fractionCompletedPass() {
if (this.value.students > 0) {
return this.completedPass / this.value.students;
} else {
return 0;
}
},
fractionCompletedFail() {
if (this.value.students > 0) {
return this.completedFail / this.value.students;
} else {
return 0;
}
},
fractionUngraded() {
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>
<circle cx="50" cy="50" :r="radius"
:style="'stroke-width: ' + (stroke*100)+'; stroke: #ccc; fill: none;'"/>
<path :d="arcpathUngraded"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--warning); fill: none;'"/>
<path :d="arcpathCompleted"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--info); fill: none;'"/>
<path :d="arcpathCompletedPass"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--success); fill: none;'"/>
<path :d="arcpathCompletedFail"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--danger); fill: none;'"/>
<circle v-if="disabled" cx="50" cy="50" :r="radius/2"
:style="'fill: var(--dark);'"/>
<circle v-else-if="value.ungraded > 0" cx="50" cy="50" :r="radius/2"
:style="'fill: var(--warning);'"/>
</g>
</svg>
`,
});
Vue.component('r-item-junction', {
props: {
value: {
type: Object,
default() {
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: `
<div :class="'r-item-junction r-item-filter completion-'+completion">
<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() {
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: `
<div :class="'r-item-finish r-item-filter completion-'+completion">
<i class="fa fa-stop-circle"></i>
</div>
`,
});
Vue.component('r-item-start', {
props: {
value: {
type: Object,
default() {
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: `
<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() {
return {};
},
},
guestmode: {
type: Boolean,
'default': false,
},
teachermode: {
type: Boolean,
'default': false,
}
},
data() {
return {
text: strings.badge,
};
},
computed: {
completion() {
return this.value.badge.issued ? "completed" : "incomplete";
},
issuedIcon() {
switch (this.value.badge.issued) {
default: // "nogrades":
return "circle-o";
case true:
return "check";
}
},
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,
};
},
arcpathIssued() {
if (this.value.badge.studentcount) {
const fraction = this.value.badge.issuedcount / this.value.badge.studentcount;
return this.arcpath(0, fraction);
} else {
return ""; // No path
}
},
arcpathProgress() {
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
)
);
}
},
methods: {
arcpath(start, end) {
const r = 44;
const t1 = start * 2 * π;
const Δ = (end * 2 * π - 0.01);
return svgarcpath([50, 50], [r, r], [t1, Δ], 1.5 * π);
},
addTargetBlank(html) {
const m = /^([^<]*< *a +)(.*)/.exec(html);
if (m) {
return `${m[1]} target="_blank" ${m[2]}`;
} else {
return html;
}
},
completionIconRq(complete) {
if (complete) {
return "check-square-o";
} else {
return "square-o";
}
},
completionIcon(complete) {
if (complete) {
return "check-circle";
} else {
return "times-circle";
}
},
status(complete) {
if (complete) {
return "complete";
} else {
return "incomplete";
}
}
},
template: `
<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"
><svg class="r-badge-backdrop " width='50px' height='50px' viewBox="0 0 100 100">
<title>{{value.badge.name}}</title>
<template v-if="teachermode">
<circle cx="50" cy="50" r="44"
style="stroke: #ccc; stroke-width: 8; fill: #ddd; fill-opacity: 0.8;"/>
<path :d="arcpathIssued"
:style="'stroke-width: 8; stroke: var(--info); fill: none;'"/>
</template>
<circle v-else-if="value.badge.issued" cx="50" cy="50" r="46"
style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/>
<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="arcpathProgress"
:style="'stroke-width: 8; stroke: var(--info); fill: none;'"/>
</template>
<circle v-else cx="50" cy="50" r="46"
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"
:style="(badgeinprogress)?'':'opacity: 0.4;'" />
</svg></a>
<b-modal
:id="'r-item-badge-details-'+value.id"
:title="value.badge.name"
size="lg"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h1><i class="fa fa-certificate"></i>
<a :href="(!guestmode)?(value.badge.infolink):undefined" target="_blank"
>{{ value.badge.name }}</a
></h1>
</div>
<div class="r-course-detail-header-right" v-if="!teachermode">
<div class="r-completion-detail-header">
{{ text['completion_'+completion] }}
<i v-b-popover.hover :class="'fa fa-'+issuedIcon+' r-completion-'+completion"
:title="text['completion_'+completion]"></i>
</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">
<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>
</ul>
<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-'+completionIcon(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-'+completionIconRq(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>
<tr v-if="!value.badge.active" class="mt-1">
<td colspan="2" class="alert alert-warning alert-block">{{text.badgedisabled}}</td>
</tr>
</table>
<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>
<a :href="value.badge.infolink" target="_blank"
>{{ text.badgeinfo }}</a></strong></p>
<p v-if="teachermode && !guestmode"
>{{text.badgeissuedstats}}:<br>
<r-completion-bar v-model="issuestats" :width="150" :height="15"></r-completion-bar>
</p>
</b-col></b-row>
</b-container>
</b-modal>
</div>
`,
});
Vue.component('r-item-dummy-course', {
props: {
value: {
type: Object,
default() {
return null;
},
},
},
data() {
return {
text: strings.invalid,
};
},
methods: {
},
template: `
<div class="r-item-dummy-course">
<b-card no-body class="r-item-course">
<b-row no-gutters>
<b-col md="1">
<span class="r-timing-indicator timing-dummy"></span>
</b-col>
<b-col md="11">
<b-card-body class="align-items-center">
</b-card-body>
</b-col>
</b-row>
</b-card>
</div>
`,
});
Vue.component('r-item-dummy-filter', {
props: {
},
data() {
return {};
},
computed: {
},
methods: {
},
template: `
<div :class="'r-item-dummy-filter'">
<i class="fa fa-circle"></i>
</div>
`,
});
Vue.component('r-item-dummy-badge', {
props: {
},
data() {
return {};
},
computed: {
},
methods: {
},
template: `
<div :class="'r-item-dummy-badge'" >
<i class="fa fa-circle"></i>
</div>
`,
});
},
};