diff --git a/application/config/abgabe.php b/application/config/abgabe.php index 63c5699c3..6a91a4d12 100644 --- a/application/config/abgabe.php +++ b/application/config/abgabe.php @@ -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'; \ No newline at end of file +$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']; \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 1a31ed674..17d8f0811 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -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'); } diff --git a/application/controllers/api/frontend/v1/organisation/Studiensemester.php b/application/controllers/api/frontend/v1/organisation/Studiensemester.php index 06d5b93c9..3c6b72d2f 100644 --- a/application/controllers/api/frontend/v1/organisation/Studiensemester.php +++ b/application/controllers/api/frontend/v1/organisation/Studiensemester.php @@ -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)); diff --git a/application/controllers/jobs/AbgabetoolJob.php b/application/controllers/jobs/AbgabetoolJob.php index 301e747c1..c380f344b 100644 --- a/application/controllers/jobs/AbgabetoolJob.php +++ b/application/controllers/jobs/AbgabetoolJob.php @@ -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'); } } \ No newline at end of file diff --git a/application/models/education/Projektarbeit_model.php b/application/models/education/Projektarbeit_model.php index 0c00629df..361fd71b0 100644 --- a/application/models/education/Projektarbeit_model.php +++ b/application/models/education/Projektarbeit_model.php @@ -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) diff --git a/public/css/components/abgabetool/abgabe.css b/public/css/components/abgabetool/abgabe.css index e8b0cb9da..ea980dc29 100644 --- a/public/css/components/abgabetool/abgabe.css +++ b/public/css/components/abgabetool/abgabe.css @@ -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); diff --git a/public/js/api/factory/abgabe.js b/public/js/api/factory/abgabe.js index 21852349b..891fe02da 100644 --- a/public/js/api/factory/abgabe.js +++ b/public/js/api/factory/abgabe.js @@ -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) { diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 2acf9e719..9354ad184 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -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 = { @@ -708,7 +734,7 @@ export const AbgabeMitarbeiterDetail = { - +