load all studiensemester for assistenz; load paabgabetyp benotbar for all paabgaben; datediff calc luxon; new dateclass 'beurteilungrequired'; 2nd quality gate validation logic option; filter notenoptions as per config; filter abgabetypoptions as per config; upload_allowed checkbox for serientermine; serientermin modal layout rearranged; abgabetoolJob fixes; 23:59 in the descriptive col, not datepicker; zusatzdaten are required; activeIndex for accordion calulated on demand by method instead of reading a computed value;

This commit is contained in:
Johann Hoffmann
2025-11-27 16:53:50 +01:00
parent 8888b6991f
commit 095d5acbc5
13 changed files with 461 additions and 205 deletions
+5 -1
View File
@@ -11,4 +11,8 @@ $config['PAABGABE_EMAIL_JOB_INTERVAL'] = '1 day';
// used as APP_ROOT.URL_STUDENTS -> cis4
$config['URL_STUDENTS'] = 'cis.php/Cis/Abgabetool/Student';
// used as APP_ROOT.URL_MITARBEITER -> old cis
$config['URL_MITARBEITER'] = 'index.ci.php/Cis/Abgabetool/Mitarbeiter';
$config['URL_MITARBEITER'] = 'index.ci.php/Cis/Abgabetool/Mitarbeiter';
// lehre.tbl_paabgabetyp bezeichnung
$config['ALLOWED_ABGABETYPEN_BETREUER'] = ['Zwischenabgabe', 'Quality Gate 1', 'Quality Gate 2'];
$config['ALLOWED_NOTEN_ABGABETOOL'] = ['Bestanden', 'Nicht bestanden'];
@@ -82,11 +82,13 @@ class Abgabe extends FHCAPI_Controller
public function getConfig() {
$this->load->config('abgabe');
$old_abgabe_beurteilung_link =$this->config->item('old_abgabe_beurteilung_link');
$turnitin_link =$this->config->item('turnitin_link');
$turnitin_link = $this->config->item('turnitin_link');
$abgabetypenBetreuer = $this->config->item('ALLOWED_ABGABETYPEN_BETREUER');
$ret = array(
'old_abgabe_beurteilung_link' => $old_abgabe_beurteilung_link,
'turnitin_link' => $turnitin_link
'turnitin_link' => $turnitin_link,
'abgabetypenBetreuer' => $abgabetypenBetreuer
);
$this->terminateWithSuccess($ret);
@@ -402,7 +404,7 @@ class Abgabe extends FHCAPI_Controller
$ci3BootstrapFilePath = "index.ci.php";
}
$path = $this->_ci->config->item('URL_MITARBEITER');
$path = $this->config->item('URL_MITARBEITER');
$url = APP_ROOT.$path;
// $this->addMeta('betreuerArray', $resBetr->retval);
@@ -701,6 +703,7 @@ class Abgabe extends FHCAPI_Controller
$bezeichnung = $_POST['bezeichnung'];
$kurzbz = $_POST['kurzbz'];
$fixtermin = $_POST['fixtermin'];
$upload_allowed = $_POST['upload_allowed'];
if (!isset($projektarbeit_ids) || !is_array($projektarbeit_ids) || empty($projektarbeit_ids)
|| !isset($datum) || isEmptyString($datum)
@@ -736,6 +739,7 @@ class Abgabe extends FHCAPI_Controller
'fixtermin' => $fixtermin,
'datum' => $datum,
'kurzbz' => $kurzbz,
'upload_allowed' => $upload_allowed,
'insertvon' => getAuthUID(),
'insertamum' => date('Y-m-d H:i:s')
)
@@ -800,7 +804,10 @@ class Abgabe extends FHCAPI_Controller
$result = $this->NoteModel->getAllActive();
$noten = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($noten);
$allowed_noten_abgabetool = $this->config->item('ALLOWED_NOTEN_ABGABETOOL');
$this->terminateWithSuccess(array($noten, $allowed_noten_abgabetool));
}
private function sendQualGateNegativEmail($projektarbeit_id, $betreuer_person_id, $paabgabe) {
@@ -904,7 +911,7 @@ class Abgabe extends FHCAPI_Controller
$this->load->library('PermissionLib');
$stg_allowed = $this->permissionlib->getSTG_isEntitledFor('basis/abgabe_assistenz:rw');
if($stg_allowed == false) {
$this->terminateWithError($this->p->t('ui', 'keineBerechtigung'), 'general');
}
@@ -160,8 +160,8 @@ class Studiensemester extends FHCAPI_Controller
$this->StudiensemesterModel->addOrder("start", "DESC");
$result = $this->StudiensemesterModel->getAktOrNextSemester();
$aktuell = getData($result)[0];
$result = $this->StudiensemesterModel->getPreviousFrom($aktuell->studiensemester_kurzbz, 10);
$this->StudiensemesterModel->addSelect('*');
$result = $this->StudiensemesterModel->load();
$studiensemester = getData($result);
$this->terminateWithSuccess(array($studiensemester, $aktuell));
@@ -20,6 +20,7 @@ class AbgabetoolJob extends JOB_Controller
$this->_ci->load->model('education/Paabgabe_model', 'PaabgabeModel');
$this->_ci->load->model('crm/Student_model', 'StudentModel');
$this->_ci->load->config('abgabe');
$this->loadPhrases([
'abgabetool'
]);
@@ -32,7 +33,7 @@ class AbgabetoolJob extends JOB_Controller
// this job gathers all new or changed file uploads via field 'abgabedatum', enduploads still
// send an email directly after happening since they are kind of important
$this->_ci->logInfo('Start job queue scheduler FHC-Core->notifyBetreuerMail');
$this->_ci->logInfo('Start job FHC-Core->notifyBetreuerMail');
$interval = $this->_ci->config->item('PAABGABE_EMAIL_JOB_INTERVAL');
@@ -102,7 +103,7 @@ class AbgabetoolJob extends JOB_Controller
// send email with bundled info
sendSanchoMail(
'paabgabeUpdatesBetSM',
'PaabgabeUpdatesBetSM',
$body_fields,
$data->private_email,
$this->p->t('abgabetool', 'changedAbgabeterminev2')
@@ -112,14 +113,14 @@ class AbgabetoolJob extends JOB_Controller
}
$this->_ci->logInfo($count . " Emails erfolgreich versandt");
$this->_ci->logInfo('End job queue scheduler FHC-Core->notifyBetreuerMail');
$this->_ci->logInfo('End job FHC-Core->notifyBetreuerMail');
}
public function notifyStudentMail()
{
// send all new projektarbeit abgabe since the last job run to the related student
$this->_ci->logInfo('Start job queue scheduler FHC-Core->notifyStudentMail');
$this->_ci->logInfo('Start job FHC-Core->notifyStudentMail');
$interval = $this->_ci->config->item('PAABGABE_EMAIL_JOB_INTERVAL');
@@ -127,7 +128,7 @@ class AbgabetoolJob extends JOB_Controller
$retval = getData($result);
if(count($retval) == 0) {
$this->logInfo("Keine Emails an Studenten versandt");
$this->_ci->logInfo("Keine Emails an Studenten versandt");
return;
}
@@ -180,7 +181,7 @@ class AbgabetoolJob extends JOB_Controller
// send email with bundled info
sendSanchoMail(
'paabgabeUpdatesSammelmail',
'PaabgabeUpdatesSammelmail',
$body_fields,
$uid.'@'.DOMAIN,
$this->p->t('abgabetool', 'changedAbgabeterminev2')
@@ -191,6 +192,6 @@ class AbgabetoolJob extends JOB_Controller
}
$this->_ci->logInfo($count . " Emails erfolgreich versandt");
$this->_ci->logInfo('End job queue scheduler FHC-Core->notifyStudentMail');
$this->_ci->logInfo('End job FHC-Core->notifyStudentMail');
}
}
@@ -175,6 +175,7 @@ class Projektarbeit_model extends DB_Model
campus.tbl_paabgabe.beurteilungsnotiz,
campus.tbl_paabgabetyp.paabgabetyp_kurzbz,
campus.tbl_paabgabetyp.bezeichnung,
campus.tbl_paabgabetyp.benotbar,
campus.tbl_paabgabe.abgabedatum,
campus.tbl_paabgabe.insertvon
FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz)
@@ -54,6 +54,35 @@
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.beurteilungerforderlich-header {
background-color: var(--fhc-orange-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.beurteilungerforderlich-header:hover {
background-color: var(--fhc-orange-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .beurteilungerforderlich-header {
background-color: var(--fhc-orange-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .beurteilungerforderlich-header:hover {
background-color: var(--fhc-orange-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.verspaetet-header {
background-color: var(--fhc-pink-40);
+2 -2
View File
@@ -68,11 +68,11 @@ export default {
params: { paabgabe_id }
};
},
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids, fixtermin) {
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postSerientermin',
params: { datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids, fixtermin }
params: { datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin }
};
},
fetchDeadlines(person_id) {
@@ -19,6 +19,7 @@ export const AbgabeMitarbeiterDetail = {
},
inject: [
'abgabeTypeOptions',
'abgabetypenBetreuer',
'allowedNotenOptions',
'turnitin_link',
'old_abgabe_beurteilung_link',
@@ -40,6 +41,7 @@ export const AbgabeMitarbeiterDetail = {
},
data() {
return {
activeIndexArray: null,
showAutomagicModalPhrase: false,
eidAkzeptiert: false,
enduploadTermin: null,
@@ -77,6 +79,25 @@ export const AbgabeMitarbeiterDetail = {
}
},
methods: {
getActiveIndexTabArray(additional = []) {
// here we try to assume which abgabetermine are the most relevant to the current user
// lets try to take the termin with nearest date
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, ...additional]
},
getPlaceholderTermin(termin) {
return termin?.bezeichnung?.bezeichnung ?? this.$p.t('abgabetool/abgabetypPlaceholder')
},
@@ -114,12 +135,16 @@ export const AbgabeMitarbeiterDetail = {
// only insert new abgabe if we actually created a new one, not when saving/editing existing
if(!existingTerminRes){
this.projektarbeit.abgabetermine.push(newTerminRes)
this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum))
} else {
const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note)
existingTerminRes.note = noteOptExisting
}
this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum))
const index = this.projektarbeit.abgabetermine.findIndex(t => termin.paabgabe_id == t.paabgabe_id)
this.activeIndexArray = this.getActiveIndexTabArray([index])
// negative abgabe -> automagically open new termin modal
// really bad feature imo that will be annoying to deal with
@@ -203,49 +228,9 @@ export const AbgabeMitarbeiterDetail = {
this.$refs.modalContainerZusatzdaten.hide()
},
async validateZusatzdaten() {
// 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.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
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.$capitalize(this.$p.t('abgabetool/warningShortAbstractEn')),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
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.$capitalize(this.$p.t('abgabetool/warningShortSchlagwoerter')),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
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.$capitalize(this.$p.t('abgabetool/warningShortSchlagwoerterEn')),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
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.$capitalize(this.$p.t('abgabetool/warningSmallSeitenanzahl')),
// just ask once
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/confirmEnduploadSpeichern'),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
@@ -295,27 +280,61 @@ export const AbgabeMitarbeiterDetail = {
window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url)
// this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid))
},
dateDiffInDays(datum, today){
const oneDayMs = 1000 * 60 * 60 * 24
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
convertDateToIsoString(date) {
// 1. Check if it is a Date object AND if the date value is valid (not 'Invalid Date')
if (param instanceof Date && !isNaN(param.getTime())) {
const year = param.getFullYear();
// getMonth() is 0-indexed, so we add 1.
const month = param.getMonth() + 1;
const day = param.getDate();
// Helper to pad single-digit numbers with a leading zero
const pad = (num) => String(num).padStart(2, '0');
// Return the formatted string: YYYY-MM-DD
return `${year}-${pad(month)}-${pad(day)}`;
}
// If it's not a valid Date, return the original parameter
return param;
},
dateDiffInDays(datumParam){
let datum = datumParam
if(datumParam instanceof Date && !isNaN(datum.getTime()))
{
const year = datumParam.getFullYear();
const month = datumParam.getMonth() + 1; // getMonth() is 0-indexed
const day = datumParam.getDate();
const pad = (num) => String(num).padStart(2, '0');
datum = `${year}-${pad(month)}-${pad(day)}`
}
const dateToday = luxon.DateTime.now().startOf('day');
const dateDatum = luxon.DateTime.fromISO(datum).startOf('day');
const duration = dateDatum.diff(dateToday, 'days');
return duration.values.days;
},
getDateStyleClass(termin) {
const datum = new Date(termin.datum)
const abgabedatum = new Date(termin.abgabedatum)
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
if (termin.abgabedatum === null) {
termin.diffindays = this.dateDiffInDays(termin.datum)
if(today > datum && termin.benotbar && !termin.note) return 'beurteilungerforderlich'
if (termin.abgabedatum === null && termin.upload_allowed) {
if(datum < today) {
return 'verpasst'
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
return 'abzugeben'
return 'verpasst' // needs upload, missed it and has not submitted anything
} else if (datum > today && termin.diffindays <= 12) {
return 'abzugeben' // needs to upload soon
} else {
return 'standard'
}
} else if(abgabedatum > datum) {
return 'verspaetet'
return 'standard' // upload in distant future
}
}
else if(abgabedatum > datum) {
return 'verspaetet' // needs upload, missed it and has submitted smth late
} else {
return 'abgegeben'
return 'abgegeben' // nothing else to do for that termin
}
},
openBeurteilungLink(link) {
@@ -343,11 +362,19 @@ export const AbgabeMitarbeiterDetail = {
window.open(link, '_blank')
},
openBenotung() {
const path = this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter'
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/' + path
window.open(link, '_blank')
// old link check ?
if(this.getSemesterBenotbar && this.projektarbeit?.abgabetermine.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null)) {
// TODO: shouldnt be hardcoded here, at least config in abgabetool -> ideally event sourced from projektarbeitsbeurteilung
const path = this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter'
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/' + path
window.open(link, '_blank')
} else {
window.open(this.old_abgabe_beurteilung_link, '_blank')
}
},
formatDate(dateParam, showTime = true) {
formatDate(dateParam) {
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
@@ -356,13 +383,12 @@ export const AbgabeMitarbeiterDetail = {
const day = padZero(date.getDate());
const year = date.getFullYear();
// abgabedatum should SHOW abgabezeit which should always be last minute of the day
return `${day}.${month}.${year}` + (showTime ? ' 23:59' : '');
return `${day}.${month}.${year}`
},
getAccTabHeaderForTermin(termin) {
let tabTitle = ''
const datumFormatted = this.formatDate(termin.datum, false)
const datumFormatted = this.formatDate(termin.datum)
tabTitle += termin.bezeichnung?.bezeichnung + ' ' + datumFormatted
return tabTitle
@@ -432,6 +458,16 @@ export const AbgabeMitarbeiterDetail = {
},
computed: {
allowedToSaveZusatzdaten() {
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
},
getAllowedAbgabeTypeOptions() {
if(this.assistenzMode) {
return this.abgabeTypeOptions
} else {
return this.abgabeTypeOptions.filter(opt => this.abgabetypenBetreuer.includes(opt.bezeichnung))
}
},
getMessagePtStyle() {
// adjust outer spacing and internal padding to appear similar to doenload button in size
return {
@@ -447,24 +483,6 @@ export const AbgabeMitarbeiterDetail = {
}
}
},
getActiveIndexTabArray() {
// here we try to assume which abgabetermine are the most relevant to the current user
// lets try to take the termin with nearest date
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')
},
@@ -518,6 +536,12 @@ export const AbgabeMitarbeiterDetail = {
class: "custom-tooltip"
}
},
getTooltipBeurteilungerforderlich() {
return {
value: this.$p.t('abgabetool/c4tooltipBeurteilungerfolderlich'),
class: "custom-tooltip"
}
},
getTooltipAbgegeben() {
return {
value: this.$p.t('abgabetool/c4tooltipAbgegeben'),
@@ -573,6 +597,8 @@ export const AbgabeMitarbeiterDetail = {
'projektarbeit'(newVal) {
// set invertedFixtermin field for UI/UX purposes -> avoid double negation in text
this.activeIndexArray = this.getActiveIndexTabArray()
newVal?.abgabetermine?.forEach(termin => termin.invertedFixtermin = !termin.fixtermin)
// default select german if projektarbeit sprache was null
@@ -639,7 +665,7 @@ export const AbgabeMitarbeiterDetail = {
<Dropdown
:style="{'width': '100%'}"
v-model="newTermin.bezeichnung"
:options="abgabeTypeOptions"
:options="getAllowedAbgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp"
scrollHeight="300px">
</Dropdown>
@@ -708,7 +734,7 @@ export const AbgabeMitarbeiterDetail = {
</button>
</div>
</div>
<Accordion :multiple="true" :activeIndex="getActiveIndexTabArray">
<Accordion :multiple="true" :activeIndex="activeIndexArray">
<template v-for="termin in this.projektarbeit?.abgabetermine">
<AccordionTab :headerClass="getDateStyleClass(termin) + '-header'">
<template #header>
@@ -719,12 +745,13 @@ export const AbgabeMitarbeiterDetail = {
<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>
<i v-else-if="getDateStyleClass(termin) == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i>
</div>
<div class="col-auto text-start" style="min-width: max(150px, 20%); 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>
<span>{{ formatDate(termin.datum) }}</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>
@@ -746,7 +773,10 @@ export const AbgabeMitarbeiterDetail = {
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
<div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
<div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div>
</div>
<div class="col-12 col-md-9">
<VueDatePicker
v-model="termin.datum"
@@ -768,7 +798,7 @@ export const AbgabeMitarbeiterDetail = {
:placeholder="getPlaceholderTermin(termin)"
v-model="termin.bezeichnung"
@change="handleChangeAbgabetyp(termin)"
:options="abgabeTypeOptions"
:options="getAllowedAbgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp"
:optionDisabled="getOptionDisabled">
</Dropdown>
@@ -778,6 +808,7 @@ export const AbgabeMitarbeiterDetail = {
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4upload_allowed') )}}</div>
<div class="col-12 col-md-9">
<Checkbox
:disabled="!termin.allowedToSave"
v-model="termin.upload_allowed"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
@@ -968,7 +999,8 @@ export const AbgabeMitarbeiterDetail = {
</template>
<template v-slot:footer>
<button class="btn btn-primary" @click="saveZusatzdaten">{{ $capitalize( $p.t('abgabetool/c4save') )}}</button>
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
<button class="btn btn-primary" :disabled="!allowedToSaveZusatzdaten" @click="saveZusatzdaten">{{ $capitalize( $p.t('abgabetool/c4save') )}}</button>
</template>
</bs-modal>
@@ -152,7 +152,7 @@ export const AbgabeStudentDetail = {
window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url)
// this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid))
},
formatDate(dateParam, showTime = true) {
formatDate(dateParam) {
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
@@ -161,7 +161,7 @@ export const AbgabeStudentDetail = {
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}` + (showTime ? ' 23:59' : '');
return `${day}.${month}.${year}`
},
async upload(termin) {
@@ -226,7 +226,7 @@ export const AbgabeStudentDetail = {
getAccTabHeaderForTermin(termin) {
let tabTitle = ''
const datumFormatted = this.formatDate(termin.datum, false)
const datumFormatted = this.formatDate(termin.datum)
tabTitle += termin.bezeichnung + ' ' + datumFormatted
return tabTitle
@@ -282,8 +282,11 @@ export const AbgabeStudentDetail = {
getEid() {
return this.$capitalize(this.$p.t('abgabetool/c4eidesstattlicheErklaerung'))
},
allowedToSaveZusatzdaten() {
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
},
getAllowedToSendEndupload() {
return !this.eidAkzeptiert
return this.eidAkzeptiert && this.allowedToSaveZusatzdaten
},
qualityGateTerminAvailable() {
let qgatefound = false
@@ -384,7 +387,7 @@ export const AbgabeStudentDetail = {
<span>{{ termin?.bezeichnung }}</span>
</div>
<div class="col-auto text-start p-0" style="min-width: max(80px, 15%); transform: translateX(-30px)">
<span>{{ formatDate(termin.datum, false) }}</span>
<span>{{ formatDate(termin.datum) }}</span>
</div>
<div class="col-auto" style="transform: translateX(-30px); min-width: 42px;">
<i v-if="termin?.fixtermin" v-tooltip.right="getTooltipFixtermin" class="fa-solid fa-lock"></i>
@@ -431,7 +434,10 @@ export const AbgabeStudentDetail = {
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
<div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
<div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div>
</div>
<div class="col-12 col-md-9">
<VueDatePicker
v-model="termin.datum"
@@ -653,7 +659,8 @@ export const AbgabeStudentDetail = {
</template>
<template v-slot:footer>
<button class="btn btn-primary" :disabled="getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
<button class="btn btn-primary" :disabled="!getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
</template>
</bs-modal>
@@ -409,6 +409,7 @@ export const AbgabetoolAssistenz = {
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
this.serienTermin.bezeichnung.bezeichnung,
this.serienTermin.kurzbz,
this.serienTermin.upload_allowed,
pids,
this.serienTermin.fixtermin
)).then(res => {
@@ -524,19 +525,27 @@ export const AbgabetoolAssistenz = {
this.$refs.modalContainerAbgabeDetail.show()
},
dateDiffInDays(datum, today){
const oneDayMs = 1000 * 60 * 60 * 24
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
dateDiffInDays(datum){
const dateToday = luxon.DateTime.now().startOf('day');
const dateDatum = luxon.DateTime.fromISO(datum).startOf('day');
const duration = dateDatum.diff(dateToday, 'days');
return duration.values.days;
},
getDateStyleClass(termin) {
const datum = new Date(termin.datum)
const abgabedatum = new Date(termin.abgabedatum)
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
if (termin.abgabedatum === null) {
termin.diffindays = this.dateDiffInDays(termin.datum)
// seperate status if termin is in the past, it needs a note but doesnt have one yet
if(today > datum && termin.benotbar && !termin.note) return 'beurteilungerforderlich'
else if (termin.abgabedatum === null) {
if(datum < today) {
return 'verpasst'
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
return termin.upload_allowed ? 'verpasst' : 'abgegeben'
} else if (datum > today && termin.diffindays <= 12) {
return 'abzugeben'
} else {
return 'standard'
@@ -754,10 +763,10 @@ export const AbgabetoolAssistenz = {
this.$api.call(ApiStudiensemester.getAllStudiensemesterAndAktOrNext()).then((res) => {
this.allSem = res.data[0]
this.curSem = res.data[1]
const all = {studiensemester_kurzbz: 'Alle'}
this.curSem = all // res.data[1]
// TODO: maybe filter only for available semester from projektarbeiten dataset
this.studiensemesterOptions = [{studiensemester_kurzbz: 'Alle'}, this.curSem, ...this.allSem]
this.studiensemesterOptions = [all, ...this.allSem]
}).catch(e => {
this.loading = false
@@ -767,12 +776,13 @@ export const AbgabetoolAssistenz = {
// 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'
)
if(res.meta.status == 'success') {
this.notenOptions = res.data[0]
this.allowedNotenOptions = this.notenOptions.filter(
opt => res.data[1].includes(opt.bezeichnung)
)
}
// allowedNotenOptions apply to quality gates abgabetermine
// this selection is about graded projektarbeiten, so take different options here
@@ -819,43 +829,51 @@ export const AbgabetoolAssistenz = {
</div>
</template>
<template v-slot:default>
<div class="row">
<div class="col-1 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4fixterminv4')}}
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
{{$capitalize($p.t('abgabetool/c4zieldatum'))}}
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabetyp')}}
</div>
<div class="col-5 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabekurzbz')}}
</div>
</div>
<div class="row">
<div class="col-1 d-flex justify-content-center align-items-center">
<Checkbox
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4fixterminv4') )}}</div>
<div class="col-12 col-md-9">
<Checkbox
v-model="serienTermin.fixtermin"
:binary="true"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
<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="row mt-2">
<div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
<div class="col-12 col-md-9">
<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="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4upload_allowed') )}}</div>
<div class="col-12 col-md-9">
<Checkbox
v-model="serienTermin.upload_allowed"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
<div class="col-12 col-md-9">
<Dropdown
:style="{'width': '100%'}"
v-model="serienTermin.bezeichnung"
@@ -863,7 +881,11 @@ export const AbgabetoolAssistenz = {
:optionLabel="getOptionLabelAbgabetyp">
</Dropdown>
</div>
<div class="col-5 d-flex justify-content-center align-items-center">
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" class="w-100"></Textarea>
</div>
</div>
@@ -11,6 +11,7 @@ export const AbgabetoolMitarbeiter = {
BsModal,
CoreFilterCmpt,
AbgabeDetail,
Checkbox: primevue.checkbox,
Dropdown: primevue.dropdown,
Textarea: primevue.textarea,
VueDatePicker,
@@ -19,6 +20,7 @@ export const AbgabetoolMitarbeiter = {
provide() {
return {
abgabeTypeOptions: Vue.computed(() => this.abgabeTypeOptions),
abgabetypenBetreuer: Vue.computed(() => this.abgabetypenBetreuer),
allowedNotenOptions: Vue.computed(() => this.allowedNotenOptions),
turnitin_link: Vue.computed(() => this.turnitin_link),
old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link)
@@ -36,6 +38,7 @@ export const AbgabetoolMitarbeiter = {
},
data() {
return {
abgabetypenBetreuer: null,
detailIsFullscreen: false,
phrasenPromise: null,
phrasenResolved: false,
@@ -52,7 +55,8 @@ export const AbgabetoolMitarbeiter = {
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
},
kurzbz: ''
kurzbz: '',
upload_allowed: false
}),
showAll: false,
tabulatorUuid: Vue.ref(0),
@@ -179,6 +183,7 @@ export const AbgabetoolMitarbeiter = {
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
this.serienTermin.bezeichnung.bezeichnung,
this.serienTermin.kurzbz,
this.serienTermin.upload_allowed,
this.selectedData?.map(projekt => projekt.projektarbeit_id),
false
)).then(res => {
@@ -346,7 +351,9 @@ export const AbgabetoolMitarbeiter = {
},
computed: {
getAllowedAbgabeTypeOptions() {
return this.abgabeTypeOptions.filter(opt => this.abgabetypenBetreuer.includes(opt.bezeichnung))
}
},
created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
@@ -355,6 +362,7 @@ export const AbgabetoolMitarbeiter = {
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
this.abgabetypenBetreuer = res.data?.abgabetypenBetreuer
}).catch(e => {
console.log(e)
this.loading = false
@@ -363,12 +371,14 @@ export const AbgabetoolMitarbeiter = {
// 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'
)
if(res.meta.status == 'success') {
this.notenOptions = res.data[0]
this.allowedNotenOptions = this.notenOptions.filter(
opt => res.data[1].includes(opt.bezeichnung)
)
}
}).catch(e => {
this.loading = false
})
@@ -395,40 +405,53 @@ export const AbgabetoolMitarbeiter = {
</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 class="row mt-2">
<div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div>
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabetyp')}}
</div>
<div class="col-6 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabekurzbz')}}
<div class="col-12 col-md-9">
<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="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 class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4upload_allowed') )}}</div>
<div class="col-12 col-md-9">
<Checkbox
v-model="serienTermin.upload_allowed"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
<div class="col-12 col-md-9"
v-if="abgabetypenBetreuer && abgabeTypeOptions"
>
<Dropdown
:style="{'width': '100%'}"
v-model="serienTermin.bezeichnung"
:options="abgabeTypeOptions"
:options="getAllowedAbgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp">
</Dropdown>
</div>
<div class="col-6 d-flex justify-content-center align-items-center">
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" class="w-100"></Textarea>
</div>
</div>
@@ -47,20 +47,26 @@ export const AbgabetoolStudent = {
};
},
methods: {
dateDiffInDays(datum, today){
const oneDayMs = 1000 * 60 * 60 * 24
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
dateDiffInDays(datum){
const dateToday = luxon.DateTime.now().startOf('day');
const dateDatum = luxon.DateTime.fromISO(datum).startOf('day');
const duration = dateDatum.diff(dateToday, 'days');
return duration.values.days;
},
getDateStyleClass(termin) {
const datum = new Date(termin.datum)
const abgabedatum = new Date(termin.abgabedatum)
// avoid renaming these statuses as their names are used as css keys
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
if (termin.abgabedatum === null) {
termin.diffindays = this.dateDiffInDays(termin.datum)
if(today > datum && termin.benotbar && !termin.note) return 'beurteilungerforderlich'
else if (termin.abgabedatum === null) {
if(datum < today) {
return 'verpasst'
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
return termin.upload_allowed ? 'verpasst' : 'abgegeben'
} else if (datum > today && termin.diffindays <= 12) {
return 'abzugeben'
} else {
return 'standard'
@@ -71,7 +77,7 @@ export const AbgabetoolStudent = {
return 'abgegeben'
}
},
checkQualityGates(termine) {
checkQualityGatesStrict(termine) {
let qgate1Passed = false
let qgate2Passed = false
@@ -88,6 +94,40 @@ export const AbgabetoolStudent = {
return qgate1Passed && qgate2Passed
},
checkQualityGatesOptional(termine) {
const qgate1found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate1')
const qgate2found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate2')
let qgate1positiv = true
if(qgate1found) {
qgate1positiv = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if (t.paabgabetyp_kurzbz == 'qualgate1') {
qgate1positiv = true
}
}
})
}
let qgate2positiv = true
if(qgate2found) {
qgate2positiv = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if (t.paabgabetyp_kurzbz == 'qualgate2') {
qgate2positiv = true
}
}
})
}
return qgate1positiv && qgate2positiv
},
isPastDate(date) {
return new Date(date) < new Date(Date.now())
},
@@ -101,11 +141,15 @@ export const AbgabetoolStudent = {
termin.allowedToUpload = false
if(termin.paabgabetyp_kurzbz == 'end') {
// production logic
termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGates(pa.abgabetermine)
// production logic when qgates are required
// termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine)
// larifari we want qgates but they are optional fhtw mode
termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesOptional(pa.abgabetermine)
// development purposes
// termin.allowedToUpload = this.checkQualityGates(pa.abgabetermine)
// termin.allowedToUpload = this.checkQualityGatesStrict(pa.abgabetermine)
// termin.allowedToUpload = true
} else if(termin.fixtermin) {
@@ -257,7 +301,13 @@ export const AbgabetoolStudent = {
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
if(res.meta.status == 'success') {
this.notenOptions = res.data[0]
this.allowedNotenOptions = this.notenOptions.filter(
opt => res.data[1].includes(opt.bezeichnung)
)
}
}).finally(() => {
this.loading = false
})
+80
View File
@@ -42538,6 +42538,26 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4abgabeuntil2359',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Abgabe bis 23:59",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Submission until 23:59",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
@@ -43558,6 +43578,26 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4tooltipBeurteilungerforderlich',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Beurteilung erforderlich",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Grading necessary',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
@@ -44114,6 +44154,46 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'confirmEnduploadSpeichern',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Möchten Sie den Endupload durchführen?",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Do you want to complete the final upload?',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4zusatzdatenausfuellen',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Alle Zusatzdatenfelder benötigen Eingabewerte!",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'All additional data fields are required!',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',