diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index ad7337257637e795b101c867d5674f2a13448711..09a3a7e5e2b57dee144b82f464354adce598c8fe 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -27,7 +27,7 @@ $element-icons: ( $tree-item-flag-icons: ( date: date, write: edit, - cant-read: lock-locked2 + cant-read: lock-locked2, ); $tile-colors: ( @@ -383,6 +383,10 @@ $consum_ribbon_width: calc(100% - 58px); } } + img { + vertical-align: text-top; + } + &.cw-ribbon-breadcrumb-item-current { flex-shrink: 1; } @@ -1388,7 +1392,7 @@ label[for="cw-keypoint-color"] { display: inline-block; border-bottom: none; font-size: 14px; - width: calc(100% - 14px); + width: calc(100% - 20px); background-repeat: no-repeat; padding-left: 18px; margin-left: 4px; @@ -1456,6 +1460,22 @@ label[for="cw-keypoint-color"] { } } } + .cw-tree-item-sequential { + display: inline-block; + position: absolute; + right: 8px; + + &.cw-tree-item-sequential-complete { + width: 16px; + height: 16px; + vertical-align: top; + @include background-icon(accept, info, 16); + } + &.cw-tree-item-sequential-percentage { + color: $black; + font-size: 14px; + } + } .cw-tree-item-ghost { opacity: 0.6; diff --git a/resources/vue/components/courseware/CoursewareProgressCircle.vue b/resources/vue/components/courseware/CoursewareProgressCircle.vue index 729d707705755ed983b5f47206fc4cdb548e5141..e14559026834136506a4bf77cdc8727bdee2a855 100644 --- a/resources/vue/components/courseware/CoursewareProgressCircle.vue +++ b/resources/vue/components/courseware/CoursewareProgressCircle.vue @@ -1,6 +1,6 @@ <template> <div class="cw-progress-circle" :class="['p' + value, value > 50 ? 'over50' : '']"> - <span>{{ value }}%</span> + <span>{{ value }} %</span> <div class="left-half-clipper"> <div class="first50-bar"></div> <div class="value-bar"></div> diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue index ccc4cb11a9b5992d8507010af28485666b9e149e..e472ee0aa057b51a52356f5d60c467379f143cef 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue @@ -35,6 +35,23 @@ > <span>{{ structuralElement.attributes.title || "–" }}</span> <span v-if="isTask">[ {{ solverName }} ]</span> + <template v-if="!userIsTeacher && inCourse"> + <studip-icon + v-if="complete" + shape="accept" + role="info" + :title="$gettext('Diese Seite wurde von Ihnen vollständig bearbeitet')" + /> + <span + v-else + :title="$gettextInterpolate( + $gettext('Fortschritt: %{progress} %'), + {progress: elementProgress} + )" + > + ({{ elementProgress }} %) + </span> + </template> </li> </template> <template #breadcrumbFallback> @@ -831,6 +848,7 @@ export default { isLink: 'currentElementisLink', templates: 'courseware-templates/all', + progressData: 'progresses', }), currentId() { @@ -1218,6 +1236,19 @@ export default { return template.attributes.purpose === this.newChapterPurpose }); }, + complete() { + return this.elementProgress === 100; + }, + elementProgress() { + if (this.structuralElementLoaded) { + return this.progressData?.[this.structuralElement.id].progress.self; + } + + return 0; + }, + progressTitle() { + return ''; + } }, methods: { @@ -1250,6 +1281,7 @@ export default { loadStructuralElement: 'loadStructuralElement', createLink: 'createLink', setCurrentElementId: 'coursewareCurrentElement', + loadProgresses: 'loadProgresses' }), initCurrent() { @@ -1735,6 +1767,10 @@ export default { if (this.isLink) { this.loadStructuralElement(this.structuralElement.attributes['target-id']); } + + if (this.inCourse && this.courseware.attributes['sequential-progression'] && !this.userIsTeacher) { + this.loadProgresses(); + } }, containers() { this.containerList = this.containers; diff --git a/resources/vue/components/courseware/CoursewareTreeItem.vue b/resources/vue/components/courseware/CoursewareTreeItem.vue index 0a5ead52b4f238dd9f57562a7ec25d42de738bf3..d2d69af90b0acff2bbbe7e00ca8742915d93ee97 100644 --- a/resources/vue/components/courseware/CoursewareTreeItem.vue +++ b/resources/vue/components/courseware/CoursewareTreeItem.vue @@ -39,6 +39,24 @@ class="cw-tree-item-flag-cant-read" :title="$gettext('Diese Seite kann von Teilnehmenden nicht gesehen werden')" ></span> + <template v-if="!userIsTeacher && inCourse"> + <span + v-if="complete" + class="cw-tree-item-sequential cw-tree-item-sequential-complete" + :title="$gettext('Diese Seite wurde von Ihnen vollständig bearbeitet')" + > + </span> + <span + v-else + class="cw-tree-item-sequential cw-tree-item-sequential-percentage" + :title="$gettextInterpolate( + $gettext('Fortschritt: %{progress}%'), + {progress: itemProgress} + )" + > + {{ itemProgress }} % + </span> + </template> </router-link> </div> <ol @@ -141,6 +159,9 @@ export default { userById: 'users/byId', groupById: 'status-groups/byId', viewMode: 'viewMode', + courseware: 'courseware', + progressData: 'progresses', + userIsTeacher: 'userIsTeacher', }), draggableData() { return { @@ -269,7 +290,19 @@ export default { }, canEdit() { return this.element.attributes['can-edit']; - } + }, + inCourse() { + return this.context.type === 'courses'; + }, + progress() { + return this.progressData?.[this.element.id]; + }, + itemProgress() { + return this.progress?.progress?.self ?? 0; + }, + complete() { + return this.itemProgress === 100; + }, }, methods: { ...mapActions({ diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js index 337b56f83b11e8738e67a3f610affdb288e7c799..5b66ef7d1954897069fc8623193dc436b5dcf65a 100644 --- a/resources/vue/courseware-index-app.js +++ b/resources/vue/courseware-index-app.js @@ -136,6 +136,7 @@ const mountApp = async (STUDIP, createApp, element) => { if (entry_type === 'courses') { await store.dispatch('loadTeacherStatus', STUDIP.USER_ID); + store.dispatch('loadProgresses'); } store.dispatch('coursewareCurrentElement', elem_id); diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index 5c408b9c3f01dd08a9dd7124473dec72f807e2db..fc688f015ea882ccd75e69497e41adb848ef89a8 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -60,7 +60,8 @@ const getDefaultState = () => { showSearchResults: false, searchResults: [], - assistiveLiveContents: '' + assistiveLiveContents: '', + progresses: null }; }; @@ -254,6 +255,9 @@ const getters = { }, assistiveLiveContents(state) { return state.assistiveLiveContents; + }, + progresses(state) { + return state.progresses; } }; @@ -1339,6 +1343,18 @@ export const actions = { options, }); }, + async loadUnitProgresses({ getters }, { unitId }) { + const response = await state.httpClient.get(`courseware-units/${unitId}/courseware-user-progresses`); + if (response.status === 200) { + return response.data; + } else { + return null; + } + }, + async loadProgresses({ dispatch, commit, getters }) { + const progresses = await dispatch('loadUnitProgresses', { unitId: getters.context.unit }); + commit('setProgresses', progresses); + } }; /* eslint no-param-reassign: ["error", { "props": false }] */ @@ -1526,6 +1542,9 @@ export const mutations = { }, setAssistiveLiveContents(state, text) { state.assistiveLiveContents = text; + }, + setProgresses(state, data) { + state.progresses = data; } };