diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index d976cea15..af598a345 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -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); diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index a39aa364c..011e75145 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -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() } diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 689bc6d02..d5caf97a6 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -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 = { diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 7d5888166..ee1d18942 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -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 '
' + '
' }, - mailFormatter(cell) { - const val = cell.getValue() - return '
' + - '
' - }, 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 = { @@ -988,7 +1027,17 @@ export const AbgabetoolMitarbeiter = { {{ $p.t('abgabetool/showDeadlines') }} - + + +