Merge branch 'feature-61164/AbgabetoolQualityGates'

This commit is contained in:
Andreas Österreicher
2026-02-27 10:48:35 +01:00
5 changed files with 100 additions and 32 deletions
+2
View File
@@ -41,3 +41,5 @@ $config['STG_MOODLE_LINK'] = 'https://moodle.technikum-wien.at/course/view.php?i
$config['ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT'] = true;
$config['ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'] = true;
$config['BETREUER_SAMMELMAIL_BUTTON_STUDENT'] = true;
@@ -89,13 +89,15 @@ class Abgabe extends FHCAPI_Controller
$abgabetypenBetreuer = $this->config->item('ALLOWED_ABGABETYPEN_BETREUER');
$ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT');
$ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER');
$BETREUER_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('BETREUER_SAMMELMAIL_BUTTON_STUDENT');
$ret = array(
'old_abgabe_beurteilung_link' => $old_abgabe_beurteilung_link,
'turnitin_link' => $turnitin_link,
'abgabetypenBetreuer' => $abgabetypenBetreuer,
'ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT' => $ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT,
'ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER' => $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER
'ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER' => $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER,
'BETREUER_SAMMELMAIL_BUTTON_STUDENT' => $BETREUER_SAMMELMAIL_BUTTON_STUDENT,
);
$this->terminateWithSuccess($ret);
@@ -17,6 +17,7 @@ export const AbgabeMitarbeiterDetail = {
Message: primevue.message,
VueDatePicker
},
emits: ['paUpdated'],
inject: [
'abgabeTypeOptions',
'abgabetypenBetreuer',
@@ -132,8 +133,8 @@ export const AbgabeMitarbeiterDetail = {
const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note)
existingTerminRes.note = noteOptExisting
const existingTerminResCurrObj = this.projektarbeit.abgabetermine.find(paa => paa.paabgabe_id == existingTerminRes.paabgabe_id)
existingTerminResCurrObj.noteBackend = noteOpt // do NOT take noteOptExisting -> should reflect the "yes the qgate grade is confirmed in backend ux behaviour"
termin.paabgabetyp_kurzbz = newTerminRes.paabgabetyp_kurzbz
termin.noteBackend = noteOpt // do NOT take noteOptExisting -> should reflect the "yes the qgate grade is confirmed in backend ux behaviour"
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
}
@@ -174,6 +175,8 @@ export const AbgabeMitarbeiterDetail = {
} else {
this.showAutomagicModalPhrase = false
}
this.$emit("paUpdated", this.projektarbeit)
} else if(res?.meta?.status == 'error'){
this.$fhcAlert.alertError()
}
@@ -257,6 +260,7 @@ export const AbgabeMitarbeiterDetail = {
// this.$p.t('global/tooltipLektorDeleteKontrolle', [this.$entryParams.permissions.kontrolleDeleteMaxReach ])
const deletedTerminIndex = this.projektarbeit.abgabetermine.findIndex(t => t.paabgabe_id === termin.paabgabe_id)
this.projektarbeit.abgabetermine.splice(deletedTerminIndex, 1)
this.$emit("paUpdated", this.projektarbeit)
} else if(res?.meta?.status == 'error'){
this.$fhcAlert.alertError()
}
@@ -210,12 +210,12 @@ export const AbgabetoolAssistenz = {
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false},
field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin',
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false},
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))),
headerFilter: 'list',
headerFilterParams: { valuesLookup: this.getQGateStatusList },
@@ -226,7 +226,7 @@ export const AbgabetoolAssistenz = {
field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false},
],
persistence: false,
persistenceID: "abgabetool_2026_02"
persistenceID: "abgabetool_2026_02_26"
},
abgabeTableEventHandlers: [
{
@@ -247,6 +247,10 @@ export const AbgabetoolAssistenz = {
]};
},
methods: {
handlePaUpdated(projektarbeit) {
this.checkAbgabetermineProjektarbeit(projektarbeit)
this.$refs.abgabeTable.tabulator.redraw(true)
},
getQGateStatusList() {
return [
this.$p.t('abgabetool/c4keinTerminVorhanden'),
@@ -309,11 +313,13 @@ export const AbgabetoolAssistenz = {
return false
},
sammelMailStudent(param) {
const emails = this.selectedData
.map(row => `${row.student_uid}@${this.domain}`)
.join(',');
const uniqueRecipients = [...new Set(emails)];
const recipientList = [];
this.selectedData.forEach(d => {
recipientList.push(`${d.student_uid}@${this.domain}`)
})
const uniqueRecipients = [...new Set(recipientList)];
const subject = this.$p.t('abgabetool/c4sammelmailStudentBetreff', [this.selectedStudiengangOption?.bezeichnung]);
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
},
@@ -362,7 +368,6 @@ export const AbgabetoolAssistenz = {
return false;
},
checkQualityGateStatus(projekt) {
// TODO: might refine the representation of these states and maybe refactor code a little
const qgate1Termine = []
const qgate2Termine = []
@@ -382,7 +387,7 @@ export const AbgabetoolAssistenz = {
// reuse luxon calculated diffMs (termin.datum in relation to today) from previous datestyle check
qgate1Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note)
const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) {
projekt.qgate1Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate1StatusRank = 5
@@ -404,7 +409,7 @@ export const AbgabetoolAssistenz = {
qgate2Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note)
const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) {
projekt.qgate2Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate2StatusRank = 5
@@ -1287,7 +1292,13 @@ export const AbgabetoolAssistenz = {
</div>
</template>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit" :isFullscreen="detailIsFullscreen" :assistenzMode="true"></AbgabeDetail>
<AbgabeDetail
:projektarbeit="selectedProjektarbeit"
:isFullscreen="detailIsFullscreen"
:assistenzMode="true"
@paUpdated="handlePaUpdated">
</AbgabeDetail>
</template>
</bs-modal>
@@ -6,6 +6,7 @@ import ApiAbgabe from '../../../api/factory/abgabe.js'
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass } from "./getDateStyleClass.js";
import { dateFilter } from '../../../tabulator/filters/Dates.js';
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
export const AbgabetoolMitarbeiter = {
name: "AbgabetoolMitarbeiter",
@@ -16,6 +17,7 @@ export const AbgabetoolMitarbeiter = {
Checkbox: primevue.checkbox,
Dropdown: primevue.dropdown,
Textarea: primevue.textarea,
TieredMenu: primevue.tieredmenu,
VueDatePicker,
FhcOverlay
},
@@ -48,6 +50,7 @@ export const AbgabetoolMitarbeiter = {
phrasenResolved: false,
turnitin_link: null,
old_abgabe_beurteilung_link: null,
BETREUER_SAMMELMAIL_BUTTON_STUDENT: null,
saving: false,
loading: false,
abgabeTypeOptions: null,
@@ -139,7 +142,6 @@ export const AbgabetoolMitarbeiter = {
},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, headerFilter: false, headerSort: false, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'},
{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, visible: 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},
@@ -151,12 +153,12 @@ export const AbgabetoolMitarbeiter = {
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false},
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin',
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false},
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))),
headerFilter: 'list',
headerFilterParams: { valuesLookup: this.getQGateStatusList },
@@ -167,7 +169,7 @@ export const AbgabetoolMitarbeiter = {
field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}
],
persistence: false,
persistenceID: 'abgabeTableBetreuer2026-02-24'
persistenceID: 'abgabeTableBetreuer2026-02-26'
},
abgabeTableEventHandlers: [{
event: "tableBuilt",
@@ -203,6 +205,20 @@ export const AbgabetoolMitarbeiter = {
]};
},
methods: {
handlePaUpdated(projektarbeit) {
this.checkAbgabetermineProjektarbeit(projektarbeit)
this.$refs.abgabeTable.tabulator.redraw(true)
},
sammelMailStudent(param) {
const recipientList = [];
this.selectedData.forEach(d => {
recipientList.push(`${d.student_uid}@${this.domain}`)
})
const uniqueRecipients = [...new Set(recipientList)];
const subject = ""; // empty subject line
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
},
getQGateStatusList() {
return [
this.$p.t('abgabetool/c4keinTerminVorhanden'),
@@ -375,7 +391,6 @@ export const AbgabetoolMitarbeiter = {
});
},
checkQualityGateStatus(projekt) {
// TODO: might refine the representation of these states and maybe refactor code a little
const qgate1Termine = []
const qgate2Termine = []
@@ -395,7 +410,7 @@ export const AbgabetoolMitarbeiter = {
// reuse luxon calculated diffMs (termin.datum in relation to today) from previous datestyle check
qgate1Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note)
const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) {
projekt.qgate1Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate1StatusRank = 5
@@ -417,7 +432,7 @@ export const AbgabetoolMitarbeiter = {
qgate2Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note)
const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) {
projekt.qgate2Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate2StatusRank = 5
@@ -677,12 +692,13 @@ export const AbgabetoolMitarbeiter = {
}
pa.abgabetermine.forEach(termin => {
termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note)
const noteOpt = this.allowedNotenOptions.find(opt => opt.note == termin.note)
if(noteOpt) termin.note = noteOpt
termin.file = []
// only set this if it has not been set yet and abgabetermin has a note (qgate)
if(!termin.noteBackend && termin.note) {
termin.noteBackend = termin.note
if(!termin.noteBackend && noteOpt) {
termin.noteBackend = noteOpt
}
// update 08-01-2026: everybody is allowed to do everything in client, critical checks happen at backend level
@@ -719,11 +735,6 @@ export const AbgabetoolMitarbeiter = {
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) {
@@ -828,6 +839,29 @@ export const AbgabetoolMitarbeiter = {
},
},
computed: {
emailItems() {
const menu = []
if(this.BETREUER_SAMMELMAIL_BUTTON_STUDENT){
menu.push({
label: this.$p.t('abgabetool/c4sendEmailStudierendev2', [this.uniqueStudentEmailCount]),
command: this.sammelMailStudent
})
}
return menu
},
uniqueStudentEmailCount() {
const emails = new Set();
this.selectedData.forEach(row => {
if (row.student_uid) {
emails.add(row.student_uid); // actually dont need domain for this
}
});
return emails.size;
},
getAllowedAbgabeTypeOptions() {
return this.abgabeTypeOptions.filter(opt => this.abgabetypenBetreuer.includes(opt.paabgabetyp_kurzbz))
}
@@ -840,6 +874,7 @@ export const AbgabetoolMitarbeiter = {
this.turnitin_link = res.data?.turnitin_link
this.old_abgabe_beurteilung_link = res.data?.old_abgabe_beurteilung_link
this.abgabetypenBetreuer = res.data?.abgabetypenBetreuer
this.BETREUER_SAMMELMAIL_BUTTON_STUDENT = res.data?.BETREUER_SAMMELMAIL_BUTTON_STUDENT
}).catch(e => {
this.loading = false
})
@@ -952,7 +987,11 @@ export const AbgabetoolMitarbeiter = {
</div>
</template>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit" :isFullscreen="detailIsFullscreen"></AbgabeDetail>
<AbgabeDetail
:projektarbeit="selectedProjektarbeit"
:isFullscreen="detailIsFullscreen"
@paUpdated="handlePaUpdated">
</AbgabeDetail>
</template>
</bs-modal>
@@ -988,7 +1027,17 @@ export const AbgabetoolMitarbeiter = {
<i class="fa fa-hourglass-end"></i>
{{ $p.t('abgabetool/showDeadlines') }}
</button>
<button
v-if="emailItems.length"
role="button"
@click="evt => $refs.menu.toggle(evt)"
class="btn btn-outline-secondary dropdown-toggle"
aria-haspopup="true"
>
<i class="fa fa-envelope"></i>
</button>
<tiered-menu ref="menu" :model="emailItems" popup :autoZIndex="false" />
</template>
</core-filter-cmpt>