This commit is contained in:
cgfhtw
2023-01-16 10:32:24 +01:00
parent ebc7b5a3d9
commit 2f2f19306b
19 changed files with 1447 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
import MylvStudent from "../../../components/Cis/Mylv/Student.js";
Vue.createApp({
components: {
MylvStudent
}
}).mount('#content');
+44
View File
@@ -0,0 +1,44 @@
import MylvSemesterStudiengang from "./Semester/Studiengang.js";
export default {
components: {
MylvSemesterStudiengang
},
provide() {
return {
studien_semester: this.semester
}
},
props: {
semester: String,
lvs: Array
},
computed: {
ready() { return this.lvs !== null; },
studiengaenge() {
return [... new Map(
this.lvs
.map(lv => [
lv.studiengang_kz + '#' + lv.semester,
{
studiengang_kz: lv.studiengang_kz,
bezeichnung: lv.sg_bezeichnung,
kuerzel: lv.studiengang_kuerzel,
semester: lv.semester
}
])
).values()].sort((a, b) => a.bezeichnung.toLowerCase() == b.bezeichnung.toLowerCase() ? a.semester > b.semester : a.bezeichnung.toLowerCase() > b.bezeichnung.toLowerCase());
},
},
methods: {
lvsForStudiengang(studiengang) {
return this.lvs.filter(lv => lv.studiengang_kz == studiengang.studiengang_kz && lv.semester == studiengang.semester);
}
},
template: `<div class="mylv-semester" v-if="ready">
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)"/>
</div>
<div class="mylv-semester text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>`
};
@@ -0,0 +1,45 @@
import MylvSemesterStudiengangLv from "./Studiengang/Lv.js";
import Phrasen from "../../../../mixins/Phrasen.js";
export default {
components: {
MylvSemesterStudiengangLv
},
mixins: [
Phrasen
],
props: {
bezeichnung: String,
kuerzel: String,
semester: Number,
lvs: Array
},
computed: {
lehrveranstaltungen() {
return [... new Map(
this.lvs
.map(lv => [
lv.lehrveranstaltung_id,
lv
])
).values()]
}
},
methods: {
note(lv) {
return lv.benotung ? lv.znote || lv.lvnote || null : null;
}
},
template: `<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{bezeichnung}} - {{kuerzel}}
<small>{{semester}}.{{p.t('lehre/semester')}}</small>
</h4>
<div class="row">
<div v-for="lv in lehrveranstaltungen" :key="lv.lehrveranstaltung_id" class="col-sm-4 mb-3">
<mylv-semester-studiengang-lv v-bind="lv" class="text-center h-100"></mylv-semester-studiengang-lv>
</div>
</div>
</div>
</div>`
};
@@ -0,0 +1,89 @@
import LvPruefungen from "./Lv/Pruefungen.js";
import LvInfo from "./Lv/Info.js";
// TODO(chris): L10n
export default {
inject: ['studien_semester'],
props: {
lehrveranstaltung_id: Number,
bezeichnung: String,
module: String,
farbe: String,
lvinfo: Boolean,
benotung: Boolean,
lvnote: String,
znote: String,
studiengang_kuerzel: String,
semester: String,
orgform_kurzbz: String,
sprache: String,
ects: Number,
incoming: Number
},
data: () => {
return {
pruefungen: null,
info: null
}
},
computed: {
bodyStyle() {
const bodyStyle = {};
if (this.farbe)
bodyStyle['background-color'] = '#' + this.farbe;
return bodyStyle;
},
grade() {
return this.benotung ? this.znote || this.lvnote || null : null;
}
},
methods: {
openPruefungen() {
if (!this.pruefungen) {
this.pruefungen = true;
LvPruefungen.popup({
lehrveranstaltung_id: this.lehrveranstaltung_id,
bezeichnung: this.bezeichnung
}).then(() => this.pruefungen = false).catch(() => this.pruefungen = false);
}
},
openInfos() {
if (!this.info) {
this.info = true;
// TODO(chris): load all this params on ajax?
LvInfo.popup({
lehrveranstaltung_id: this.lehrveranstaltung_id,
bezeichnung: this.bezeichnung,
studiengang_kuerzel: this.studiengang_kuerzel,
semester: this.semester,
studien_semester: this.studien_semester,
orgform_kurzbz: this.orgform_kurzbz,
sprache: this.sprache,
ects: this.ects,
incoming: this.incoming
}).then(() => this.info = false).catch(() => this.info = false);
}
}
},
template: `<div class="mylv-semester-studiengang-lv card">
<div v-if="module" class="card-header">
{{module}}
</div>
<div class="card-body" :style="bodyStyle">
<h5 class="card-title">{{bezeichnung}}</h5>
</div>
<div class="card-footer">
<div class="row">
<a href="#" class="col-auto text-start text-decoration-none" @click.prevent="openPruefungen">
{{ grade || '-' }}
</a>
<div v-if="lvinfo" class="col text-end">
<a class="card-link" href="#" @click.prevent="openInfos">
<i class="fa fa-info-circle" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
</div>`
};
@@ -0,0 +1,187 @@
import BsModal from '../../../../../Bootstrap/Modal.js';
import Phrasen from '../../../../../../mixins/Phrasen.js';
const infos = {};
export default {
components: {
BsModal
},
mixins: [
BsModal,
Phrasen
],
props: {
lehrveranstaltung_id: Number,
bezeichnung: String,
studiengang_kuerzel: String,
semester: Number,
studien_semester: String,
orgform_kurzbz: String,
sprache: String,
ects: Number,
incoming: Number,
/*
* NOTE(chris):
* Hack to expose in "emits" declared events to $props which we use
* in the v-bind directive to forward all events.
* @see: https://github.com/vuejs/core/issues/3432
*/
onHideBsModal: Function,
onHiddenBsModal: Function,
onHidePreventedBsModal: Function,
onShowBsModal: Function,
onShownBsModal: Function
},
data: () => ({
result: true,
info: null
}),
computed: {
lektorNames() {
return this.info.lektoren.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
},
lvLeitung() {
return this.info.lvLeitung && this.info.lvLeitung.length ? this.info.lvLeitung.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim()) : null;
},
oe() {
return this.info.oe.organisationseinheittyp ? (this.info.oe.organisationseinheittyp + ' ' + this.info.oe.bezeichnung) : '';
},
oeLeitung() {
if (!this.info.oeLeitung || !this.info.oeLeitung.length)
return ['-'];
return this.info.oeLeitung.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
},
koordinator() {
if (!this.info.koordinator || !this.info.koordinator.length)
return null;
return this.info.koordinator.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
},
currentLang() {
if (!this.info)
return null;
if (this.info.lastLang)
return this.info.lastLang;
if (!this.info.lvinfo)
return null;
return this.info.lvinfoDefaultLang && this.info.lvinfo[this.info.lvinfoDefaultLang] ? this.info.lvinfoDefaultLang : Object.keys(this.info.lvinfo).shift();
}
},
created() {
if (infos[this.lehrveranstaltung_id]) {
this.info = infos[this.lehrveranstaltung_id];
} else {
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Info/' + this.studien_semester + '/' + this.lehrveranstaltung_id).then(res => {
this.info = infos[this.lehrveranstaltung_id] = res.data.retval || [];
}).catch(() => this.info = {});
}
},
mounted() {
this.modal = this.$refs.modalContainer.modal;
},
popup(options) {
return BsModal.popup.bind(this)(null, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-alert" v-bind="$props" body-class="" dialog-class="modal-fullscreen">
<template v-slot:title>
{{p.t('lvinfo/lehrveranstaltungsinformationen')}}
</template>
<template v-slot:default>
<div v-if="!info" class="text-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<table v-else class="table table-hover">
<tbody>
<tr>
<th>{{p.t('lehre/lehrveranstaltung')}}</th>
<td>{{bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre/studiengang')}}</th>
<td>{{studiengang_kuerzel}}</td>
</tr>
<tr>
<th>{{p.t('lehre/semester')}}</th>
<td>{{semester}}</td>
</tr>
<tr>
<th>{{p.t('lehre/studiensemester')}}</th>
<td>{{studien_semester}}</td>
</tr>
<tr>
<th>{{p.t('lehre/organisationsform')}}</th>
<td>{{orgform_kurzbz}}</td>
</tr>
<tr>
<th>{{p.t('lehre/lehrbeauftragter')}}</th>
<td>
<ul v-if="lektorNames.length" class="list-unstyled mb-0">
<li v-for="name in lektorNames" :key="name">
<!-- TODO(chris): link? -->
{{name}}
</li>
</ul>
<template v-else>
{{p.t('lehre/keinLektorZugeordnet')}}
</template>
</td>
</tr>
<tr v-if="lvLeitung">
<th>{{p.t('lehre/lvleitung')}}</th>
<td>
<ul class="list-unstyled mb-0">
<li v-for="name in lvLeitung" :key="name">
<!-- TODO(chris): link? -->
{{name}}
</li>
</ul>
</td>
</tr>
<tr>
<th>{{p.t('global/sprache')}}</th>
<td>{{sprache}}</td>
</tr>
<tr>
<th>{{p.t('lehre/ects')}}</th>
<td>{{ects}}</td>
</tr>
<tr>
<th>{{p.t('lehre/incomingplaetze')}}</th>
<td>{{incoming}}</td>
</tr>
<tr>
<th>{{p.t('lehre/organisationseinheit')}}</th>
<td>
{{oe}} <br>
(
<i>{{p.t('global/leitung')}}: </i>{{oeLeitung.join(', ')}}
<template v-if="koordinator">
<i>{{p.t('global/koordination')}}: </i>{{koordinator.join(', ')}}
</template>
)
</td>
</tr>
</tbody>
</table>
<div v-if="info && info.lvinfo">
<div v-if="Object.keys(info.lvinfo).length > 1" class="text-end">
<div class="btn-group" role="group" :title="p.t('global/verfuegbareSprachen')" :aria-label="p.t('global/verfuegbareSprachen')">
<template v-for="lang in info.sprachen" :key="lang.index">
<button v-if="info.lvinfo[lang.sprache]" type="button" class="btn btn-outline-primary" :class="lang.sprache == currentLang ? 'active' : ''" @click.prevent="info.lastLang = lang.sprache"><img class="d-block" :src="'data:image/gif;base64, ' + lang.flagge" :alt="lang.bezeichnung[lang.index]"></button>
</template>
</div>
</div>
<template v-for="i in info.lvinfo[currentLang]" :key="info">
<h4>{{i.header}}</h4>
<h6 v-if="i.subheader">{{i.subheader}}</h6>
<ul v-if="Array.isArray(i.body)">
<li v-for="e in i.body" :key="e">{{e}}</li>
</ul>
<p v-else>
{{i.body}}
</p>
</template>
</div>
</template>
</bs-modal>`
}
@@ -0,0 +1,72 @@
import BsModal from '../../../../../Bootstrap/Modal.js';
const pruefungen = {};
export default {
components: {
BsModal
},
mixins: [
BsModal
],
props: {
lehrveranstaltung_id: Number,
bezeichnung: String,
/*
* NOTE(chris):
* Hack to expose in "emits" declared events to $props which we use
* in the v-bind directive to forward all events.
* @see: https://github.com/vuejs/core/issues/3432
*/
onHideBsModal: Function,
onHiddenBsModal: Function,
onHidePreventedBsModal: Function,
onShowBsModal: Function,
onShownBsModal: Function
},
data: () => ({
result: true,
pruefungen: null
}),
created() {
if (pruefungen[this.lehrveranstaltung_id])
this.pruefungen = pruefungen[this.lehrveranstaltung_id];
else
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Pruefungen/' + this.lehrveranstaltung_id).then(res => {
this.pruefungen = pruefungen[this.lehrveranstaltung_id] = res.data.retval || [];
});
},
mounted() {
this.modal = this.$refs.modalContainer.modal;
},
popup(options) {
return BsModal.popup.bind(this)(null, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-alert" v-bind="$props" body-class="">
<template v-slot:title>
Prüfungen: {{bezeichnung}}
</template>
<template v-slot:default>
<div v-if="!pruefungen" class="text-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<p v-else-if="!pruefungen.length" class="alert alert-info mb-0">
Keine Prüfungen vorhanden!
</p>
<table v-else class="table table-hover">
<thead>
<td>&nbsp;</td>
<td>Datum</td>
<td class="text-end">Note</td>
</thead>
<tbody>
<tr v-for="pruefung in pruefungen" :key="pruefung.pruefung_id">
<th>{{pruefung.pruefungstyp_kurzbz}}</th>
<td>{{pruefung.datum}}</td>
<td class="text-end">{{pruefung.note}}</td>
</tr>
</tbody>
</table>
</template>
</bs-modal>`
}
+116
View File
@@ -0,0 +1,116 @@
import MylvSemester from "./Semester.js";
import Phrasen from "../../../mixins/Phrasen.js";
// TODO(chris): phrase: global/studiensemester_auswaehlen
// TODO(chris): phrase: next & prev +aria-label
export default {
components: {
MylvSemester
},
mixins: [
Phrasen
],
data: () => {
return {
firstLoad: true,
studiensemester: null,
lvs: {},
currentSemester: null
};
},
computed: {
ready() {
return this.studiensemester !== null && (!this.firstLoad || this.current.lvs !== null);
},
current() {
if (this.currentSemester === null)
return { semester: null, lvs: null };
if (this.lvs[this.currentSemester] === undefined) {
this.lvs[this.currentSemester] = {
semester: this.currentSemester,
lvs: null
};
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Lvs/' + this.currentSemester).then(res => {
this.lvs[this.currentSemester].lvs = res.data.retval;
this.firstLoad = false;
});
}
return this.lvs[this.currentSemester];
},
nearestSem() {
let now = Date.now();
let nearestSem = null;
let nearestSemDiff = 0;
this.studiensemester.forEach(sem => {
let start = new Date(sem.start);
let end = new Date(sem.ende);
if (now >= start && now <= end) {
nearestSem = sem.studiensemester_kurzbz;
nearestSemDiff = 0;
return;
}
let diff = Math.min(Math.abs(now - start), Math.abs(now - end));
if (nearestSem === null || diff < nearestSemDiff) {
nearestSem = sem.studiensemester_kurzbz;
nearestSemDiff = diff;
}
});
return nearestSem;
},
currentIsFirst() {
return this.studiensemester[0].studiensemester_kurzbz == this.currentSemester;
},
currentIsLast() {
return this.studiensemester[this.studiensemester.length-1].studiensemester_kurzbz == this.currentSemester;
}
},
methods: {
prevSem() {
this.$refs.studiensemester.selectedIndex--;
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
},
nextSem() {
this.$refs.studiensemester.selectedIndex++;
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
},
setHash(val) {
location.hash = val;
}
},
created() {
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Studiensemester').then(res => {
this.studiensemester = res.data.retval;
const hash = location.hash.substring(1);
if (hash && this.studiensemester.filter(s => s.studiensemester_kurzbz == hash).length)
this.currentSemester = hash;
else
this.currentSemester = this.nearestSem;
});
},
template: `<div class="mylv-student" v-if="ready">
<div class="row justify-content-center mb-3">
<div class="col-auto d-none">
<label class="col-form-label">{{p.t('lehre/studiensemester')}}</label>
</div>
<div class="col-auto">
<div class="input-group">
<button class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
<i class="fa fa-caret-left" aria-hidden="true"></i>
</button>
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
</select>
<button class="btn btn-outline-secondary" type="button" :disabled="currentIsLast" @click="nextSem">
<i class="fa fa-caret-right" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<mylv-semester v-bind="current"/>
</div>
<div class="mylv-student text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>`
};
+69
View File
@@ -0,0 +1,69 @@
const categories = {};
const loadingModules = {};
function extractCategory(obj, category) {
return obj.filter(e => e.category == category).reduce((res, elem) => {
if (!res[elem.phrase])
res[elem.phrase] = elem.text;
return res;
}, {});
}
function loadLazy(category, val) {
// NOTE(chris): load module if it's not loaded yet
if (loadingModules[category]) {
loadingModules[category].push(val);
if (categories[category]) // NOTE(chris): this is for safety in case the loading finished the moment before the val was pushed into the array
while (loadingModules[category].length)
Vue.triggerRef(loadingModules[category].pop());
return Vue.unref(val);
}
loadingModules[category] = [val];
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Phrasen/LoadModule/' + category).then(res => {
if (res.data.retval)
categories[category] = extractCategory(res.data.retval, category);
else
categories[category] = {};
while (loadingModules[category].length)
Vue.triggerRef(loadingModules[category].pop());
}).catch(err => console.error(err));
}
export default {
data: () => {
return {
p: {
t(category, phrase, params) {
if (params === undefined && (
(Array.isArray(category) && category.length == 2) ||
(category.split && category.split('/').length == 2))
) {
params = phrase;
[category, phrase] = category.split ? category.split('/') : category;
}
if (phrase === undefined) {
console.error('invalid input');
return '';
}
if (!categories[category]) {
if (window.FHC_JS_PHRASES_STORAGE_OBJECT !== undefined)
categories[category] = extractCategory(FHC_JS_PHRASES_STORAGE_OBJECT, category);
if (!categories[category] || Object.keys(categories[category]).length === 0) {
let val = Vue.ref('');
loadLazy(category, val);
return Vue.unref(val);
}
}
let result = categories[category][phrase];
if (!result)
return '<< PHRASE ' + phrase + '>>';
if (params)
return result.replace(/\{([^}]*)\}/g, (match, p1) => params[p1] || match);
return result;
}
}
}
}
}