load projektarbeiten for studiengänge -> assistenz page; speed dial position rework; automagicmodal logic fix; activeTabIndex by date in detail views; tooltips on icons; $capitalize phrasen to ensure capitalization; phrasenpromise & resolve similar to anw; modal component emits fullscreen event now;

This commit is contained in:
Johann Hoffmann
2025-10-27 14:40:56 +01:00
parent 0d2e41cf2f
commit 259c2aec14
19 changed files with 1345 additions and 314 deletions
+7
View File
@@ -100,4 +100,11 @@ export default {
url: '/api/frontend/v1/Abgabe/getNoten'
};
},
getProjektarbeitenForStudiengang(studiengang_kz) {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getProjektarbeitenForStudiengang',
params: { studiengang_kz }
};
}
};
+2
View File
@@ -2,6 +2,7 @@ import PluginsPhrasen from '../../plugins/Phrasen.js';
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import {capitalize} from "../../helpers/StringHelpers.js";
const app = Vue.createApp({
name: 'AbgabetoolApp',
@@ -51,6 +52,7 @@ const app = Vue.createApp({
</template>
`
});
app.config.globalProperties.$capitalize = capitalize;
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
+11 -1
View File
@@ -14,11 +14,13 @@ import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.js";
import ApiRenderers from '../../api/factory/renderers.js';
import ApiRouteInfo from '../../api/factory/routeinfo.js';
import {capitalize} from "../../helpers/StringHelpers.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
@@ -55,6 +57,12 @@ const router = VueRouter.createRouter({
component: AbgabetoolMitarbeiter,
props: true
},
{
path: `/Cis/Abgabetool/Assistenz`,
name: 'AbgabetoolAssistenz',
component: AbgabetoolAssistenz,
props: true
},
{
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview',
@@ -275,6 +283,7 @@ const app = Vue.createApp({
}
},
async created(){
await this.$api
.call(ApiRenderers.loadRenderers())
.then(res => res.data)
@@ -315,6 +324,7 @@ const app = Vue.createApp({
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
@@ -323,7 +333,7 @@ const app = Vue.createApp({
// kind of a bandaid for bad css on some pages to avoid horizontal scroll
setScrollbarWidth();
app.config.globalProperties.$capitalize = capitalize;
app.use(router);
app.use(primevue.config.default, {
zIndex: {
+3 -1
View File
@@ -46,7 +46,8 @@ export default {
"hiddenBsModal",
"hidePreventedBsModal",
"showBsModal",
"shownBsModal"
"shownBsModal",
"toggleFullscreen"
],
methods: {
dispose() {
@@ -66,6 +67,7 @@ export default {
},
toggleFullscreen() {
this.fullscreen = !this.fullscreen
this.$emit('toggleFullscreen')
}
},
mounted() {
@@ -27,12 +27,15 @@ export const AbgabeMitarbeiterDetail = {
projektarbeit: {
type: Object,
default: null
},
isFullscreen: {
type: Boolean,
default: false
}
},
data() {
return {
showAutomagicModalPhrase: false,
sdModel: [],
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
@@ -60,8 +63,8 @@ export const AbgabeMitarbeiterDetail = {
}
},
methods: {
openZusatzdatenModal(termin) {
getPlaceholderTermin(termin) {
return termin?.bezeichnung?.bezeichnung ?? this.$p.t('abgabetool/abgabetypPlaceholder')
},
saveTermin(termin) {
const paabgabe_id = termin.paabgabe_id
@@ -97,10 +100,10 @@ export const AbgabeMitarbeiterDetail = {
// really bad feature imo that will be annoying to deal with
// termin is completely new and has negative note
const savedNewWithNegative = !existingTerminRes && !newTerminRes.note?.positiv
const savedNewWithNegative = !existingTerminRes && !newTerminRes.note?.positiv && newTerminRes.note !== null
// termin existed previously + oldTermin had different note/positive note or no note at all
const savedExistingNoteAsNegativeAndWasNotNegativeBefore = existingTerminRes && !newTerminRes.note?.positiv && (existingTerminRes.note?.positiv || existingTerminRes.note === undefined)
const savedExistingNoteAsNegativeAndWasNotNegativeBefore = existingTerminRes && !newTerminRes.note?.positiv && newTerminRes.note !== null && (existingTerminRes.note?.positiv || existingTerminRes.note === undefined)
const openModalDueToNegativeBeurteilung = savedNewWithNegative || savedExistingNoteAsNegativeAndWasNotNegativeBefore
if(openModalDueToNegativeBeurteilung) {
@@ -198,6 +201,9 @@ export const AbgabeMitarbeiterDetail = {
getOptionLabelAbgabetyp(option){
return option.bezeichnung
},
getOptionDisabled(option) {
return !option.aktiv
},
getNotenOptionLabel(option) {
return option.bezeichnung
},
@@ -298,8 +304,23 @@ export const AbgabeMitarbeiterDetail = {
},
computed: {
activeAbgabeTypeOptions() {
return this.abgabeTypeOptions?.filter(opt => opt.aktiv === true)
getActiveIndexTabArray() {
// here we try to do mind reading logic by assuming which abgabetermine are the most relevant to the current user
// lets try to take the termin with nearest date and watch who complains and why
let closestIndex = -1;
let minDiff = Infinity;
const today = new Date();
this.projektarbeit.abgabetermine.forEach((obj, i) => {
const diff = Math.abs(new Date(obj.datum) - today);
if (diff < minDiff) {
minDiff = diff;
closestIndex = i;
}
});
return [closestIndex]
},
getEid() {
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
@@ -323,12 +344,16 @@ export const AbgabeMitarbeiterDetail = {
})
return qgatefound
},
getPageWrapperStyle() {
return 'position: relative; min-height: 100vh;'
},
getSpeedDialStyle() {
return 'position: static !important;'
},
getSpeedDialWrapperStyle() {
// fullscreen -> position wrapper fixed in right bottom corner of viewport/screen
if(this.isFullscreen) return 'position: fixed; z-index: 9999; bottom: 24px; right: 24px;'
// non fullscreen -> wrapper is positioned on right bottom corner of modal, wherever that is
return 'position: absolute; z-index: 9999; bottom: -28px; right: -28px;'
},
getTooltipVerspaetet() {
return {
value: this.$p.t('abgabetool/c4tooltipVerspaetet'),
@@ -358,6 +383,24 @@ export const AbgabeMitarbeiterDetail = {
value: this.$p.t('abgabetool/c4tooltipAbgegeben'),
class: "custom-tooltip"
}
},
getTooltipFixtermin() {
return {
value: this.$p.t('abgabetool/c4tooltipFixtermin'),
class: "custom-tooltip"
}
},
getTooltipNotAllowedToSave() {
return {
value: this.$p.t('abgabetool/c4notAllowedToEditAbgabeTermin'),
class: "custom-tooltip"
}
},
getTooltipNotAllowedToDelete() {
return {
value: this.$p.t('abgabetool/c4notAllowedToDeleteAbgabeTermin'),
class: "custom-tooltip"
}
}
},
watch: {
@@ -378,23 +421,23 @@ export const AbgabeMitarbeiterDetail = {
},
template: `
<bs-modal
id="innerModalNewAbgabe"
ref="modalContainerCreateNewAbgabe"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg"
:backdrop="true"
@hideBsModal="console.log('hideBsModal'); showAutomagicModalPhrase=false;"
>
<template v-slot:title>
<div>
{{ $p.t('abgabetool/c4newAbgabetermin') }}
</div>
</template>
<template v-slot:default>
<div v-if="showAutomagicModalPhrase" class="text-center"><p>{{$p.t('abgabetool/c4abgabeQualGateNegativAddNewAutomagisch')}}</p></div>
<bs-modal
id="innerModalNewAbgabe"
ref="modalContainerCreateNewAbgabe"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg"
:backdrop="true"
@hideBsModal="console.log('hideBsModal'); showAutomagicModalPhrase=false;"
>
<template v-slot:title>
<div>
{{ $p.t('abgabetool/c4newAbgabetermin') }}
</div>
</template>
<template v-slot:default>
<div v-if="showAutomagicModalPhrase" class="text-center"><p>{{$p.t('abgabetool/c4abgabeQualGateNegativAddNewAutomagisch')}}</p></div>
<!-- minheight to avoid z-index magic for the datepicker inside the modal inside the modal...-->
<div v-if="newTermin">
<div v-if="newTermin">
<!-- fixtermin is not an option for lektors-->
<!-- <div class="row">-->
<!-- <div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4fixtermin')}}</div>-->
@@ -407,12 +450,132 @@ export const AbgabeMitarbeiterDetail = {
<!-- </Checkbox>-->
<!-- </div>-->
<!-- </div>-->
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ $capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
<div class="col-8 col-md-9">
<VueDatePicker
v-model="newTermin.datum"
:clearable="false"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ $capitalize( $p.t('abgabetool/c4abgabetypv2') )}}</div>
<div class="col-8 col-md-9">
<Dropdown
:style="{'width': '100%'}"
v-model="newTermin.bezeichnung"
:options="abgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp"
scrollHeight="300px">
</Dropdown>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ $capitalize( $p.t('abgabetool/c4upload_allowed') )}}</div>
<div class="col-8 col-md-9">
<Checkbox
v-model="newTermin.upload_allowed"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ $capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="newTermin.kurzbz" rows="1" :cols=" isMobile ? 30 : 60"></Textarea>
</div>
</div>
</div>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="handleSaveNewAbgabe(newTermin)">{{$capitalize( $p.t('abgabetool/c4saveNewAbgabetermin') )}}</button>
</template>
</bs-modal>
<div v-if="projektarbeit">
<div id="speedDialWrapper" :style="getSpeedDialWrapperStyle">
<SpeedDial
:style="getSpeedDialStyle"
:model="speedDialItems"
direction="up"
:radius="80"
type="linear"
buttonClass="p-button-rounded p-button-lg p-button-primary"
:tooltipOptions="{ position: 'left' }"
/>
</div>
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeMitarbeiterbereich') )}}</h5>
<div class="row">
<div class="col-6">
<p> {{projektarbeit?.student}}</p>
<p> {{projektarbeit?.titel}}</p>
<p v-if="projektarbeit?.zweitbegutachter"> {{projektarbeit?.zweitbegutachter}}</p>
</div>
<div v-if="!isMobile" class="col-3"></div>
<div :class="isMobile ? 'col-6' : 'col-3'">
<SpeedDial
:model="speedDialItems"
direction="left"
:radius="80"
type="linear"
buttonClass="p-button-rounded p-button-lg p-button-primary"
:tooltipOptions="{ position: 'down' }"
/>
</div>
</div>
<Accordion :multiple="true" :activeIndex="getActiveIndexTabArray">
<template v-for="termin in this.projektarbeit?.abgabetermine">
<AccordionTab :headerClass="getDateStyleClass(termin) + '-header'">
<template #header>
<div class="d-flex row w-100">
<div class="col-auto" style="transform: translateX(-62px)">
<i v-if="getDateStyleClass(termin) == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i>
<i v-else-if="getDateStyleClass(termin) == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i>
<i v-else-if="getDateStyleClass(termin) == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i>
<i v-else-if="getDateStyleClass(termin) == 'standard'" v-tooltip.right="getTooltipStandard" class="fa-solid fa-clock"></i>
<i v-else-if="getDateStyleClass(termin) == 'abgegeben'" v-tooltip.right="getTooltipAbgegeben" class="fa-solid fa-check"></i>
</div>
<div class="col-auto text-start" style="min-width: max(150px, 15%); max-width: min(300px, 30%); transform: translateX(-30px)">
<span>{{ termin?.bezeichnung?.bezeichnung }}</span>
</div>
<div class="col-auto text-start" style="min-width: 100px; transform: translateX(-30px)">
<span>{{ formatDate(termin.datum, false) }}</span>
</div>
<div v-if="termin?.fixtermin" class="col-auto" style="transform: translateX(-30px)">
<i v-tooltip.right="getTooltipFixtermin" class="fa-solid fa-lock"></i>
</div>
</div>
</template>
<!-- <div class="row">-->
<!-- <div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4fixterminv2')}}</div>-->
<!-- <div class="col-8 col-md-9">-->
<!-- <Checkbox -->
<!-- v-model="termin.fixtermin"-->
<!-- disabled-->
<!-- :binary="true" -->
<!-- :pt="{ root: { class: 'ml-auto' }}"-->
<!-- >-->
<!-- </Checkbox>-->
<!-- </div>-->
<!-- </div>-->
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4zieldatum')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
<div class="col-8 col-md-9">
<VueDatePicker
v-model="newTermin.datum"
v-model="termin.datum"
:clearable="false"
:disabled="!termin.allowedToSave"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
@@ -421,196 +584,106 @@ export const AbgabeMitarbeiterDetail = {
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabetyp')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4abgabetypv2') )}}</div>
<div class="col-8 col-md-9">
<Dropdown
:style="{'width': '100%'}"
v-model="newTermin.bezeichnung"
:options="activeAbgabeTypeOptions"
:disabled="!termin.allowedToSave"
:placeholder="getPlaceholderTermin(termin)"
v-model="termin.bezeichnung"
@change="handleChangeAbgabetyp(termin)"
:options="abgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp"
scrollHeight="300px">
:optionDisabled="getOptionDisabled">
</Dropdown>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4upload_allowed')}}</div>
<div class="row mt-2" v-if="termin.bezeichnung?.paabgabetyp_kurzbz === 'qualgate1' || termin.bezeichnung?.paabgabetyp_kurzbz === 'qualgate2'">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4upload_allowed') )}}</div>
<div class="col-8 col-md-9">
<Checkbox
v-model="newTermin.upload_allowed"
v-model="termin.upload_allowed"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
<div class="row mt-2" v-if="termin.bezeichnung?.benotbar">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4note') )}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="newTermin.kurzbz" rows="1" :cols=" isMobile ? 30 : 60"></Textarea>
<Dropdown
:style="{'width': '100%'}"
v-model="termin.note"
:options="allowedNotenOptions"
:optionLabel="getNotenOptionLabel">
</Dropdown>
</div>
</div>
</div>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="handleSaveNewAbgabe(newTermin)">{{ $p.t('abgabetool/c4saveNewAbgabetermin') }}</button>
</template>
</bs-modal>
<div v-if="projektarbeit">
<div style="position: fixed; bottom: 24px; right: 24px; z-index: 9999;">
<SpeedDial
:style="getSpeedDialStyle"
:model="speedDialItems"
direction="up"
:radius="80"
type="linear"
buttonClass="p-button-rounded p-button-lg p-button-primary"
:tooltipOptions="{ position: 'left' }"
/>
</div>
<h5>{{$p.t('abgabetool/c4abgabeMitarbeiterbereich')}}</h5>
<div class="row">
<div class="col-6">
<p> {{projektarbeit?.student}}</p>
<p> {{projektarbeit?.titel}}</p>
<p v-if="projektarbeit?.zweitbegutachter"> {{projektarbeit?.zweitbegutachter}}</p>
</div>
</div>
<Accordion :multiple="true" :activeIndex="[0]">
<template v-for="termin in this.projektarbeit?.abgabetermine">
<AccordionTab :headerClass="getDateStyleClass(termin) + '-header'">
<template #header>
<div class="d-flex row w-100">
<div class="col-auto" style="transform: translateX(-62px)">
<i v-if="getDateStyleClass(termin) == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i>
<i v-else-if="getDateStyleClass(termin) == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i>
<i v-else-if="getDateStyleClass(termin) == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i>
<i v-else-if="getDateStyleClass(termin) == 'standard'" v-tooltip.right="getTooltipStandard" class="fa-solid fa-clock"></i>
<i v-else-if="getDateStyleClass(termin) == 'abgegeben'" v-tooltip.right="getTooltipAbgegeben" class="fa-solid fa-check"></i>
</div>
<div class="col-6 text-start">
<span>{{getAccTabHeaderForTermin(termin)}}</span>
</div>
</div>
</template>
<div class="row">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4fixterminv2')}}</div>
<div class="col-8 col-md-9">
<!-- always keep fixtermin checkbox disabled for mitarbeiter tool -->
<Checkbox
v-model="termin.fixtermin"
disabled
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
</div>
<div class="row mt-2" v-if="termin.bezeichnung?.benotbar">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" :cols=" isMobile ? 30 : 90" :disabled="!termin.allowedToSave"></Textarea>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4zieldatum')}}</div>
<div class="col-8 col-md-9">
<VueDatePicker
v-model="termin.datum"
:clearable="false"
:disabled="!termin.allowedToSave"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="1" :cols=" isMobile ? 30 : 90" :disabled="!termin.allowedToSave"></Textarea>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabetyp')}}</div>
<div class="col-8 col-md-9">
<Dropdown
:style="{'width': '100%'}"
:disabled="!termin.allowedToSave"
v-model="termin.bezeichnung"
@change="handleChangeAbgabetyp(termin)"
:options="activeAbgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp">
</Dropdown>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4abgabedatum') )}}</div>
<div class="col-8 col-md-9">
<template v-if="termin?.abgabedatum">
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
<button v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" class="btn btn-primary">
<a> {{$capitalize( $p.t('abgabetool/c4downloadAbgabe') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
</template>
<template v-else>
{{ $capitalize( $p.t('abgabetool/c4nochNichtsAbgegeben') )}}
</template>
</div>
<div class="row mt-2" v-if="termin.bezeichnung?.paabgabetyp_kurzbz === 'qualgate1' || termin.bezeichnung?.paabgabetyp_kurzbz === 'qualgate2'">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4upload_allowed')}}</div>
<div class="col-8 col-md-9">
<Checkbox
v-model="termin.upload_allowed"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
</div>
<div class="row mt-2" v-if="termin.bezeichnung?.benotbar">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4note')}}</div>
<div class="col-8 col-md-9">
<Dropdown
:style="{'width': '100%'}"
v-model="termin.note"
:options="allowedNotenOptions"
:optionLabel="getNotenOptionLabel">
</Dropdown>
</div>
</div>
<div class="row mt-2" v-if="termin.bezeichnung?.benotbar">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4notizQualGatev2')}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" :cols=" isMobile ? 30 : 90" :disabled="!termin.allowedToSave"></Textarea>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="1" :cols=" isMobile ? 30 : 90" :disabled="!termin.allowedToSave"></Textarea>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabedatum')}}</div>
<div class="col-8 col-md-9">
<template v-if="termin?.abgabedatum">
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
<button v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" class="btn btn-primary">
<a> {{$p.t('abgabetool/c4downloadAbgabe')}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
</template>
<template v-else>
{{ $p.t('abgabetool/c4nochNichtsAbgegeben') }}
</template>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">
<!-- TODO: row description? -->
</div>
<div class="col-8 col-md-9">
<div class="row">
<div class="col-auto">
<button v-if="termin.allowedToSave" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
{{ $p.t('abgabetool/c4save') }}
</div>
<div class="col-8 col-md-9">
<div class="row">
<div class="col-auto">
<button v-if="termin.allowedToSave" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
{{ $capitalize( $p.t('abgabetool/c4save') )}}
<i class="fa-solid fa-floppy-disk"></i>
</button>
<div v-else v-tooltip.right="getTooltipNotAllowedToSave">
<button disabled style="max-height: 40px;" class="btn btn-primary border-0">
{{$capitalize( $p.t('abgabetool/c4save') )}}
<i class="fa-solid fa-floppy-disk"></i>
</button>
</div>
<div class="col-auto">
<button v-if="termin.allowedToDelete && termin.paabgabe_id > 0" style="max-height: 40px;" class="btn btn-danger border-0" @click="handleDeleteTermin(termin)">
{{ $p.t('abgabetool/c4delete') }}
</div>
<div class="col-auto">
<button v-if="termin.allowedToDelete && termin.paabgabe_id > 0" style="max-height: 40px;" class="btn btn-danger border-0" @click="handleDeleteTermin(termin)">
{{$capitalize( $p.t('abgabetool/c4delete') )}}
<i class="fa-solid fa-trash"></i>
</button>
<div v-else v-tooltip.right="getTooltipNotAllowedToDelete">
<button disabled style="max-height: 40px;" class="btn btn-danger border-0">
{{$capitalize( $p.t('abgabetool/c4delete') )}}
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</AccordionTab>
</template>
</Accordion>
</div>
</div>
</AccordionTab>
</template>
</Accordion>
</div>
`,
};
@@ -48,7 +48,7 @@ export const AbgabeStudentDetail = {
methods: {
async validate(termin, endupload = false) {
if(!termin.file.length) {
this.$fhcAlert.alertWarning(this.$p.t('global/warningChooseFile'));
this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningChooseFile')));
return false
}
@@ -57,49 +57,49 @@ export const AbgabeStudentDetail = {
// check these input fields for length of entry
if(this.form['abstract'].length < 100 && await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/warningShortAbstract'),
acceptLabel: this.$p.t('abgabetool/c4AcceptAndProceed'),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$p.t('abgabetool/c4Cancel'),
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
if(this.form['abstract_en'].length < 100 && await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/warningShortAbstractEn'),
acceptLabel: this.$p.t('abgabetool/c4AcceptAndProceed'),
message: this.$capitalize(this.$p.t('abgabetool/warningShortAbstractEn')),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$p.t('abgabetool/c4Cancel'),
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
if(this.form['schlagwoerter'].length < 50 && await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/warningShortSchlagwoerter'),
acceptLabel: this.$p.t('abgabetool/c4AcceptAndProceed'),
message: this.$capitalize(this.$p.t('abgabetool/warningShortSchlagwoerter')),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$p.t('abgabetool/c4Cancel'),
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
if(this.form['schlagwoerter_en'].length < 50 && await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/warningShortSchlagwoerterEn'),
acceptLabel: this.$p.t('abgabetool/c4AcceptAndProceed'),
message: this.$capitalize(this.$p.t('abgabetool/warningShortSchlagwoerterEn')),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$p.t('abgabetool/c4Cancel'),
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
if(this.form['seitenanzahl'] <= 5 && await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/warningSmallSeitenanzahl'),
acceptLabel: this.$p.t('abgabetool/c4AcceptAndProceed'),
message: this.$capitalize(this.$p.t('abgabetool/warningSmallSeitenanzahl')),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$p.t('abgabetool/c4Cancel'),
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
@@ -192,13 +192,13 @@ export const AbgabeStudentDetail = {
},
handleUploadRes(res, termin) {
if(res.meta.status == "success") {
this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/c4fileUploadSuccessv3'))
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3')))
// update 'abgabedatum' for successful upload -> shows the pdf icon and date once set
termin.abgabedatum = new Date().toISOString().split('T')[0];
} else {
this.$fhcAlert.alertError(this.$p.t('abgabetool/c4fileUploadErrorv3'))
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4fileUploadErrorv3')))
}
if(res.meta.signaturInfo) {
@@ -262,8 +262,26 @@ export const AbgabeStudentDetail = {
}
},
computed: {
getActiveIndexTabArray() {
// here we try to do mind reading logic by assuming which abgabetermine are the most relevant to the current user
// lets try to take the termin with nearest date and watch who complains and why
let closestIndex = -1;
let minDiff = Infinity;
const today = new Date();
this.projektarbeit.abgabetermine.forEach((obj, i) => {
const diff = Math.abs(new Date(obj.datum) - today);
if (diff < minDiff) {
minDiff = diff;
closestIndex = i;
}
});
return [closestIndex]
},
getEid() {
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
return this.$capitalize(this.$p.t('abgabetool/c4eidesstattlicheErklaerung'))
},
getAllowedToSendEndupload() {
return !this.eidAkzeptiert
@@ -280,37 +298,56 @@ export const AbgabeStudentDetail = {
},
getTooltipVerspaetet() {
return {
value: this.$p.t('abgabetool/c4tooltipVerspaetet'),
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')),
class: "custom-tooltip"
}
},
getTooltipVerpasst() {
return {
value: this.$p.t('abgabetool/c4tooltipVerpasst'),
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')),
class: "custom-tooltip"
}
},
getTooltipAbzugeben() {
return {
value: this.$p.t('abgabetool/c4tooltipAbzugeben'),
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')),
class: "custom-tooltip"
}
},
getTooltipStandard() {
return {
value: this.$p.t('abgabetool/c4tooltipStandard'),
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandard')),
class: "custom-tooltip"
}
},
getTooltipAbgegeben() {
return {
value: this.$p.t('abgabetool/c4tooltipAbgegeben'),
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')),
class: "custom-tooltip"
}
},
getTooltipFixtermin() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')),
class: "custom-tooltip"
}
},
getTooltipNotAllowedToUpload() {
if(this.isViewMode) {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')),
class: "custom-tooltip"
}
} else {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')),
class: "custom-tooltip"
}
}
}
},
created() {
},
mounted() {
@@ -320,16 +357,15 @@ export const AbgabeStudentDetail = {
<i class="fa-solid fa-spinner fa-pulse fa-5x"></i>
</div>
<div v-if="projektarbeit">
<h5>{{$p.t('abgabetool/c4abgabeStudentenbereich')}}</h5>
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}</h5>
<div class="row">
<p> {{projektarbeit?.betreuer}}</p>
<p> {{projektarbeit?.titel}}</p>
</div>
<Accordion :multiple="true" :activeIndex="[0]">
<Accordion :multiple="true" :activeIndex="getActiveIndexTabArray">
<template v-for="termin in this.projektarbeit?.abgabetermine">
<AccordionTab :headerClass="getDateStyleClass(termin) + '-header'">
<template #header>
@@ -341,26 +377,32 @@ export const AbgabeStudentDetail = {
<i v-else-if="getDateStyleClass(termin) == 'standard'" v-tooltip.right="getTooltipStandard" class="fa-solid fa-clock"></i>
<i v-else-if="getDateStyleClass(termin) == 'abgegeben'" v-tooltip.right="getTooltipAbgegeben" class="fa-solid fa-check"></i>
</div>
<div class="col-6 text-start">
<span>{{getAccTabHeaderForTermin(termin)}}</span>
<div class="col-auto text-start" style="min-width: max(150px, 15%); max-width: min(300px, 30%); transform: translateX(-30px)">
<span>{{ termin?.bezeichnung }}</span>
</div>
<div class="col-auto text-start" style="min-width: 100px; transform: translateX(-30px)">
<span>{{ formatDate(termin.datum, false) }}</span>
</div>
<div v-if="termin?.fixtermin" class="col-auto" style="transform: translateX(-30px)">
<i v-tooltip.right="getTooltipFixtermin" class="fa-solid fa-lock"></i>
</div>
</div>
</template>
<div class="row">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4fixterminv2')}}</div>
<div class="col-8 col-md-9">
<Checkbox
disabled
v-model="termin.fixtermin"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
</div>
<!-- <div class="row">-->
<!-- <div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4fixterminv2')}}</div>-->
<!-- <div class="col-8 col-md-9">-->
<!-- <Checkbox -->
<!-- disabled-->
<!-- v-model="termin.fixtermin"-->
<!-- :binary="true" -->
<!-- :pt="{ root: { class: 'ml-auto' }}"-->
<!-- >-->
<!-- </Checkbox>-->
<!-- </div>-->
<!-- </div>-->
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4zieldatum')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
<div class="col-8 col-md-9">
<VueDatePicker
v-model="termin.datum"
@@ -375,57 +417,56 @@ export const AbgabeStudentDetail = {
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabetyp')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4abgabetypv2') )}}</div>
<div class="col-8 col-md-9">
{{ termin.bezeichnung }}
</div>
</div>
<div class="row mt-2" v-if="termin.paabgabetyp_kurzbz === 'qualgate1' || termin.paabgabetyp_kurzbz === 'qualgate2'">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4note')}}</div>
<div class="row mt-2" v-if="termin.note">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4note') )}}</div>
<div class="col-8 col-md-9">
<div v-if="termin.paabgabetyp_kurzbz === 'qualgate1' || termin.paabgabetyp_kurzbz === 'qualgate2'" class="col-1 d-flex justify-content-start align-items-start">
<div class="col-1 d-flex justify-content-start align-items-start">
{{ getTerminNoteBezeichnung(termin) }}
</div>
</div>
</div>
<div class="row mt-2" v-if="termin.paabgabetyp_kurzbz === 'qualgate1' || termin.paabgabetyp_kurzbz === 'qualgate2'">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4notizQualGatev2')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" :cols=" isMobile ? 30 : 90" disabled></Textarea>
</div>
</div>
<div v-if="termin.kurzbz && termin.kurzbz.length > 0" class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div>
<div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="1" :cols=" isMobile ? 25 : 90" :disabled="true"></Textarea>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4abgabedatum')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4abgabedatum') )}}</div>
<div class="col-8 col-md-9">
<template v-if="termin?.abgabedatum">
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
<button v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" class="btn btn-primary">
<a> {{$p.t('abgabetool/c4downloadAbgabe')}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
<a> {{$capitalize($p.t('abgabetool/c4downloadAbgabe') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
</template>
<template v-else>
{{ $p.t('abgabetool/c4nochNichtsAbgegeben') }}
{{ $capitalize( $p.t('abgabetool/c4nochNichtsAbgegeben') )}}
</template>
</div>
</div>
<div class="row mt-2" v-if="termin.upload_allowed">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4fileupload')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4fileupload') )}}</div>
<div class="col-8 col-md-9">
<div class="row">
<div class="row" v-if="termin?.allowedToUpload">
<div class="col-12 col-sm-6 mb-2">
<Upload
:disabled="!termin?.allowedToUpload || isViewMode"
accept=".pdf"
v-model="termin.file"
></Upload>
@@ -434,9 +475,27 @@ export const AbgabeStudentDetail = {
<button
class="btn btn-primary border-0 w-100"
@click="upload(termin)"
:disabled="!termin.allowedToUpload || isViewMode"
>
{{$p.t('abgabetool/c4upload')}}
{{$capitalize( $p.t('abgabetool/c4upload') )}}
<i class="fa-solid fa-upload"></i>
</button>
</div>
</div>
<div class="row" v-else-if="!termin?.allowedToUpload || isViewMode" v-tooltip.right="getTooltipNotAllowedToUpload">
<div class="col-12 col-sm-6 mb-2">
<Upload
disabled
accept=".pdf"
v-model="termin.file"
></Upload>
</div>
<div class="col-12 col-sm-6">
<button
class="btn btn-primary border-0 w-100"
@click="upload(termin)"
disabled
>
{{$capitalize( $p.t('abgabetool/c4upload') )}}
<i class="fa-solid fa-upload"></i>
</button>
</div>
@@ -454,7 +513,7 @@ export const AbgabeStudentDetail = {
dialogClass="bordered-modal modal-lg">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4enduploadZusatzdaten')}}
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
</div>
<div class="row mb-3 align-items-start">
@@ -463,13 +522,13 @@ export const AbgabeStudentDetail = {
</div>
<div class="row mb-3 align-items-start">
<p class="ml-4 mr-4">{{$p.t('abgabetool/c4titel')}}: {{ projektarbeit?.titel }}</p>
<p class="ml-4 mr-4">{{$capitalize( $p.t('abgabetool/c4titel') )}}: {{ projektarbeit?.titel }}</p>
</div>
</template>
<template v-slot:default>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4Sprache')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4Sprache') )}}</div>
<div class="row">
<Dropdown
:style="{'width': '100%'}"
@@ -490,21 +549,21 @@ export const AbgabeStudentDetail = {
<!-- -->
<!-- </div>-->
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4schlagwoerterGer')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter"></Textarea>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4schlagwoerterEng')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterEng') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter_en"></Textarea>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4abstractGer')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4abstractGer') )}}</div>
<div class="row">
<Textarea v-model="form.abstract" rows="10" maxlength="5000"></Textarea>
<p>{{ form.abstract?.length ? form.abstract.length : 0 }} / 5000 characters</p>
@@ -512,7 +571,7 @@ export const AbgabeStudentDetail = {
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4abstractEng')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4abstractEng') )}}</div>
<div class="row">
<Textarea v-model="form.abstract_en" rows="10" maxlength="5000"></Textarea>
<p>{{ form.abstract_en?.length ? form.abstract_en.length : 0 }} / 5000 characters</p>
@@ -520,7 +579,7 @@ export const AbgabeStudentDetail = {
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4seitenanzahl')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4seitenanzahl') )}}</div>
<div class="row">
<InputNumber
v-model="form.seitenanzahl"
@@ -533,7 +592,7 @@ export const AbgabeStudentDetail = {
<div v-html="getEid"></div>
<div class="row">
<div class="col-9"></div>
<div class="col-2"><p>{{ $p.t('abgabetool/c4gelesenUndAkzeptiert') }}</p></div>
<div class="col-2"><p>{{$capitalize( $p.t('abgabetool/c4gelesenUndAkzeptiert') )}}</p></div>
<div class="col-1">
<Checkbox
@@ -548,7 +607,7 @@ export const AbgabeStudentDetail = {
</template>
<template v-slot:footer>
<button class="btn btn-primary" :disabled="getAllowedToSendEndupload" @click="triggerEndupload">{{$p.t('ui/hochladen')}}</button>
<button class="btn btn-primary" :disabled="getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
</template>
</bs-modal>
@@ -0,0 +1,534 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import AbgabeDetail from "./AbgabeMitarbeiterDetail.js";
import VerticalSplit from "../../verticalsplit/verticalsplit.js"
import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
export const AbgabetoolAssistenz = {
name: "AbgabetoolAssistenz",
components: {
BsModal,
CoreFilterCmpt,
AbgabeDetail,
VerticalSplit,
Dropdown: primevue.dropdown,
Textarea: primevue.textarea,
VueDatePicker
},
provide() {
return {
abgabeTypeOptions: Vue.computed(() => this.abgabeTypeOptions),
allowedNotenOptions: Vue.computed(() => this.allowedNotenOptions),
turnitin_link: Vue.computed(() => this.turnitin_link),
old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link)
}
},
props: {
viewData: {
type: Object,
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.name && value.uid
}
}
},
data() {
return {
detailIsFullscreen: false,
showZweitbetreuerCol: false,
phrasenPromise: null,
phrasenResolved: false,
turnitin_link: null,
old_abgabe_beurteilung_link: null,
saving: false,
loading: false,
abgabeTypeOptions: null,
notenOptions: null,
allowedNotenOptions: null,
serienTermin: Vue.reactive({
datum: new Date(),
bezeichnung: {
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
},
kurzbz: ''
}),
showAll: false,
tabulatorUuid: Vue.ref(0),
selectedData: [],
domain: '',
student_uid: null,
detail: null,
detailOffset: 0,
projektarbeiten: null,
selectedProjektarbeit: null,
tableBuiltResolve: null,
tableBuiltPromise: null,
abgabeTableOptions: {
minHeight: 250,
index: 'projektarbeit_id',
layout: 'fitDataStretch',
placeholder: this.$p.t('global/noDataAvailable'),
selectable: true,
selectableCheck: this.selectionCheck,
rowHeight: 80,
columns: [
{
formatter: 'rowSelection',
titleFormatter: 'rowSelection',
titleFormatterParams: {
rowRange: "active" // Only toggle the values of the active filtered rows
},
hozAlign:"center",
headerSort: false,
frozen: true,
width: 70
},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
// {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: true, field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, tooltip: false}
// {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), headerFilter: true, field: 'nextTermin', formatter: this.abgabterminFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4kontakt'))), field: 'mail', formatter: this.mailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuer'))), field: 'erstbetreuer', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuer'))), field: 'zweitbetreuer', formatter: this.centeredTextFormatter, widthGrow: 1, visible: this.showZweitbetreuerCol}
],
persistence: false,
},
abgabeTableEventHandlers: [{
event: "tableBuilt",
handler: async () => {
this.tableBuiltResolve()
}
},
{
event: "cellClick",
handler: async (e, cell) => {
if(cell.getColumn().getField() === "details") {
this.setDetailComponent(cell.getValue())
this.undoSelection(cell)
} else if (cell.getColumn().getField() === "mail") {
this.undoSelection(cell)
}
}
},
{
event: "rowSelectionChanged",
handler: async(data) => {
this.selectedData = data
}
}
]};
},
methods: {
handleToggleFullscreenDetail() {
this.detailIsFullscreen = !this.detailIsFullscreen
},
getOptionLabelAbgabetyp(option){
return option.bezeichnung
},
formatDate(dateParam) {
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`;
},
undoSelection(cell) {
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
// but actually just revert it after the fact
const row = cell.getRow()
if(row.isSelected()) {
row.deselect();
}
},
selectionCheck(row) {
const data = row.getData()
if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false
return true
},
showDeadlines(){
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/Cis/Abgabetool/Deadlines'
window.open(link, '_blank')
},
toggleShowAll(showall) {
this.showAll = showall
this.loading = true
this.loadProjektarbeiten(showall, () => {
this.$refs.abgabeTable?.tabulator.redraw(true)
this.$refs.abgabeTable?.tabulator.setSort([]);
this.loading = false
})
},
openAddSeriesModal() {
this.$refs.modalContainerAddSeries.show()
},
addSeries() {
this.saving = true
this.$api.call(ApiAbgabe.postSerientermin(
this.serienTermin.datum.toISOString(),
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
this.serienTermin.bezeichnung.bezeichnung,
this.serienTermin.kurzbz,
this.selectedData?.map(projekt => projekt.projektarbeit_id)
)).then(res => {
if (res.meta.status === "success" && res.data) {
this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/serienTerminGespeichert'))
// TODO: sticky lifetime erhöhen um sinnvoll lesen zu können?
this.$fhcAlert.alertInfo(this.$p.t('abgabetool/serienTerminEmailSentInfo', [this.createInfoString(res.data)]));
} else {
this.$fhcAlert.alertError(this.$p.t('abgabetool/errorSerienterminSpeichern'))
}
}).finally(()=>{
this.saving = false
})
this.$refs.modalContainerAddSeries.hide()
},
createInfoString(data) {
let str = '';
data.forEach(name => {
str += name
str += '; '
})
return str
},
isPastDate(date) {
return new Date(date) < new Date(Date.now())
},
setDetailComponent(details){
const pa = this.projektarbeiten.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
// pa.isCurrent = res.data[1]
pa.abgabetermine.forEach(termin => {
termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note)
termin.file = []
termin.allowedToSave = termin.insertvon == this.viewData?.uid && pa.betreuerart_kurzbz != 'Zweitbegutachter'
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
})
pa.student_uid = details.student_uid
pa.student = `${pa.vorname} ${pa.nachname}`
this.selectedProjektarbeit = pa
this.$refs.modalContainerAbgabeDetail.show()
},
centeredTextFormatter(cell) {
const val = cell.getValue()
if(!val) return
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<p style="max-width: 100%; width: 100%; overflow-wrap: break-word; word-break: break-word; white-space: normal; margin: 0px; text-align: center">'+val+'</p></div>'
},
detailFormatter(cell) {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
},
mailFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
},
beurteilungFormatter(cell) {
const val = cell.getValue()
if(val) {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
} else return '-'
},
pkzTextFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</p></div>'
},
abgabterminFormatter(termin) {
},
tableResolve(resolve) {
this.tableBuiltResolve = resolve
},
buildMailToLink(abgabe) {
return 'mailto:' + abgabe.student_uid +'@'+ this.domain
},
buildPKZ(projekt) {
return `${projekt.student_uid} / ${projekt.matrikelnr}`
},
buildStg(projekt) {
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
},
buildErstbetreuer(projekt) {
return projekt.betreuer_vorname + ' ' + projekt.betreuer_nachname + ' - ' + projekt.betreuerart + '(' + projekt.betreuer_benutzer_uid + ')'
},
buildZweitbetreuer(projekt) {
return projekt.zweitbetreuer_full_name ?? ''
},
setupData(data){
this.projektarbeiten = data[0]
this.domain = data[1]
const d = data[0].map(projekt => {
let mode = 'detailTermine'
// only show 2tbetreuer col if any projektarbeit has one
if(projekt.zweitbetreuer_full_name) this.showZweitbetreuerCol = true
// first Abgabetermin in the past
projekt.abgabetermine.forEach(termin => {
const date = luxon.DateTime.fromISO(termin.datum)
termin.diff = date.diffNow('milliseconds')
// console.log(termin.diff)
})
// console.log('\n\n')
return {
...projekt,
abgabetermine: projekt.abgabetermine,
details: {
student_uid: projekt.uid,
projektarbeit_id: projekt.projektarbeit_id,
},
pkz: this.buildPKZ(projekt),
beurteilung: projekt.beurteilungLink ?? null,
sem: projekt.studiensemester_kurzbz,
stg: this.buildStg(projekt),
mail: this.buildMailToLink(projekt),
erstbetreuer: this.buildErstbetreuer(projekt),
zweitbetreuer: this.buildZweitbetreuer(projekt),
typ: projekt.projekttyp_kurzbz,
titel: projekt.titel
}
})
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
this.$refs.abgabeTable.tabulator.setData(d);
},
loadProjektarbeiten(all = false, callback) {
this.loading = true
this.$api.call(ApiAbgabe.getProjektarbeitenForStudiengang(this.getCurrentStudiengang))
.then(res => {
if(res?.data) this.setupData(res.data)
}).finally(() => {
if(callback) {
callback()
}
}).finally(()=>{
this.loading=false
})
},
loadAbgaben(details) {
return new Promise((resolve) => {
this.$api.call(ApiAbgabe.getStudentProjektabgaben(details))
.then(res => {
resolve(res)
})
})
},
handleUuidDefined(uuid) {
this.tabulatorUuid = uuid
},
calcMaxTableHeight() {
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
this.abgabeTableOptions.height = window.visualViewport.height - rect.top - 80
this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height)
},
async setupMounted() {
this.tableBuiltPromise = new Promise(this.tableResolve)
await this.tableBuiltPromise
this.loadProjektarbeiten()
// this.$refs.verticalsplit.collapseBottom()
this.calcMaxTableHeight()
},
sendEmailBegutachter() {
// TODO: implement
},
sendEmailStudierende() {
// TODO: implement
}
},
watch: {
},
computed: {
getCurrentStudiengang() {
// TODO: sophisticated logic pulling from default value by viewData or dropdown select
return 257
}
},
created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
// fetch config to avoid hard coded links
this.$api.call(ApiAbgabe.getConfig()).then(res => {
this.turnitin_link = res.data?.turnitin_link
this.old_abgabe_beurteilung_link = res.data?.old_abgabe_beurteilung_link
}).catch(e => {
console.log(e)
this.loading = false
})
// fetch noten options
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
this.$api.call(ApiAbgabe.getNoten()).then(res => {
this.notenOptions = res.data
// TODO: more sophisticated way to filter for these two, in essence it is still hardcoded
this.allowedNotenOptions = this.notenOptions.filter(
opt => opt.bezeichnung === 'Bestanden'
|| opt.bezeichnung === 'Nicht bestanden'
)
}).catch(e => {
this.loading = false
})
// fetch abgabetypen options
this.$api.call(ApiAbgabe.getPaAbgabetypen()).then(res => {
this.abgabeTypeOptions = res.data
}).catch(e => {
this.loading = false
})
},
mounted() {
this.setupMounted()
},
template: `
<template v-if="phrasenResolved">
<div id="loadingOverlay" v-show="loading || saving" style="position: absolute; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.5); z-index: 99999999999;">
<i class="fa-solid fa-spinner fa-pulse fa-5x"></i>
</div>
<bs-modal ref="modalContainerAddSeries" class="bootstrap-prompt"
dialogClass="modal-lg">
<template v-slot:title>
<div>
{{ $p.t('abgabetool/neueTerminserie') }}
</div>
</template>
<template v-slot:default>
<div class="row">
<div class="col-3 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4zieldatum')}}
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabetypv2')}}
</div>
<div class="col-6 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabekurzbz')}}
</div>
</div>
<div class="row">
<div class="col-3 d-flex justify-content-center align-items-center">
<div>
<VueDatePicker
style="width: 95%;"
v-model="serienTermin.datum"
:clearable="false"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
<Dropdown
:style="{'width': '100%'}"
v-model="serienTermin.bezeichnung"
:options="abgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp">
</Dropdown>
</div>
<div class="col-6 d-flex justify-content-center align-items-center">
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" cols="40"></Textarea>
</div>
</div>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="addSeries">{{ $p.t('global/speichern') }}</button>
</template>
</bs-modal>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true"
@toggle-fullscreen="handleToggleFullscreenDetail">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4abgabeMitarbeiterDetailTitle')}}
</div>
</template>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit" :isFullscreen="detailIsFullscreen"></AbgabeDetail>
</template>
</bs-modal>
<!-- low max height on this vsplit wrapper to avoid padding scrolls, elements have their inherent height anyways -->
<div style="max-height:40vw;">
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<hr>
<core-filter-cmpt
:title="''"
@uuidDefined="handleUuidDefined"
ref="abgabeTable"
:newBtnShow="true"
:newBtnLabel="$p.t('abgabetool/neueTerminserie')"
:newBtnDisabled="!selectedData.length"
@click:new=openAddSeriesModal
:tabulator-options="abgabeTableOptions"
:tabulator-events="abgabeTableEventHandlers"
tableOnly
:sideMenu="false"
:useSelectionSpan="false"
>
<template #actions>
<button @click="sendEmailStudierende" role="button" class="btn btn-secondary ml-2">
{{ $p.t('abgabetool/sendEmailStudierende') }}
</button>
<button @click="sendEmailBegutachter" role="button" class="btn btn-secondary ml-2">
{{ $p.t('abgabetool/sendEmailBegutachter') }}
</button>
</template>
</core-filter-cmpt>
</div>
</template>
`,
};
export default AbgabetoolAssistenz;
@@ -36,6 +36,9 @@ export const AbgabetoolMitarbeiter = {
},
data() {
return {
detailIsFullscreen: false,
phrasenPromise: null,
phrasenResolved: false,
turnitin_link: null,
old_abgabe_beurteilung_link: null,
saving: false,
@@ -82,16 +85,16 @@ export const AbgabetoolMitarbeiter = {
frozen: true,
width: 70
},
{title: Vue.computed(() => this.$p.t('abgabetool/c4details')), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4personenkennzeichen')), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4kontakt')), field: 'mail', formatter: this.mailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4vorname')), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4nachname')), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4projekttyp')), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4titel')), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$p.t('abgabetool/c4betreuerart')), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1}
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4kontakt'))), field: 'mail', formatter: this.mailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerart'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1}
],
persistence: false,
},
@@ -121,6 +124,9 @@ export const AbgabetoolMitarbeiter = {
]};
},
methods: {
handleToggleFullscreenDetail() {
this.detailIsFullscreen = !this.detailIsFullscreen
},
getOptionLabelAbgabetyp(option){
return option.bezeichnung
},
@@ -202,6 +208,7 @@ export const AbgabetoolMitarbeiter = {
return new Date(date) < new Date(Date.now())
},
setDetailComponent(details){
this.loading=true
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
@@ -226,6 +233,7 @@ export const AbgabetoolMitarbeiter = {
pa.abgabetermine.forEach(termin => {
termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note)
termin.file = []
termin.allowedToSave = termin.insertvon == this.viewData?.uid && pa.betreuerart_kurzbz != 'Zweitbegutachter'
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
@@ -239,7 +247,7 @@ export const AbgabetoolMitarbeiter = {
this.$refs.modalContainerAbgabeDetail.show()
})
}).finally(()=>{this.loading = false})
},
centeredTextFormatter(cell) {
const val = cell.getValue()
@@ -356,6 +364,8 @@ export const AbgabetoolMitarbeiter = {
},
created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
// fetch config to avoid hard coded links
this.$api.call(ApiAbgabe.getConfig()).then(res => {
this.turnitin_link = res.data?.turnitin_link
@@ -389,6 +399,7 @@ export const AbgabetoolMitarbeiter = {
this.setupMounted()
},
template: `
<template v-if="phrasenResolved">
<div id="loadingOverlay" v-show="loading || saving" style="position: absolute; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.5); z-index: 99999999999;">
<i class="fa-solid fa-spinner fa-pulse fa-5x"></i>
</div>
@@ -406,7 +417,7 @@ export const AbgabetoolMitarbeiter = {
{{$p.t('abgabetool/c4zieldatum')}}
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabetyp')}}
{{$p.t('abgabetool/c4abgabetypv2')}}
</div>
<div class="col-6 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabekurzbz')}}
@@ -446,20 +457,21 @@ export const AbgabetoolMitarbeiter = {
</bs-modal>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true">
dialogClass="modal-xl" :allowFullscreenExpand="true"
@toggle-fullscreen="handleToggleFullscreenDetail">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4abgabeMitarbeiterDetailTitle')}}
</div>
</template>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
<AbgabeDetail :projektarbeit="selectedProjektarbeit" :isFullscreen="detailIsFullscreen"></AbgabeDetail>
</template>
</bs-modal>
<!-- low max height on this vsplit wrapper to avoid padding scrolls, elements have their inherent height anyways -->
<div style="max-height:40vw;">
<!-- low max height on this vsplit wrapper to avoid padding scrolls, elements have their inherent height anyways -->
<div style="max-height:40vw;">
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<hr>
@@ -493,6 +505,7 @@ export const AbgabetoolMitarbeiter = {
</core-filter-cmpt>
</div>
</template>
`,
};
@@ -34,6 +34,9 @@ export const AbgabetoolStudent = {
},
data() {
return {
phrasenPromise: null,
phrasenResolved: false,
loading: false,
notenOptions: null,
domain: '',
student_uid: null,
@@ -64,6 +67,7 @@ export const AbgabetoolStudent = {
return new Date(date) < new Date(Date.now())
},
setDetailComponent(details){
this.loading = true
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
@@ -71,16 +75,15 @@ export const AbgabetoolStudent = {
termin.file = []
termin.allowedToUpload = false
// termin.datum = '2025-10-16'
// TODO: fixtermin logic?
// TODO: change this and corresponding tooltips every time FH decides QG work different now
if(termin.paabgabetyp_kurzbz == 'end') {
termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGates(pa.abgabetermine)
} else if(termin.paabgabetyp_kurzbz == 'qualgate1' || termin.paabgabetyp_kurzbz == 'qualgate2') {
termin.allowedToUpload = termin.upload_allowed
} else if(termin.fixtermin) {
termin.allowedToUpload = !this.isPastDate(termin.datum)
} else {
termin.allowedToUpload = true
// TODO: this will confuse people so much, by requirement we should NOT show people this flag
// but it still should have an effect?
termin.allowedToUpload = termin.upload_allowed
}
})
@@ -92,7 +95,7 @@ export const AbgabetoolStudent = {
this.$refs.modalContainerAbgabeDetail.show()
// this.$refs.verticalsplit.showBoth()
})
}).finally(()=>{this.loading=false})
},
centeredTextFormatter(cell) {
const val = cell.getValue()
@@ -137,9 +140,9 @@ export const AbgabetoolStudent = {
// this.projektarbeiten = data[0]
this.domain = data[1]
this.student_uid = data[2]
this.projektarbeiten = data[0] ?? null
if(!this.projektarbeiten) return
this.projektarbeiten = this.projektarbeiten.map(projekt => {
const projektarbeiten = data[0] ?? null
if(!projektarbeiten) return
this.projektarbeiten = projektarbeiten.map(projekt => {
let mode = 'detailTermine'
if (projekt.babgeschickt || projekt.zweitbetreuer_abgeschickt) {
@@ -166,7 +169,7 @@ export const AbgabetoolStudent = {
titel: projekt.titel
}
})
},
loadProjektarbeiten() {
this.$api.call(ApiAbgabe.getStudentProjektarbeiten(this.student_uid_prop || this.viewData?.uid || null))
@@ -218,11 +221,14 @@ export const AbgabetoolStudent = {
}
},
async created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
this.loading = true
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
await this.$api.call(ApiAbgabe.getNoten()).then(res => {
this.notenOptions = res.data
}).catch(e => {
}).finally(() => {
this.loading = false
})
@@ -231,12 +237,16 @@ export const AbgabetoolStudent = {
this.setupMounted()
},
template: `
<template v-if="phrasenResolved">
<div id="loadingOverlay" v-show="loading || saving" style="position: absolute; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.5); z-index: 99999999999;">
<i class="fa-solid fa-spinner fa-pulse fa-5x"></i>
</div>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4abgabeStudentDetailTitle')}}
{{$capitalize( $p.t('abgabetool/c4abgabeStudentDetailTitle') )}}
</div>
</template>
<template v-slot:default>
@@ -245,11 +255,11 @@ export const AbgabetoolStudent = {
</template>
</bs-modal>
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<h2>{{$capitalize( $p.t('abgabetool/abgabetoolTitle') )}}</h2>
<hr>
<div v-if="projektarbeiten === null">
{{$p.t('abgabetool/c4abgabeStudentNoProjectsFound')}}
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
</div>
<Accordion :multiple="true" :activeIndex="[0]">
@@ -268,30 +278,30 @@ export const AbgabetoolStudent = {
</template>
<div class="row">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4details')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4details') )}}</div>
<div class="col-8 col-md-9">
<button @click="setDetailComponent(projektarbeit.details)" class="btn btn-primary">
{{$p.t('abgabetool/c4projektdetailsOeffnen')}} <a><i class="fa fa-folder-open"></i></a>
{{$capitalize( $p.t('abgabetool/c4projektdetailsOeffnen') )}} <a><i class="fa fa-folder-open"></i></a>
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4beurteilung')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4beurteilung') )}}</div>
<div class="col-8 col-md-9">
<button v-if="projektarbeit.beurteilung" @click="handleDownloadBeurteilung(projektarbeit)" class="btn btn-primary">
<a> {{$p.t('abgabetool/c4downloadBeurteilung')}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
<a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilung') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
<a v-else>{{$p.t('abgabetool/c4nobeurteilungVorhanden')}}</a>
<a v-else>{{$capitalize( $p.t('abgabetool/c4nobeurteilungVorhanden') )}}</a>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4sem')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4sem') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.sem }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4stg')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4stg') )}}</div>
<div class="col-8 col-md-9">
<div class="col-1 d-flex justify-content-start align-items-start">
{{ projektarbeit.stg }}
@@ -299,31 +309,31 @@ export const AbgabetoolStudent = {
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4betreuer')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuer') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.betreuer }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4betreuerEmailKontakt')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontakt') )}}</div>
<div class="col-8 col-md-9">
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
</div>
</div>
<div v-if="projektarbeit.zweitbetreuer" class="row mt-2">
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ projektarbeit.zweitbetreuer_betreuerart_kurzbz}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.zweitbetreuer_betreuerart_beschreibung}}: {{ projektarbeit.zweitbetreuer?.first }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4projekttyp')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4projekttyp') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.projekttypbezeichnung }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$p.t('abgabetool/c4titel')}}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4titel') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.titel }}
</div>
@@ -331,7 +341,7 @@ export const AbgabetoolStudent = {
</AccordionTab>
</template>
</Accordion>
</template>
`,
};
@@ -34,7 +34,7 @@ export const DeadlineOverview = {
columns: [
{title: Vue.computed(() => this.$p.t('abgabetool/c4zieldatum')), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4fixterminv2')), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabetyp')), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabetypv2')), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabekurzbz')), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3},
{title: Vue.computed(() => this.$p.t('person/studentIn')), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2},
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1},
+4
View File
@@ -0,0 +1,4 @@
export function capitalize(string) {
if (!string) return '';
return string[0].toUpperCase() + string.slice(1);
}