diff --git a/application/config/routes.php b/application/config/routes.php index de8e6e098..d7393c13c 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -63,6 +63,7 @@ $route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1'; $route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1'; $route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1'; +$route['Cis/Benotungstool/.*'] = 'Cis/Benotungstool/index/$1'; $route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1'; // Studierendenverwaltung List Routes diff --git a/application/controllers/Cis/Benotungstool.php b/application/controllers/Cis/Benotungstool.php new file mode 100644 index 000000000..3ca3c62ef --- /dev/null +++ b/application/controllers/Cis/Benotungstool.php @@ -0,0 +1,51 @@ + self::PERM_LOGGED + ]); + + $this->_ci =& get_instance(); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Public methods + + /** + * @return void + */ + public function index() + { + + + // TODO: check if related CIS config is also loaded when being routed in Cis4 by vuerouter + // TODO: check if new benotungstool should be configurable the exact same way? + + + $viewData = array( + 'uid'=>getAuthUID(), + 'CIS_GESAMTNOTE_UEBERSCHREIBEN' => CIS_GESAMTNOTE_UEBERSCHREIBEN, + 'CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF' => CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF, + 'CIS_GESAMTNOTE_PRUEFUNG_TERMIN3' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN3, + 'CIS_GESAMTNOTE_PRUEFUNG_TERMIN2' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN2, + 'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE, + 'CIS_GESAMTNOTE_PUNKTE' => CIS_GESAMTNOTE_PUNKTE, + 'CIS_GESAMTNOTE_GEWICHTUNG' => CIS_GESAMTNOTE_GEWICHTUNG, + 'CIS_ANWESENHEITSLISTE_NOTENLISTE_ANZEIGEN' => CIS_ANWESENHEITSLISTE_NOTENLISTE_ANZEIGEN + ); + + $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Benotungstool']); + } + +} \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/Lehre.php b/application/controllers/api/frontend/v1/Lehre.php index d5d0282bd..da27cbdea 100644 --- a/application/controllers/api/frontend/v1/Lehre.php +++ b/application/controllers/api/frontend/v1/Lehre.php @@ -47,7 +47,10 @@ class Lehre extends FHCAPI_Controller 'postProjektarbeitAbgabe' => self::PERM_LOGGED, 'deleteProjektarbeitAbgabe' => self::PERM_LOGGED, 'postSerientermin' => self::PERM_LOGGED, - 'fetchDeadlines' => self::PERM_LOGGED // TODO: mitarbeiter recht prüfen + 'fetchDeadlines' => self::PERM_LOGGED, // TODO: mitarbeiter recht prüfen + 'getLvViewData' => self::PERM_LOGGED, + 'getZugewieseneLv' => self::PERM_LOGGED, + 'getLeForLv' => self::PERM_LOGGED ]); $this->load->library('PhrasesLib'); @@ -133,7 +136,7 @@ class Lehre extends FHCAPI_Controller $projektarbeit_id = $this->input->get("projektarbeit_id",TRUE); // TODO: error messages - + if (!isset($projektarbeit_id) || isEmptyString($projektarbeit_id)) $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); @@ -145,12 +148,12 @@ class Lehre extends FHCAPI_Controller $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); $paIsCurrent = $projektarbeit_obj->projektarbeitIsCurrent($projektarbeit_id); - + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); $ret = $this->ProjektarbeitModel->getProjektarbeitAbgabetermine($projektarbeit_id); - + // TODO: fetch zweitbetreuer - + $this->terminateWithSuccess(array($ret, $paIsCurrent)); } @@ -542,10 +545,10 @@ class Lehre extends FHCAPI_Controller $this->terminateWithSuccess($result); } } - + public function deleteProjektarbeitAbgabe() { $paabgabe_id = $_POST['paabgabe_id']; - + if (!isset($paabgabe_id) || isEmptyString($paabgabe_id)) $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); @@ -553,8 +556,8 @@ class Lehre extends FHCAPI_Controller $result = $this->PaabgabeModel->load($paabgabe_id); $result = $this->getDataOrTerminateWithError($result); - - if(count($result) == 0) + + if(count($result) == 0) $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); // TODO: berechtigung? @@ -584,26 +587,26 @@ class Lehre extends FHCAPI_Controller || !isset($bezeichnung) || isEmptyString($bezeichnung) || !isset($paabgabetyp_kurzbz) || isEmptyString($paabgabetyp_kurzbz)) $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); - + // old script checks if there already are tbl_paabgabe entries with exact date, type & kurzbz // for each termin - good to check that in principle but should not matter in this place. if necessary // duplicate abgabetermine can be easily deleted manually, also via cronjob@night. - + // since this entry includes the kurzbz string match, it should have only ever mattered when there were // multiple users entering the exact same set of (date, type, kurzbz) - which is a much more narrow case than the // general "saveMultiple" function should handle - + // old script afterwards again queries if user is not the zweitbetreuer of any id - this is blocked in the ui // and should never unintentionally happen - + // TODO: check berechtigung &/|| zuordnung? - + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - + $res = []; foreach ($projektarbeit_ids as $projektarbeit_id) { - + $result = $this->PaabgabeModel->insert( array( 'projektarbeit_id' => $projektarbeit_id, @@ -615,9 +618,9 @@ class Lehre extends FHCAPI_Controller 'insertamum' => date('Y-m-d H:i:s') ) ); - + $data = $this->getDataOrTerminateWithError($result); - + // $res[] = $data; // send mail to student @@ -625,15 +628,15 @@ class Lehre extends FHCAPI_Controller $data = $this->getDataOrTerminateWithError($result); // $this->addMeta('emaildata'.$projektarbeit_id, $data); - + $datetime = new DateTime($datum); $dateEmailFormatted = $datetime->format('d.m.Y'); - + $anredeFillString = $data[0]->anrede=="Herr"?"r":""; - + $fullFormattedNameString = trim($data[0]->titelpre." ".$data[0]->vorname." ".$data[0]->nachname." ".$data[0]->titelpost); $res[] = $fullFormattedNameString; - + // Prepare mail content $body_fields = array( 'anrede' => $data[0]->anrede, @@ -645,7 +648,7 @@ class Lehre extends FHCAPI_Controller ); $email = $data[0]->uid."@".DOMAIN; - + sendSanchoMail( 'neuerAbgabetermin', $body_fields, @@ -653,18 +656,17 @@ class Lehre extends FHCAPI_Controller $this->p->t('abgabetool', 'neuerTerminBachelorMasterbetreuung') ); } - + $this->terminateWithSuccess($res); - + } - + public function fetchDeadlines() { $person_id = $_POST['person_id']; - + if (!isset($person_id) || isEmptyString($person_id)) $person_id = getAuthPersonId(); - if($person_id !== getAuthPersonId()) { $this->load->library('PermissionLib'); $isAdmin = $this->permissionlib->isBerechtigt('admin'); @@ -677,5 +679,47 @@ class Lehre extends FHCAPI_Controller $this->terminateWithSuccess($data); } + + /** + * fetches all assigned lehrveranstaltungen of a mitarbeiter for a given semester + * @param mixed $uid + * @param mixed $sem_kurzbz + * @return void + */ + public function getZugewieseneLv() { + $uid = $this->input->get("uid",TRUE); + $sem_kurzbz = $this->input->get("sem_kurzbz",TRUE); + + // TODO: error messages + + if(!isset($sem_kurzbz) || isEmptyString($sem_kurzbz)) + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + + if (!isset($uid) || isEmptyString($uid)) + $uid = getAuthUID(); + + // querying other ma_uids data requires admin permission + if($uid !== getAuthUID()) { + $this->load->library('PermissionLib'); + $isAdmin = $this->permissionlib->isBerechtigt('admin'); + if(!$isAdmin) $this->terminateWithError($this->p->t('ui', 'keineBerechtigung'), 'general'); + } + + $this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); + $result = $this->LehrveranstaltungModel->getLvForLektorInSemester($sem_kurzbz, $uid); + $data = $this->getDataOrTerminateWithError($result); + $this->terminateWithSuccess($data); + } + + public function getLeForLv() { + $lv_id = $this->input->get("lv_id",TRUE); + $sem_kurzbz = $this->input->get("sem_kurzbz",TRUE); + + $this->load->model('education/Lehreinheit_model', 'LehreinheitModel'); + +// $this->terminateWithSuccess($this->LehreinheitModel->getLesForLv($lv_id, $sem_kurzbz)); + $this->terminateWithSuccess($this->LehreinheitModel->getAllLehreinheitenForLvaAndMaUid($lv_id, getAuthUID(), $sem_kurzbz)); + } + } diff --git a/application/controllers/api/frontend/v1/Noten.php b/application/controllers/api/frontend/v1/Noten.php new file mode 100644 index 000000000..70788d54d --- /dev/null +++ b/application/controllers/api/frontend/v1/Noten.php @@ -0,0 +1,929 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +use CI3_Events as Events; + +class Noten extends FHCAPI_Controller +{ + + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'getStudentenNoten' => array('lehre/benotungstool:rw'), + 'getNoten' => array('lehre/benotungstool:rw'), + 'saveStudentenNoten' => array('lehre/benotungstool:rw'), + 'getNotenvorschlagStudent' => array('lehre/benotungstool:rw'), + 'saveNotenvorschlag' => array('lehre/benotungstool:rw'), + 'saveStudentPruefung' => array('lehre/benotungstool:rw'), + 'createPruefungen' => array('lehre/benotungstool:rw'), + 'saveNotenvorschlagBulk' => array('lehre/benotungstool:rw'), + 'savePruefungenBulk' => array('lehre/benotungstool:rw') + ]); + + $this->load->library('AuthLib', null, 'AuthLib'); + $this->load->library('PhrasesLib'); + + // Loads phrases system + $this->loadPhrases([ + 'global', + 'person', + 'benotungstool', + 'lehre', + 'ui' + ]); + require_once(FHCPATH . 'include/mobilitaet.class.php'); + + $this->load->model('education/LePruefung_model', 'LePruefungModel'); + $this->load->model('education/Lvgesamtnote_model', 'LvgesamtnoteModel'); + $this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); + $this->load->model('person/Person_model', 'PersonModel'); + $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); + $this->load->model('crm/Student_model', 'StudentModel'); + + $this->load->helper('hlp_sancho_helper'); + + } + + + /** + * GET METHOD + * expects 'lv_id', 'sem_kurzbz' + * returns List of all Students of given lehrveranstaltung and semester and fetches their grades. + * Loads LvGesamtnote aswell as Teilnoten from externalSources via getExternalGrades Event. + * Calculates the Notenvorschlag for every student based on averaging their Teilnoten. + * Finally also fetches all Prüfungen for every student which are linked to lva and semester. + */ + public function getStudentenNoten() { + $lv_id = $this->input->get("lv_id",TRUE); + $sem_kurzbz = $this->input->get("sem_kurzbz",TRUE); + + if (!isset($lv_id) || isEmptyString($lv_id) + || !isset($sem_kurzbz) || isEmptyString($sem_kurzbz)) + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + + // get studenten for lva & sem with zeugnisnote if available + $studenten = $this->LehrveranstaltungModel->getStudentsByLv($sem_kurzbz, $lv_id); + $studentenData = $this->getDataOrTerminateWithError($studenten); + + $func = function ($value) { + return $value->uid; + }; + + $grades = array(); + $student_uids = array_map($func, $studentenData); + + foreach($student_uids as $uid) { + $grades[$uid]['grades'] = []; + + $res = $this->StudentModel->load([$uid]); + if(!isError($res) && hasData($res)) $student = getData($res)[0]; + + $prestudent_id = $student->prestudent_id; + + + // TODO: last class to get rid of but this one is complicated + $mobility = new mobilitaet(); + $mobility->loadPrestudent($prestudent_id); + $output = $mobility->result; + $eintrag = ''; + foreach ($output as $k) + { + if(($k->mobilitaetstyp_kurzbz == 'GS') && ($k->studiensemester_kurzbz == $sem_kurzbz)) + $eintrag = ' (d.d.)'; + } + $grades[$uid]['mobility'] = $eintrag; + + $result = $this->LvgesamtnoteModel->getLvGesamtNoten($lv_id, $uid, $sem_kurzbz); + + if(!isError($result) && hasData($result)) { + $lvgesamtnote = getData($result)[0]; + $grades[$uid]['note_lv'] = $lvgesamtnote->note; + $grades[$uid]['freigabedatum'] = $lvgesamtnote->freigabedatum; + $grades[$uid]['benotungsdatum'] = $lvgesamtnote->benotungsdatum; + $grades[$uid]['punkte_lv'] = $lvgesamtnote->punkte; + } else { + $grades[$uid]['note_lv'] = null; + $grades[$uid]['freigabedatum'] = null; + $grades[$uid]['benotungsdatum'] = null; + $grades[$uid]['punkte_lv'] = null; + } + } + + // send $grades reference to moodle addon + Events::trigger( + 'getExternalGrades', + function & () use (&$grades) + { + return $grades; + }, + [ + 'lvid' => $lv_id, + 'stsem' => $sem_kurzbz + ] + ); + + // calculate notenvorschläge from teilnoten + foreach($studentenData as $student) { + $g = $grades[$student->uid]['grades']; + $note_lv = $grades[$student->uid]['note_lv']; + + // overwrite any calculation with lv note once available + if(!is_null($note_lv)) { + $student->note_vorschlag = $note_lv; + } else if(count($g) > 0) { + + $notensumme = 0; + $notensumme_gewichtet = 0; + $gewichtsumme = 0; + $punktesumme = 0; + $punktesumme_gewichtet = 0; + $anzahlnoten = 0; + foreach($g as $teilnote) { + if (is_numeric($teilnote['grade']) || (is_null($teilnote['grade']) && is_numeric($teilnote['points']))) + { + $notensumme += $teilnote['grade']; + $punktesumme += $teilnote['points']; + $notensumme_gewichtet += $teilnote['grade'] * $teilnote['weight']; + $punktesumme_gewichtet += $teilnote['points'] * $teilnote['weight']; + $gewichtsumme += $teilnote['weight']; + $anzahlnoten += 1; + } + } + + + // TODO: develop the punkte feature with models + // calculate grades points from notenschlüssel + if (CIS_GESAMTNOTE_PUNKTE) + { + if (defined('CIS_GESAMTNOTE_GEWICHTUNG') && CIS_GESAMTNOTE_GEWICHTUNG) + { + // Lehreinheitsgewichtung + $punkte_vorschlag = round($punktesumme_gewichtet / $gewichtsumme, 2); + $notenschluessel = new notenschluessel(); + $note_vorschlag = $notenschluessel->getNote($punkte_vorschlag, $lv_id, $sem_kurzbz); + } + else + { + $punkte_vorschlag = round($punktesumme / $anzahlnoten, 2); + $notenschluessel = new notenschluessel(); + $note_vorschlag = $notenschluessel->getNote($punkte_vorschlag, $lv_id, $sem_kurzbz); + } + } + else + { + if (defined('CIS_GESAMTNOTE_GEWICHTUNG') && CIS_GESAMTNOTE_GEWICHTUNG) + { + $note_vorschlag = round($notensumme_gewichtet / $gewichtsumme); + } + else + { + $note_vorschlag = round($notensumme / $anzahlnoten); + } + } + + + $student->note_vorschlag = $note_vorschlag; + } + } + + // get all prüfungen with noten held in that semester in that lva + $pruefungen = $this->LePruefungModel->getPruefungenByLvStudiensemester($lv_id, $sem_kurzbz); + $pruefungenData = getData($pruefungen); + + $this->terminateWithSuccess(array($studentenData, $pruefungenData, DOMAIN, $grades)); + } + + /** + * GET METHOD + * returns List of all available & active NotenOptions + */ + public function getNoten() { + $this->load->model('education/Note_model', 'NoteModel'); + + $result = $this->NoteModel->getAllActive(); + $noten = $this->getDataOrTerminateWithError($result); + $this->terminateWithSuccess($noten); + } + + /** + * POST METHOD + * expects 'lv_id', 'sem_kurzbz', 'password', 'noten' + * Notenfreigabe method which checks the users password as a security measure. + * Tries to load Lehrveranstaltung, Studiengang and Person via Model in order to validate the coherency of input parameters + * lv_id & sem_kurzbz in relation to the noten array delivered. + * Updates the LvGesamtnote note, aswell as freigabedatum, which is key in the logic of the freigegeben/offen/changed notenStatus + * Along this process builds a html table to be placed in a confirmation email (uid only and full variant depending on config) + * which is being sent to the Lektor, aswell as the assigned Assistenz. + */ + public function saveStudentenNoten() { + $result = $this->getPostJSON(); + + if(!property_exists($result, 'sem_kurzbz') || !property_exists($result, 'lv_id') || + !property_exists($result, 'password') || !property_exists($result, 'noten')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + if(!$this->AuthLib->checkUserAuthByUsernamePassword(getAuthUID(), $result->password)->retval) { + $this->terminateWithError($this->p->t('global', 'wrongPassword'), 'general'); + } + + $lv_id = $result->lv_id; + $sem_kurzbz = $result->sem_kurzbz; + + $ret = []; + + $res = $this->LehrveranstaltungModel->load($lv_id); + if(isError($res) || !hasData($res)) { + $this->terminateWithError($this->p->t('benotungstool', 'noValidLvFoundForId', [$lv_id])); + } + + $lv = getData($res)[0]; + + $studiengang_kz = $lv->studiengang_kz; + $res = $this->StudiengangModel->load($studiengang_kz); + if(isError($res) || !hasData($res)) { + $this->terminateWithError($this->p->t('benotungstool', 'noValidStudiengangFoundForId', [$studiengang_kz])); + } + $sg = getData($res)[0]; + $lvaFullName = $sg->kurzbzlang . ' ' . $lv->semester . '.Semester + ' . $lv->bezeichnung . " - " .$lv->lehrform_kurzbz. " " . $lv->orgform_kurzbz . " - " . $sem_kurzbz; + + $emails = explode(', ', $sg->email); + + + $res = $this->PersonModel->load(getAuthPersonId()); + if(isError($res) || !hasData($res)) { + $this->terminateWithError($this->p->t('benotungstool', 'noValidPersonFoundForId', [getAuthPersonId()])); + } + $pers = getData($res)[0]; + $lektorFullName = $pers->anrede.' '.$pers->vorname.' '.$pers->nachname; //.' ('.$pers->kurzbz.')'; + + + $res = $this->StudienplanModel->getStudienplanByLvaSemKurzbz($lv_id, $sem_kurzbz); + $data = getData($res); + $studienplan_bezeichnung = ''; + foreach ($data as $row) { + $studienplan_bezeichnung .= $row->bezeichnung . ' '; + } + $betreff = $this->p->t('benotungstool','notenfreigabe').' ' . $lv->bezeichnung . ' ' . $lv->orgform_kurzbz . ' - ' . $studienplan_bezeichnung; + + $studlist = ""; + + if (defined('CIS_GESAMTNOTE_FREIGABEMAIL_NOTE') && CIS_GESAMTNOTE_FREIGABEMAIL_NOTE) { + $studlist .= "\n + \n + \n + \n"; + $studlist .= "\n"; + $studlist .= "\n"; + } else { + $studlist .= "\n"; + } + + foreach($result->noten as $note) { + + $resultLVGes = $this->LvgesamtnoteModel->getLvGesamtNoten($lv_id, $note->uid, $sem_kurzbz); + + if (!isError($resultLVGes) && hasData($resultLVGes)) + { + $lvgesamtnote = getData($resultLVGes)[0]; + + if ($lvgesamtnote->benotungsdatum > $lvgesamtnote->freigabedatum) + { + + $id = $this->LvgesamtnoteModel->update( + [$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id], + array( + 'note' => $note->note, + 'freigabevon_uid' => getAuthUID(), + 'freigabedatum' => date("Y-m-d H:i:s"), + 'updateamum' => date("Y-m-d H:i:s"), + 'updatevon' => getAuthUID() + ) + ); + + if($id) { + $res = $this->LvgesamtnoteModel->load($id->retval); + if(hasData($res)) { + $lvgesamtnote = getData($res)[0]; + $ret[] = array('uid' => $note->uid, 'freigabedatum' => $lvgesamtnote->freigabedatum, 'benotungsdatum' => $lvgesamtnote->benotungsdatum); + } + } + + if (defined('CIS_GESAMTNOTE_FREIGABEMAIL_NOTE') && CIS_GESAMTNOTE_FREIGABEMAIL_NOTE) + { + $studlist .= ""; + $studlist .= ""; + $studlist .= ""; + $studlist .= ""; + + // TODO: if defined(CIS_PUNKTE) ... + $studlist .= ""; + + $studlist .= ""; + } else { + $studlist .= "\n"; + } + } + } + } + $studlist .= "
" . $this->p->t('person','personenkennzeichen') . "" . $this->p->t('lehre','studiengang') . "" . $this->p->t('benotungstool','c4nachname') . "" . $this->p->t('benotungstool','c4vorname') . "" . $this->p->t('benotungstool','c4grade') . "" . $this->p->t('ui','bearbeitetVon') . "
" . $this->p->t('person','uid') . "
" . trim($note->matrikelnr) . "" . trim($note->kuerzel) . "" . trim($note->nachname) . "" . trim($note->vorname) . "" .$note->noteBezeichnung. "" . $lvgesamtnote->mitarbeiter_uid; + if ($lvgesamtnote->updatevon != '') + $studlist .= " (" . $lvgesamtnote->updatevon . ")"; + $studlist .= "
" . trim($note->uid) . "
"; + + // always send the mail, config toggles data contents + $this->sendEmail($lektorFullName, $lvaFullName, count($result->noten), $emails, $studlist, $betreff); + + $this->terminateWithSuccess($ret); + } + + + private function sendEmail($lektorFullName, $lvaFullName, $notenCount, $emailAdressen, $studlist, $betreff) + { + $emailAdressen[] = getAuthUID() . "@" . DOMAIN; // also send mail to lektors own adress + $adressen = implode(";", $emailAdressen); + + foreach ($emailAdressen as $email) + { + // Prepare mail content + $body_fields = array( + 'lektor' => $lektorFullName, + 'lvaname' => $lvaFullName, + 'studlist' => $studlist, + 'neuenotencount' => $notenCount, + 'adressen' => $adressen + ); + + // Send mail + sendSanchoMail( + 'Notenfreigabe', + $body_fields, + $email, + $betreff + ); + } + + } + + /** + * GET METHOD + * should return Notenvorschlag for single Students, not yet implemented since it is not needed anywhere right now, + * but could be useful later on. + */ + public function getNotenvorschlagStudent() { + // TODO: Notenvorschlag laden allgemeiner Endpunkt, der im Backend mit Logik (z.B. Moodle) angepasst werden kann. + + $this->terminateWithSuccess(); + } + + /** + * POST METHOD + * expects 'datum', 'lva_id', 'student_uid', 'note' + * Inserts or updates a pruefung for lva & student_uid at given datum (YYYY-MM-DD). When creating a new + * Pruefung, sets the provided (Prüfungs-) Note. + * Updates the LvGesamtnote of student. + * Can return 1 or 2 Prüfungen, since the original grade before the first prüfung is being saved as "Termin1" when + * a "Termin2" is being created. + */ + public function saveStudentPruefung() { // einzelne pruefung speichern + $result = $this->getPostJSON(); + + if(!property_exists($result, 'datum') || !property_exists($result, 'lva_id') || + !property_exists($result, 'student_uid') || !property_exists($result, 'note')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $student_uid = $result->student_uid; + $note = $result->note; + $punkte = null; + $datum = $result->datum; + $lva_id = $result->lva_id; + $lehreinheit_id = $result->lehreinheit_id; + + $stsem = $result->sem_kurzbz; + $typ = $result->typ; + + $jetzt = date("Y-m-d H:i:s"); + + // nachpruefungeintragen.php script calls query on campus.student_lehrveranstaltung to find a + // lehreinheit_id for lva_id -> lehreinheit should be determined prior to that in new benotungstool + // by retrieving it from students row in campus.vw_student_lehrveranstaltung earlier on + +// $lehreinheit_id = getLehreinheit($db, $lvid, $student_uid, $stsem); +// $lehreinheit_id = $result->lehreinheit_id; + + $punkte = null; + +// if($punkte!='') +// { +// // Bei Punkteeingabe wird die Note nochmals geprueft und ggf korrigiert +// $notenschluessel = new notenschluessel(); +// $note_pruef = $notenschluessel->getNote($punkte, $lva_id, $stsem); +// if($note_pruef!=$note) +// { +// $note = $note_pruef; +// $note_dirty=true; +// } +// } + + // TODO: more sophisticated empty check + if($note=='') + $note = 9; + + + $this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + $res = $this->LehrveranstaltungModel->load($lva_id); + if(isError($res) || !hasData($res)) { + $this->terminateWithError('Keine gültige Lehrveranstaltung gefunden für ID: '.$lva_id); + } + + $studiengang_kz = getData($res)[0]->studiengang_kz; + $res = $this->StudiengangModel->load($studiengang_kz); + if(isError($res) || !hasData($res)) { + $this->terminateWithError('Kein gültiger Studiengang gefunden für ID: '.$studiengang_kz); + } + + $pruefungenChanged = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum); + + //Gesamtnote updaten + $result = $this->LvgesamtnoteModel->getLvGesamtNoten($lva_id, $student_uid, $stsem); + if(!isError($result) && !hasData($result)) { + + $id = $this->LvgesamtnoteModel->insert( + array( + 'student_uid' => $student_uid, + 'lehrveranstaltung_id' => $lva_id, + 'studiensemester_kurzbz' => $stsem, + 'note' => $note, + 'punkte' => $punkte, + 'mitarbeiter_uid' => getAuthUID(), + 'benotungsdatum' => $jetzt, + 'freigabedatum' => null, + 'freigabevon_uid' => null, + 'bemerkung' => null, + 'updateamum' => null, + 'updatevon' => null, + 'insertamum' => $jetzt, + 'insertvon' => getAuthUID() + ) + ); + if($id) { + $res = $this->LvgesamtnoteModel->load($id->retval); + if(hasData($res)) $lvgesamtnote = getData($res)[0]; + } + } + else if(!isError($result) && hasData($result)) + { + $lvgesamtnote = getData($result)[0]; + + $id = $this->LvgesamtnoteModel->update( + [$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id], + array( + 'note' => $note, + 'punkte' => $punkte, + 'benotungsdatum' => $jetzt, + 'updateamum' => $jetzt, + 'updatevon' => getAuthUID() + ) + ); + + if($id) { + $res = $this->LvgesamtnoteModel->load($id->retval); + if(hasData($res)) $lvgesamtnote = getData($res)[0]; + } + + } + + $savedPruefung = $pruefungenChanged['savedPruefung'] ?? null; + $extraPruefung = $pruefungenChanged['extraPruefung'] ?? null; + + $savedPruefungData = count($savedPruefung) > 0 ? $savedPruefung[0] : null; + $extraPruefungData = count($extraPruefung) > 0 ? $extraPruefung[0] : null; + + $this->terminateWithSuccess(array($savedPruefungData, $lvgesamtnote, $extraPruefungData)); + } + + /** + * private helper method to update/insert pruefungstermine + */ + private function savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum) + { + + $status = []; + + // send $grades reference to moodle addon + Events::trigger( + 'getEntschuldigungsStatusForStudentOnDate', + function & () use (&$status) + { + return $status; + }, + [ + 'student_uid' => $student_uid, + 'datum' => $datum + ] + ); + + if(count($status) > 0 && $status[0] == true) { + $note = 17; //entschuldigt + } + + $jetzt = date("Y-m-d H:i:s"); + + $pruefungenChanged = []; + + $this->load->model('education/Lvgesamtnote_model', 'LvgesamtnoteModel'); + + if($typ == "Termin2" && defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN2') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN2) + { + + // Wenn eine Nachprüfung angelegt wird, wird zuerst eine Pruefung mit 1. Termin angelegt welche für die ursprüngliche Note + // vor den Prüfungsantritten zählt + + $result1 = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, "Termin1", $lva_id, $stsem); + + // if there is a termin 1 entry already do nothing + if(!isError($result1) && hasData($result1)) { + + } else if(!isError($result1) && !hasData($result1)) { + // new entry termin1 + + $resultLV = $this->LvgesamtnoteModel->getLvGesamtNoten($lva_id, $student_uid, $stsem); + + // update Termin1 note + if (hasData($resultLV)) + { + $lvgesamtnote = getData($resultLV)[0]; + $pr_note = $lvgesamtnote->note; + $pr_punkte = $lvgesamtnote->punkte; + $benotungsdatum = $lvgesamtnote->benotungsdatum; + } + else if(!hasData($resultLV))// set Termin1 note to "noch nicht eingetragen" + { + $pr_note = 9; + $pr_punkte = null; + $benotungsdatum = $jetzt; + } + + $id = $this->LePruefungModel->insert( + array( + 'lehreinheit_id' => $lehreinheit_id, + 'student_uid' => $student_uid, + 'mitarbeiter_uid' => getAuthUID(), + 'note' => $pr_note, +// 'punkte' => $pr_punkte, + 'pruefungstyp_kurzbz' => "Termin1", + 'datum' => $benotungsdatum, + 'anmerkung' => "", + 'insertamum' => $jetzt, + 'insertvon' => getAuthUID(), + 'updateamum' => null, + 'updatevon' => null, + 'ext_id' => null + ) + ); + if($id) { + $res = $this->LePruefungModel->load($id->retval); + if(hasData($res)) $pruefungenChanged['extraPruefung'] = getData($res); + } + } + + + // Die Pruefung wird als Termin2 eingetragen + $result2 = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, "Termin2", $lva_id, $stsem); + // if there is a termin 2 entry already update it + if(!isError($result2) && hasData($result2)) { + // update + $termin2 = getData($result2)[0]; + $id = $this->LePruefungModel->update( + $termin2->pruefung_id, + array( + 'updateamum' => $jetzt, + 'updatevon' => getAuthUID(), + 'note' => $note, +// 'punkte' => $punkte, + 'datum' => $datum, + 'anmerkung' => "" + ) + ); + if($id) { + $res = $this->LePruefungModel->load($id->retval); + if(hasData($res)) $pruefungenChanged['savedPruefung'] = getData($res); + } + + } else if(!isError($result2) && !hasData($result2)) { + // new entry termin 2 + + $id = $this->LePruefungModel->insert( + array( + 'lehreinheit_id' => $lehreinheit_id, + 'student_uid' => $student_uid, + 'mitarbeiter_uid' => getAuthUID(), + 'note' => $note, +// 'punkte' => null,//$punkte, + 'pruefungstyp_kurzbz' => $typ, + 'datum' => $datum, + 'anmerkung' => "", + 'insertamum' => $jetzt, + 'insertvon' => getAuthUID(), + 'updateamum' => null, + 'updatevon' => null, + 'ext_id' => null + ) + ); + if($id) { + $res = $this->LePruefungModel->load($id->retval); + if(hasData($res)) $pruefungenChanged['savedPruefung'] = getData($res); + } + } + + } else if($typ == "Termin3" && defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN3') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN3) + { + + $result3 = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, "Termin3", $lva_id, $stsem); + + if(!isError($result3) && hasData($result3)) { + // update + $termin3 = getData($result3)[0]; + + $id = $this->LePruefungModel->update( + $termin3->pruefung_id, + array( + 'updateamum' => $jetzt, + 'updatevon' => getAuthUID(), + 'note' => $note, +// 'punkte' => $punkte, + 'datum' => $datum, + 'anmerkung' => "" + ) + ); + if($id) { + $res = $this->LePruefungModel->load($id->retval); + if(hasData($res)) $pruefungenChanged['savedPruefung'] = getData($res); + } + + } else if(!isError($result3) && !hasData($result3)) { + // insert new termin3 + + $id = $this->LePruefungModel->insert( + array( + 'lehreinheit_id' => $lehreinheit_id, + 'student_uid' => $student_uid, + 'mitarbeiter_uid' => getAuthUID(), + 'note' => $note, +// 'punkte' => null,//$punkte, + 'pruefungstyp_kurzbz' => $typ, + 'datum' => $datum, + 'anmerkung' => "", + 'insertamum' => $jetzt, + 'insertvon' => getAuthUID(), + 'updateamum' => null, + 'updatevon' => null, + 'ext_id' => null + ) + ); + if($id) { + $res = $this->LePruefungModel->load($id->retval); + if(hasData($res)) $pruefungenChanged['savedPruefung'] = getData($res); + } + + } + } else { + $this->terminateWithError($this->p->t('benotungstool', 'wrongPruefungType', [$student_uid, $typ]), 'general'); + } + + return $pruefungenChanged; + } + + /** + * POST METHOD + * expects 'sem_kurzbz', 'lv_id', 'student_uid', 'note' + * Method that sets lv_note of student in lva and semester from provided Points/Grade Selection. + * Updates the note & benotungsdatum, which is key in the noten state offen/freigegeben/changed + */ + public function saveNotenvorschlag() { + $result = $this->getPostJSON(); + + if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') || + !property_exists($result, 'student_uid') || !property_exists($result, 'note')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $lv_id = $result->lv_id; + $student_uid = $result->student_uid; + $sem_kurzbz = $result->sem_kurzbz; + $note = $result->note; + + $result = $this->LvgesamtnoteModel->getLvGesamtNoten($lv_id, $student_uid, $sem_kurzbz); + + if(!isError($result) && hasData($result)) { + $lvgesamtnote = getData($result)[0]; + + $id = $this->LvgesamtnoteModel->update( + [$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id], + array( + 'note' => $note, + 'punkte' => null, + 'benotungsdatum' => date("Y-m-d H:i:s"), + 'updateamum' => date("Y-m-d H:i:s"), + 'updatevon' => getAuthUID() + ) + ); + + if($id) { + $res = $this->LvgesamtnoteModel->load($id->retval); + if(hasData($res)) $lvgesamtnote = getData($res)[0]; + } + } else if(!isError($result) && !hasData($result)) { + $id = $this->LvgesamtnoteModel->insert( + array( + 'student_uid' => $student_uid, + 'lehrveranstaltung_id' => $lv_id, + 'studiensemester_kurzbz' => $sem_kurzbz, + 'note' => $note, + 'punkte' => null, + 'mitarbeiter_uid' => getAuthUID(), + 'benotungsdatum' => date("Y-m-d H:i:s"), + 'freigabedatum' => null, + 'freigabevon_uid' => null, + 'bemerkung' => null, + 'updateamum' => null, + 'updatevon' => null, + 'insertamum' => date("Y-m-d H:i:s"), + 'insertvon' => getAuthUID() + ) + ); + if($id) { + $res = $this->LvgesamtnoteModel->load($id->retval); + if(hasData($res)) $lvgesamtnote = getData($res)[0]; + } + } + + $this->terminateWithSuccess(array($lvgesamtnote)); + } + + /** + * POST METHOD + * expects 'sem_kurzbz', 'lv_id', 'noten' + * Bulk variant of saveNotenvorschlag, used when importing grades from csv. + */ + public function saveNotenvorschlagBulk() { + $result = $this->getPostJSON(); + + if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') || + !property_exists($result, 'noten')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $lv_id = $result->lv_id; + $sem_kurzbz = $result->sem_kurzbz; + $noten = $result->noten; + + $retLvNoten = []; + + foreach($noten as $note) + { + + $result = $this->LvgesamtnoteModel->getLvGesamtNoten($lv_id, $note->uid, $sem_kurzbz); + + if(!isError($result) && hasData($result)) { + $lvgesamtnote = getData($result)[0]; + + $id = $this->LvgesamtnoteModel->update( + [$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id], + array( + 'note' => trim($note->note), + 'punkte' => null, + 'benotungsdatum' => date("Y-m-d H:i:s"), + 'updateamum' => date("Y-m-d H:i:s"), + 'updatevon' => getAuthUID() + ) + ); + + if($id) { + $res = $this->LvgesamtnoteModel->load($id->retval); + if(hasData($res)) $lvgesamtnote = getData($res)[0]; + } + } else if(!isError($result) && !hasData($result)) { + $id = $this->LvgesamtnoteModel->insert( + array( + 'student_uid' => $note->uid, + 'lehrveranstaltung_id' => $lv_id, + 'studiensemester_kurzbz' => $sem_kurzbz, + 'note' => trim($note->note), + 'punkte' => null, + 'mitarbeiter_uid' => getAuthUID(), + 'benotungsdatum' => date("Y-m-d H:i:s"), + 'freigabedatum' => null, + 'freigabevon_uid' => null, + 'bemerkung' => null, + 'updateamum' => null, + 'updatevon' => null, + 'insertamum' => date("Y-m-d H:i:s"), + 'insertvon' => getAuthUID() + ) + ); + if($id) { + $res = $this->LvgesamtnoteModel->load($id->retval); + if(hasData($res)) $lvgesamtnote = getData($res)[0]; + } + } + + $retLvNoten[] = $lvgesamtnote; + } + + $this->terminateWithSuccess($retLvNoten); + } + + /** + * POST METHOD + * expects 'uids', 'datum' + * Bulk variant of saveStudentPruefung, used when creating a new Prüfung for several students. Always sets note to + * "noch nicht eingetragen" for the created Prüfung. + */ + public function createPruefungen() { + $result = $this->getPostJSON(); + + if(!property_exists($result, 'uids') || !property_exists($result, 'datum')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $uids = $result->uids; + $datum = $result->datum; + $lva_id = $result->lva_id; + + $stsem = $result->sem_kurzbz; + + $ret = []; + + foreach ($uids as $student) { + $student_uid = $student->uid; + $typ = $student->typ; + $note = 9; //$result->note; // TODO: parameterize for import maybe + $punkte = ''; // TODO: check punkte feature + + $lehreinheit_id = $student->lehreinheit_id; + $ret[$student->uid] = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum); + } + + $this->terminateWithSuccess($ret); + } + + /** + * POST METHOD + * expects 'lv_id', 'sem_kurzbz', 'pruefungen' + * Bulk variant of saveStudentPruefung, used when importing pruefungsdata from csv with available noten. + */ + public function savePruefungenBulk() { + $result = $this->getPostJSON(); + + if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') || + !property_exists($result, 'pruefungen')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $lv_id = $result->lv_id; + $sem_kurzbz = $result->sem_kurzbz; + $pruefungen = $result->pruefungen; + + $ret = []; + + foreach ($pruefungen as $pruefung) { + $student_uid = $pruefung->uid; + $typ = $pruefung->typ; + $note = $pruefung->note; // TODO: parameterize for import maybe + $datum = $pruefung->datum; + $punkte = ''; // TODO: check punkte feature + + $lehreinheit_id = $pruefung->lehreinheit_id; + $ret[$student_uid] = $this->savePruefungstermin($typ, $student_uid, $lv_id, $sem_kurzbz, $lehreinheit_id, $note, $punkte, $datum); + } + + $this->terminateWithSuccess($ret); + } + +} + diff --git a/application/controllers/api/frontend/v1/Studiensemester.php b/application/controllers/api/frontend/v1/Studiensemester.php new file mode 100644 index 000000000..44f236a3c --- /dev/null +++ b/application/controllers/api/frontend/v1/Studiensemester.php @@ -0,0 +1,67 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); +class Studiensemester extends FHCAPI_Controller +{ + + private $_ci; + + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'getStudiensemester'=> self::PERM_LOGGED, + + ]); + + $this->_ci =& get_instance(); + + $this->_ci->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); + + + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + /** + * GET METHOD + * returns List of all studiensemester as well as current one + */ + public function getStudiensemester() + { + $this->_ci->StudiensemesterModel->addOrder("start", "DESC"); + $result = $this->_ci->StudiensemesterModel->load(); + + $studiensemester = getData($result); + $result = $this->_ci->StudiensemesterModel->getAkt(); + $aktuell = getData($result); + + $this->terminateWithSuccess(array($studiensemester, $aktuell)); + } + + //------------------------------------------------------------------------------------------------------------------ + // Private methods + + + +} + diff --git a/application/models/crm/Prestudent_model.php b/application/models/crm/Prestudent_model.php index ff56c3268..04ee23b9a 100644 --- a/application/models/crm/Prestudent_model.php +++ b/application/models/crm/Prestudent_model.php @@ -782,4 +782,8 @@ class Prestudent_model extends DB_Model return $this->execQuery($query, array($person_id)); } + + public function getMobilityPrestudent(){ + + } } diff --git a/application/models/education/LePruefung_model.php b/application/models/education/LePruefung_model.php index 6e51f1975..f2da314bc 100644 --- a/application/models/education/LePruefung_model.php +++ b/application/models/education/LePruefung_model.php @@ -52,4 +52,53 @@ class LePruefung_model extends DB_Model 'student_uid' => $student_uid ]); } + + public function getPruefungenByLvStudiensemester($lv_id, $sem_kurzbz) { + $qry = "SELECT tbl_pruefung.*, tbl_lehrveranstaltung.bezeichnung as lehrveranstaltung_bezeichnung, tbl_lehrveranstaltung.lehrveranstaltung_id, + tbl_note.bezeichnung as note_bezeichnung, tbl_pruefungstyp.beschreibung as typ_beschreibung, tbl_lehreinheit.studiensemester_kurzbz as studiensemester_kurzbz + FROM lehre.tbl_pruefung, lehre.tbl_lehreinheit, lehre.tbl_lehrveranstaltung, lehre.tbl_note, lehre.tbl_pruefungstyp + WHERE tbl_pruefung.lehreinheit_id=tbl_lehreinheit.lehreinheit_id + AND tbl_lehreinheit.lehrveranstaltung_id=tbl_lehrveranstaltung.lehrveranstaltung_id + AND tbl_pruefung.note = tbl_note.note + AND tbl_pruefung.pruefungstyp_kurzbz=tbl_pruefungstyp.pruefungstyp_kurzbz + AND tbl_lehrveranstaltung.lehrveranstaltung_id = ? + AND tbl_lehreinheit.studiensemester_kurzbz = ? + ORDER BY datum DESC;"; + + return $this->execReadOnlyQuery($qry, array($lv_id, $sem_kurzbz)); + } + + public function getPruefungenByUidTypLvStudiensemester($uid, $typ = null, $lv_id = null, $sem_kurzbz = null) { + $params = [$uid]; + $qry = "SELECT tbl_pruefung.*, tbl_lehrveranstaltung.bezeichnung as lehrveranstaltung_bezeichnung, tbl_lehrveranstaltung.lehrveranstaltung_id, + tbl_note.bezeichnung as note_bezeichnung, tbl_pruefungstyp.beschreibung as typ_beschreibung, tbl_lehreinheit.studiensemester_kurzbz as studiensemester_kurzbz + FROM lehre.tbl_pruefung, lehre.tbl_lehreinheit, lehre.tbl_lehrveranstaltung, lehre.tbl_note, lehre.tbl_pruefungstyp + WHERE student_uid= ? + AND tbl_pruefung.lehreinheit_id=tbl_lehreinheit.lehreinheit_id + AND tbl_lehreinheit.lehrveranstaltung_id=tbl_lehrveranstaltung.lehrveranstaltung_id + AND tbl_pruefung.note = tbl_note.note + AND tbl_pruefung.pruefungstyp_kurzbz=tbl_pruefungstyp.pruefungstyp_kurzbz"; + if ($typ != null) + { + $qry .= " AND tbl_pruefungstyp.pruefungstyp_kurzbz = ?"; + $params[] = $typ; + } + + if ($lv_id != null) + { + $qry .= " AND tbl_lehrveranstaltung.lehrveranstaltung_id = ?"; + $params[] = $lv_id; + } + + if ($sem_kurzbz != null) + { + $qry .= " AND tbl_lehreinheit.studiensemester_kurzbz = ?"; + $params[] = $sem_kurzbz; + } + + + $qry .= " ORDER BY datum DESC"; + + return $this->execReadOnlyQuery($qry, $params); + } } diff --git a/application/models/education/Lehreinheit_model.php b/application/models/education/Lehreinheit_model.php index e52af4cba..53228418d 100644 --- a/application/models/education/Lehreinheit_model.php +++ b/application/models/education/Lehreinheit_model.php @@ -314,6 +314,28 @@ EOSQL; return $this->execQuery($query, $params); } + public function getAllLehreinheitenForLvaAndMaUid($lva_id, $ma_uid, $sem_kurzbz) + { + $query = "SELECT DISTINCT tbl_lehreinheitmitarbeiter.lehreinheit_id, tbl_lehreinheit.lehrveranstaltung_id, tbl_lehreinheit.lehrform_kurzbz, + tbl_lehreinheitmitarbeiter.mitarbeiter_uid, + tbl_lehreinheitgruppe.semester, + tbl_lehreinheitgruppe.verband, + tbl_lehreinheitgruppe.gruppe, + tbl_lehreinheitgruppe.gruppe_kurzbz, + tbl_lehrveranstaltung.kurzbz, + tbl_studiengang.kurzbzlang, + (SELECT COUNT(DISTINCT datum) FROM campus.vw_stundenplan WHERE lehreinheit_id = lehre.tbl_lehreinheit.lehreinheit_id) as termincount, + (SELECT COUNT(*) FROM campus.vw_student_lehrveranstaltung WHERE lehreinheit_id = lehre.tbl_lehreinheit.lehreinheit_id) as studentcount + FROM lehre.tbl_lehreinheit JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id) + JOIN lehre.tbl_lehreinheitgruppe USING(lehreinheit_id) + JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id) + JOIN public.tbl_studiengang ON (tbl_lehreinheitgruppe.studiengang_kz = tbl_studiengang.studiengang_kz) + WHERE lehrveranstaltung_id = ? AND studiensemester_kurzbz = ? AND mitarbeiter_uid = ? + ORDER BY tbl_lehreinheitgruppe.gruppe_kurzbz"; + + return $this->execQuery($query, [$lva_id, $sem_kurzbz, $ma_uid]); + } + public function getOes($lehreinheit_id) { diff --git a/application/models/education/Lehrveranstaltung_model.php b/application/models/education/Lehrveranstaltung_model.php index 97bb48cbf..3244c51af 100644 --- a/application/models/education/Lehrveranstaltung_model.php +++ b/application/models/education/Lehrveranstaltung_model.php @@ -317,7 +317,9 @@ class Lehrveranstaltung_model extends DB_Model tbl_bisio.bisio_id, tbl_bisio.von, tbl_bisio.bis, tbl_student.studiengang_kz AS stg_kz_student, tbl_zeugnisnote.note, tbl_mitarbeiter.mitarbeiter_uid, tbl_person.matr_nr, tbl_benutzer.uid, UPPER(tbl_studiengang.typ::varchar(1) || tbl_studiengang.kurzbz) as kuerzel, tbl_studiengang.orgform_kurzbz, vw_student_lehrveranstaltung.semester, vw_student_lehrveranstaltung.studiensemester_kurzbz, vw_student_lehrveranstaltung.bezeichnung, - tbl_student.prestudent_id + tbl_student.prestudent_id, + campus.vw_student_lehrveranstaltung.lehreinheit_id + FROM campus.vw_student_lehrveranstaltung JOIN public.tbl_benutzer USING(uid) @@ -1051,6 +1053,26 @@ class Lehrveranstaltung_model extends DB_Model return $this->execQuery($qry, $params); } + public function getLvForLektorInSemester($sem_kurzbz, $uid) { + $qry = "SELECT DISTINCT (tbl_lehrveranstaltung.lehrveranstaltung_id), + UPPER(tbl_studiengang.typ::varchar(1) || tbl_studiengang.kurzbz) as stg_kurzbz, + tbl_lehrveranstaltung.semester as lv_semester, + tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung, + (SELECT kurzbz FROM public.tbl_mitarbeiter + WHERE mitarbeiter_uid=tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as lektor + FROM + lehre.tbl_lehreinheit JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id) + JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id) + JOIN public.tbl_studiengang USING(studiengang_kz) + JOIN lehre.tbl_lehrveranstaltung as lehrfach ON(tbl_lehreinheit.lehrfach_id=lehrfach.lehrveranstaltung_id) + WHERE + tbl_lehreinheit.studiensemester_kurzbz = ? + AND mitarbeiter_uid = ? + ORDER BY stg_kurzbz,lv_semester,lv_bezeichnung"; + + return $this->execReadOnlyQuery($qry, array($sem_kurzbz, $uid)); + } + public function getLvsByOrganization($oe_kurzbz) { $qry=" diff --git a/application/models/education/Note_model.php b/application/models/education/Note_model.php index 80b454398..f5ffa2cd3 100644 --- a/application/models/education/Note_model.php +++ b/application/models/education/Note_model.php @@ -11,4 +11,14 @@ class Note_model extends DB_Model $this->dbTable = 'lehre.tbl_note'; $this->pk = 'note'; } + + public function getAllActive() { + $qry ="SELECT * + FROM lehre.tbl_note + WHERE aktiv = true"; + + return $this->execReadOnlyQuery($qry); + } + + } \ No newline at end of file diff --git a/application/models/education/Pruefung_model.php b/application/models/education/Pruefung_model.php index 927d83c82..2f7ba8cf8 100644 --- a/application/models/education/Pruefung_model.php +++ b/application/models/education/Pruefung_model.php @@ -306,4 +306,5 @@ class Pruefung_model extends DB_Model return $this->loadWhereCommitteeExamsFailed(); } + } diff --git a/application/models/organisation/Studienplan_model.php b/application/models/organisation/Studienplan_model.php index 481a0564c..282959084 100644 --- a/application/models/organisation/Studienplan_model.php +++ b/application/models/organisation/Studienplan_model.php @@ -59,6 +59,37 @@ class Studienplan_model extends DB_Model 'tbl_studienplan_lehrveranstaltung.semester' => $semester )); } + + public function getStudienplanByLvaSemKurzbz($lehrveranstaltung_id, $studiensemester_kurzbz) { + $qry= " + SELECT + DISTINCT tbl_studienplan.* + FROM + lehre.tbl_studienplan + JOIN lehre.tbl_studienplan_lehrveranstaltung + USING(studienplan_id) + WHERE + tbl_studienplan_lehrveranstaltung.lehrveranstaltung_id IN ( + SELECT + lv.lehrveranstaltung_id + FROM + lehre.tbl_lehrveranstaltung AS lv + LEFT JOIN lehre.tbl_lehrveranstaltung AS t ON t.lehrveranstaltung_id=lv.lehrveranstaltung_template_id + WHERE + lv.lehrtyp_kurzbz<>'tpl' + AND (lv.lehrveranstaltung_id= ? OR (lv.lehrveranstaltung_template_id= ? AND t.lehrtyp_kurzbz='tpl')) + ) + AND EXISTS ( + SELECT 1 + FROM + lehre.tbl_studienplan_semester + WHERE studienplan_id=tbl_studienplan.studienplan_id + AND studiensemester_kurzbz= ? + AND semester = tbl_studienplan_lehrveranstaltung.semester) + ORDER BY bezeichnung"; + + return $this->execReadOnlyQuery($qry, array($lehrveranstaltung_id, $lehrveranstaltung_id, $studiensemester_kurzbz)); + } public function getStudienplanLehrveranstaltungForPrestudent($studienplan_id, $semester, $prestudent_id) { diff --git a/application/views/CisRouterView/CisRouterView.php b/application/views/CisRouterView/CisRouterView.php index ab22fbb81..fe5191da1 100644 --- a/application/views/CisRouterView/CisRouterView.php +++ b/application/views/CisRouterView/CisRouterView.php @@ -11,6 +11,7 @@ $includesArray = array( 'skipID' => '#fhccontent', 'vuedatepicker11' => true, 'customCSSs' => array( + 'vendor/vuejs/vuedatepicker_css/main.css', 'public/css/components/verticalsplit.css', 'public/css/components/searchbar/searchbar.css', 'public/css/Fhc.css', @@ -30,10 +31,13 @@ $includesArray = array( 'vendor/npm-asset/primevue/inputnumber/inputnumber.min.js', 'vendor/npm-asset/primevue/textarea/textarea.min.js', 'vendor/npm-asset/primevue/checkbox/checkbox.min.js', - 'vendor/moment/luxonjs/luxon.min.js' + 'vendor/moment/luxonjs/luxon.min.js', + 'vendor/npm-asset/primevue/password/password.js', + 'vendor/npm-asset/primevue/multiselect/multiselect.js' ), 'customJSModules' => array( - 'public/js/apps/Dashboard/Fhc.js' + 'public/js/apps/Dashboard/Fhc.js', + 'vendor/olifolkerd/tabulator5/src/js/modules/ColumnCalcs/ColumnCalcs.js' ), ); diff --git a/public/css/Cis4/Cis.css b/public/css/Cis4/Cis.css index ff1b181dd..7237a4739 100644 --- a/public/css/Cis4/Cis.css +++ b/public/css/Cis4/Cis.css @@ -854,4 +854,21 @@ html { #cis-main .modal-footer { background-color: var(--fhc-secondary); +} + +/* styling for editable dropdown column of notenvorschläge in benotungstool*/ +#notentable .tabulator-tableholder .tabulator-editable { + position: relative; + background-color: rgba(255, 255, 157, 0.73); + cursor: pointer; +} + +#notentable .tabulator-tableholder .tabulator-editable::after { + content: "▾"; + position: absolute; + right: 6px; + color: rgba(176, 176, 106, 0.73);; + font-size: x-large; + bottom: 6px; + pointer-events: none; } \ No newline at end of file diff --git a/public/js/api/factory/lehre.js b/public/js/api/factory/lehre.js index 84c8e8662..05edd02ca 100644 --- a/public/js/api/factory/lehre.js +++ b/public/js/api/factory/lehre.js @@ -35,5 +35,19 @@ export default { method: 'get', url: `/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}` }; + }, + getZugewieseneLv(uid, sem_kurzbz){ + return { + method: 'get', + url: '/api/frontend/v1/Lehre/getZugewieseneLv', + params: { uid, sem_kurzbz} + }; + }, + getLeForLv(lv_id, sem_kurzbz) { + return { + method: 'get', + url: '/api/frontend/v1/Lehre/getLeForLv', + params: { lv_id, sem_kurzbz } + }; } }; \ No newline at end of file diff --git a/public/js/api/factory/noten.js b/public/js/api/factory/noten.js new file mode 100644 index 000000000..2ac91ad21 --- /dev/null +++ b/public/js/api/factory/noten.js @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getStudentenNoten(lv_id, sem_kurzbz) { + return { + method: 'get', + url: '/api/frontend/v1/Noten/getStudentenNoten', + params: { lv_id, sem_kurzbz } + }; + }, + getNoten(){ + return { + method: 'get', + url: '/api/frontend/v1/Noten/getNoten' + }; + }, + saveStudentenNoten(password, noten, lv_id, sem_kurzbz) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/saveStudentenNoten', + params: { password, noten, lv_id, sem_kurzbz } + }; + }, + saveNotenvorschlag(lv_id, sem_kurzbz, student_uid, note) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/saveNotenvorschlag', + params: { lv_id, sem_kurzbz, student_uid, note } + }; + }, + saveStudentPruefung(student_uid, note, punkte, datum, lva_id, lehreinheit_id, sem_kurzbz, typ){ + return { + method: 'post', + url: '/api/frontend/v1/Noten/saveStudentPruefung', + params: { student_uid, note, punkte, datum, lva_id, lehreinheit_id, sem_kurzbz, typ } + }; + }, + createPruefungen(uids, datum, lva_id, sem_kurzbz){ + return { + method: 'post', + url: '/api/frontend/v1/Noten/createPruefungen', + params: { uids, datum, lva_id, sem_kurzbz } + }; + }, + saveNotenvorschlagBulk(lv_id, sem_kurzbz, noten) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/saveNotenvorschlagBulk', + params: { lv_id, sem_kurzbz, noten } + }; + }, + saveStudentPruefungBulk(lv_id, sem_kurzbz, pruefungen) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/savePruefungenBulk', + params: { lv_id, sem_kurzbz, pruefungen } + }; + } +}; \ No newline at end of file diff --git a/public/js/api/factory/studiensemester.js b/public/js/api/factory/studiensemester.js new file mode 100644 index 000000000..ac4819e98 --- /dev/null +++ b/public/js/api/factory/studiensemester.js @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getStudiensemester() { + return { + method: 'get', + url: '/api/frontend/v1/Studiensemester/getStudiensemester' + }; + }, +}; \ No newline at end of file diff --git a/public/js/apps/Dashboard/Fhc.js b/public/js/apps/Dashboard/Fhc.js index 093ad815b..32bab8808 100644 --- a/public/js/apps/Dashboard/Fhc.js +++ b/public/js/apps/Dashboard/Fhc.js @@ -19,6 +19,7 @@ import Studium from "../../components/Cis/Studium/Studium.js"; import ApiRenderers from '../../api/factory/renderers.js'; import ApiRouteInfo from '../../api/factory/routeinfo.js'; +import Benotungstool from "../../components/Cis/Benotungstool/Benotungstool.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; @@ -67,6 +68,12 @@ const router = VueRouter.createRouter({ component: Raumsuche, props: true }, + { + path: `/Cis/Benotungstool/:lv_id/:sem_kurzbz`, + name: 'Benotungstool', + component: Benotungstool, + props: true + }, // Redirect old links to new format { path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz", diff --git a/public/js/components/Cis/Benotungstool/Benotungstool.js b/public/js/components/Cis/Benotungstool/Benotungstool.js new file mode 100644 index 000000000..9581c7eda --- /dev/null +++ b/public/js/components/Cis/Benotungstool/Benotungstool.js @@ -0,0 +1,1744 @@ +import {CoreFilterCmpt} from "../../filter/Filter.js"; +import ApiLehre from "../../../api/factory/lehre.js"; +import ApiNoten from "../../../api/factory/noten.js"; +import ApiStudiensemester from "../../../api/factory/studiensemester.js"; +import BsModal from '../../Bootstrap/Modal.js'; +import VueDatePicker from '../../vueDatepicker.js.php'; +import LehreinheitenModule from '../../DropdownModes/LehreinheitenModule'; +import MobilityLegende from '../../Mobility/Legende.js'; + +export const Benotungstool = { + name: "Benotungstool", + components: { + BsModal, + CoreFilterCmpt, + MobilityLegende, + Dropdown: primevue.dropdown, + Password: primevue.password, + Textarea: primevue.textarea, + Datepicker: VueDatePicker, + Multiselect: primevue.multiselect + }, + props: { + lv_id: { + default: null, + required: true + }, + sem_kurzbz: { + default: null, + required: true + }, + viewData: { + type: Object, + required: true, + default: () => ({uid: ''}), + validator(value) { + return value && value.uid + } + } + }, + data() { + return { + neuesPruefungsdatumModalVisible: false, + offsetLeft: 0, + headerEl: null, + loading: false, + selectedUids: [], // shared selection state + selectedLehreinheit: null, + tabulatorCanBeBuilt: false, + selectedPruefungNote: null, + selectedPruefungDate: new Date(), // v-model for pruefung edit datepicker + distinctPruefungsDates: null, + pruefungStudent: null, + pruefung: null, + password: '', + changedNotenCounter: 0, + tabulatorUuid: Vue.ref(0), + domain: '', + importString: '', + teilnoten: null, + lv: null, + studenten: null, + pruefungen: null, + studiensemester: null, + selectedSemester: null, + lehrveranstaltungen: null, + selectedLehrveranstaltung: null, + tableBuiltResolve: null, + notenOptions: null, + notenOptionsLehre: null, + notenOptionsPromise: null, + tableBuiltPromise: null, + notenTableOptions: null, // built later when noten are available + notenTableEventHandlers: [{ + event: "tableBuilt", + handler: async () => { + this.tableBuiltResolve() + } + }, + { + event: "rowSelectionChanged", + handler: async (data, rows) => { + // avoid an expensive update loop if selection happens in modal + if(this.neuesPruefungsdatumModalVisible) return + + if(data.length == 1 && this.selectedUids.length == 1 && data[0].uid === this.selectedUids[0].uid){ + // special case to work around an internal tabulator selection quirk + this.selectedUids = [] + } else { + this.selectedUids = data.filter(d => d.selectable); + } + + } + }, + { + event: "cellEdited", + handler: async (cell) => { + const field = cell.getField() + + if(field === 'note_vorschlag') { + const rowData = cell.getRow().getData(); + const newValue = cell.getValue(); + const original = rowData._originalNoteVorschlag; + + // If nothing was selected, restore + if (newValue == null || newValue === "" || newValue === original) { + // revert value + cell.setValue(original, true); + } + + delete rowData._originalNoteVorschlag; // Clean up + + const row = cell.getRow() + row.reformat() // trigger reformat of arrow + } + } + } + ]}; + }, + methods: { + isValidDate_ddmmyyyy(str) { + if (typeof str !== 'string') return false; + + // Check format: dd.mm.yyyy + const regex = /^(\d{2})\.(\d{2})\.(\d{4})$/; + const match = str.match(regex); + if (!match) return false; + + // Extract date parts + const day = parseInt(match[1], 10); + const month = parseInt(match[2], 10); + const year = parseInt(match[3], 10); + + // Check valid ranges + if (month < 1 || month > 12 || day < 1 || day > 31) return false; + + // Handle months with different days and leap years + const date = new Date(year, month - 1, day); + return ( + date.getFullYear() === year && + date.getMonth() === month - 1 && + date.getDate() === day + ); + }, + identifyUid(str) { + if (typeof str !== 'string') return null; + const firstChar = str.charAt(0); + + if (/^[0-9]$/.test(firstChar)) { + return 'matrikelnr'; + } else if (/^[a-zA-Z]$/.test(firstChar)) { + return 'uid'; + } else { + return null; + } + }, + validatePruefungBulk(pruefungen) { + // need to check pruefungen for validity in respect to the students nr of antritte + // pruefungsdatum will be validated aswell so we dont get a termin 3 chronologically before + // a termin 2 which is totally possible in the old tool + const validatedPruefungen = [] + pruefungen.forEach( p => { + const student = this.studenten.find(s => s.uid === p.uid) + // check if student antrittCount is too high already + if(student.hoechsterAntritt >= 3) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat bereits ' + student.hoechsterAntritt + ' Prüfungsantritte abgelegt. Die Zeile wurde übersprungen.') + return + } + + // get student for pruefung and check if proposed datum does not conflict (no new pruefungen before existing ones) + const youngerPruefung = student.pruefungen.find(pr => { + return pr.dateObj >= p.dateObj + }) + if(youngerPruefung) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat bereits eine Prüfung am '+ youngerPruefung.datum +' eingetragen. Die Zeile wurde übersprungen.') + return + } + + validatedPruefungen.push(p) + }) + + pruefungen.splice(0, pruefungen.length, ...validatedPruefungen); + }, + validateNotenBulk(noten) { + // in case we need to further validate noten, currently parser does all + }, + parseNote(rowParts, notenbulk, rowNum) { + const id = this.identifyUid(rowParts[0]) + let student = null + if(id === 'matrikelnr') { // find student by matrnr and use uid later on + student = this.studenten.find(s => s.matrikelnr === rowParts[0]) + } else if(id === 'uid') { + student = this.studenten.find(s => s.uid === uid) + } + if(!student) { + this.$fhcAlert.alertWarning('Kein Student gefunden für ID ' + rowParts[0] + ' in Zeile Nr. ' + rowNum + ' Die Zeile wurde übersprungen.') + return + } + + const note = rowParts[1] + + // find notenoption and check if its allowed to use in lehre + const notenOption = this.notenOptions.find(n => n.note == note) + if(!notenOption.lehre) { + this.$fhcAlert.alertWarning('Keine gültige Note gefunden für ID ' + rowParts[0] + ' in Zeile Nr. ' + rowNum + ' Die Zeile wurde übersprungen.') + return + } + + notenbulk.push({uid: student.uid, note}) + }, + parsePruefung(rowParts, pruefungbulk, rowNum) { + const id = this.identifyUid(rowParts[0]) + let student = null + if(id === 'matrikelnr') { // find student by matrnr and use uid later on + student = this.studenten.find(s => s.matrikelnr === rowParts[0]) + } else if(id === 'uid') { + student = this.studenten.find(s => s.uid === rowParts[0]) + } + if(!student) { + this.$fhcAlert.alertWarning('Kein Student gefunden für ID ' + rowParts[0] + ' in Zeile Nr. ' + rowNum + ' Die Zeile wurde übersprungen.') + return + } + + const datum = rowParts[1] // should be in 'dd.MM.yyyy' + if(!this.isValidDate_ddmmyyyy(datum)) { + this.$fhcAlert.alertWarning('Ungültiges Datumformat für ID ' + rowParts[0] + ' in Zeile Nr. ' + rowNum + '. Bitte verwenden Sie das Format "DD.MM.YYYY". Die Zeile wurde übersprungen.') + return + } + const datumParts = datum.split('.') + const day = datumParts[0] + const month = datumParts[1].padStart(2, '0') + const year = datumParts[2].padStart(2, '0') + const dateStr = `${year}-${month}-${day}` + + // build date obj for validation later on + let monthInt = parseInt(month, 10) + monthInt -= 1 + const dateObj = new Date(year, monthInt, day) + + const note = rowParts[2] + + // find notenoption and check if its allowed to use in lehre + const notenOption = this.notenOptions.find(n => n.note == note) + if(!notenOption.lehre) { + + + this.$fhcAlert.alertWarning('Keine gültige Note gefunden für ID ' + rowParts[0] + ' in Zeile Nr. ' + rowNum + ' Die Zeile wurde übersprungen.') + return + } + + const typ = this.getPruefungstypForStudentByAntritt(student) + + pruefungbulk.push({uid: student.uid, datum: dateStr, note, typ, lehreinheit_id: student.lehreinheit_id, dateObj}) + }, + saveNotenBulk(notenbulk) { + this.loading = true + this.$api.call(ApiNoten.saveNotenvorschlagBulk(this.lv_id, this.sem_kurzbz, notenbulk)).then(res => { + if(res.meta.status === 'success') { + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/notenImportSuccessAlert')) + const lvNoten = res.data + + + lvNoten.forEach(lvn => { + // 1.) get relevant student row by uid + const s = this.studenten.find(s => s.uid === lvn.student_uid) + s.note_vorschlag = lvn.note // TODO: check if note_vorschlag should be changed by import + + s.lv_note = lvn.note + + this.teilnoten[s.uid].note_lv = lvn.note + // recalculate freigabestatus + s.freigabedatum = this.parseDate(lvn['freigabedatum']) + s.benotungsdatum = this.parseDate(lvn['benotungsdatum']) + + s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid); + }) + + } + + this.$refs.notenTable.tabulator.redraw(true) + }).finally(()=>{ + this.loading = false + }) + }, + savePruefungBulk(pruefungenbulk) { + this.loading = false + this.$api.call(ApiNoten.saveStudentPruefungBulk(this.lv_id, this.sem_kurzbz, pruefungenbulk)) + .then((res)=> { + if(res.meta.status === 'success') { + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/pruefungImportSuccessAlert')) + this.handleAddNewPruefungenResponse(res, pruefungenbulk) + } + }).finally(()=>{this.loading = false}) + }, + handleAddNewPruefungenResponse(res, uids) { + const pruefungen = res.data + uids.forEach(entry => { + const saved = pruefungen[entry.uid].savedPruefung?.[0] + const extra = pruefungen[entry.uid].extraPruefung?.[0] + + const student = this.studenten.find(s => s.uid == entry.uid) + if(!student) return + + // check for extra pruefung (termin1) to add before + if(extra) { + extra.datum = extra.datum.split(' ')[0] + if(!this.distinctPruefungsDates.includes(extra.datum)) { + this.insertSortedDate(this.distinctPruefungsDates, extra.datum) + } + + student.pruefungen.push(extra) + student[extra.datum] = extra + } + + if(!this.distinctPruefungsDates.includes(saved.datum)) { + this.insertSortedDate(this.distinctPruefungsDates, saved.datum) + } + + // add pruefung to pruefungen array + student.pruefungen.push(saved) + + // add pruefung to student via its datum as a field + student[saved.datum] = saved + + // usually should be in order naturally, just to be save + student.pruefungen.sort((p1, p2) => { + if(p1.datum > p2.datum) { + return 1 + } else if (p1.datum < p2.datum) { + return -1 + } else { + return 0 + } + }) + + // recalculate student antritte + student.hoechsterAntritt = this.getAntrittCountStudent(student) + }) + + // add col to table + const cols = [...this.notenTableOptions.columns.slice(0, -1)]; + const kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1]; + + // TODO: could reuse cols instead of recreating all from a variable maybe + this.distinctPruefungsDates.forEach((date, index)=>{ + const dateparts = date.split('-') + const titledate = `${dateparts[2]}.${dateparts[1]}.${dateparts[0]}` + + + // TODO: test if this holds true + const originalNote = index === 0 + cols.push({ + title: titledate,//this.$p.t('benotungstool/pruefungNr', [index+1]), + field: date, + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 150, + originalNote, + visible: true + }) + }) + + cols.push(kommCol) // keep kommPruef Col as last + // redraw table + + this.loading = false + + this.$refs.notenTable.tabulator.clearSort() + this.$refs.notenTable.tabulator.setColumns(cols) + this.$refs.notenTable.tabulator.setData(this.studenten); + this.$refs.notenTable.tabulator.redraw(true); + }, + importNoten() { + const rows = this.importString.split('\n') + const bulk = [] + let mode = '' + // read the lines + rows.forEach((r,i) => { + const rowParts = r.split('\t') + if(rowParts.length === 3) { + this.parsePruefung(rowParts, bulk, i) + mode = 'pruefung' // if line parts are not uniform we are in trouble + } else if(rowParts.length === 2) { + this.parseNote(rowParts, bulk, i) + mode = 'note' + } + }) + + // parsers check for notenOption.lehre === true and if student uid/matrikelnr matches + + // pruefungen check for younger pruefungen, so there are no further antritte with + // previous dates from automatic imports + if(mode === 'note') { + this.validateNotenBulk(bulk) + this.saveNotenBulk(bulk) + } + else if (mode === 'pruefung') { + this.validatePruefungBulk(bulk) + this.savePruefungBulk(bulk) + } + + this.$refs.modalContainerNotenImport.hide() + }, + selectionArraysAreEqual(arr1, arr2) { + if(arr1.length !== arr2.length) return false + + const sortFunc = (s1, s2) => { + if(s1.nachname > s2.nachname) { + return 1 + } else if (s1.nachname < s2.nachname) { + return -1 + } else { + return 0 + } + } + const sortedArr1 = arr1.sort(sortFunc) + const sortedArr2 = arr2.sort(sortFunc) + + const arrsREqual = sortedArr1.every((val, index) => val === sortedArr2[index]); + + return arrsREqual + }, + getNotenTableOptions() { + return { + rowHeight: 40, + height: 700, + index: 'uid', + layout: 'fitDataStretch', + placeholder: this.$p.t('global/noDataAvailable'), + selectable: true, + selectableRangeMode: "click", // shift+click + selectablePersistence: false, // reset selection on table reload + selectableCheck: function(row, e){ + const data = row.getData(); + + if(data['kommPruef']) return false + else if(data.hoechsterAntritt >= 3) return false // 3 pruefungen counted + + return true; // student can be selected to add pruefung + }, + rowFormatter: this.fixTabulatorSelectionFormatter, + columns: [ + { + formatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle select manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + return checkbox; + }, + titleFormatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle "select all" manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + return checkbox; + }, + hozAlign: "center", + headerSort: false, + formatterParams: { + handleClick: this.selectHandler + }, + titleFormatterParams: { + handleClick: this.selectAllHandler + }, + width: 50, + }, + {title: Vue.computed(() => this.$p.t('benotungstool/c4mail')), field: 'email', formatter: this.mailFormatter, tooltip: false, widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4antrittCount')), field: 'hoechsterAntritt', tooltip: false, widthGrow: 1}, + {title: 'UID', field: 'uid', tooltip: false, widthGrow: 1, topCalc: this.sumCalcFunc}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4vorname')), field: 'vorname', headerFilter: true, tooltip: false, widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4nachname')), field: 'nachname', headerFilter: true, widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4teilnoten')), field: 'teilnote', widthGrow: 1, formatter: this.teilnotenFormatter}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4note')), field: 'note_vorschlag', + editor: 'list', + editorParams: (cell) => { + // write original cell value into row to it can be retrieved if edit is cancelled without selection + const rowData = cell.getRow().getData(); + rowData._originalNoteVorschlag = cell.getValue(); + + return { + values: this.notenOptionsLehre.map(opt => ({ + label: opt.bezeichnung, + value: opt.note + })) + }; + }, + editable: (cell) => { + const rowData = cell.getRow().getData(); + const noteOption = this.notenOptions.find(opt => opt.note == rowData.note) + if(!noteOption) return true + + // also if student has any pruefungsnote disable noten selection + if(this.pruefungen?.find(p => p.student_uid == rowData.uid)) return false + + return noteOption.lkt_ueberschreibbar + }, + formatter: (cell) => { + const rowData = cell.getRow().getData(); + const value = cell.getValue() + const match = this.notenOptions?.find(opt => opt.note == value) + const val = match ? match.bezeichnung : value + const p = this.pruefungen?.find(p => p.student_uid == rowData.uid) + let style = '' + + if(val === undefined) return '' + if(p || !match?.lkt_ueberschreibbar) style = 'color: gray;font-style: italic; background-color: #f0f0f0;pointer-events: none;opacity: 0.6;user-select: none;cursor: not-allowed;' + return '
' + val + '
' + }, + widthGrow: 1}, + {title: '', width: 50, hozAlign: 'center', formatter: this.arrowFormatter, cellClick: this.saveNote}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4lvnote')), field: 'lv_note', + formatter: this.notenFormatter, + headerFilter: 'list', + headerFilterParams: () => { + return { values: ["\u00A0",this.$p.t('benotungstool/c4noteEmpty') ,this.$p.t('benotungstool/c4positiv'), this.$p.t('benotungstool/c4negativ') ,...this.notenOptions.map(opt => opt.bezeichnung)] } + }, + headerFilterFunc: this.notenFilterFunc, + widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4freigabe')), field: 'freigegeben', widthGrow: 1, formatter: this.freigabeFormatter}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4zeugnisnote')), + field: 'note', + formatter: this.notenFormatter, + topCalc: this.negativeNotenCalc, + topCalcFormatter: this.negativeNotenCalcFormatter, + headerFilter: 'list', + headerFilterParams: () => { + return { values: ["\u00A0", this.$p.t('benotungstool/c4noteEmpty'),this.$p.t('benotungstool/c4positiv'), this.$p.t('benotungstool/c4negativ') ,...this.notenOptions.map(opt => opt.bezeichnung)] } + }, + headerFilterFunc: this.notenFilterFunc, + widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('benotungstool/c4kommPruef')), + field: 'kommPruef', widthGrow: 1, + formatter: this.pruefungFormatter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", minWidth: 150} + ], + persistence: false, + } + }, + selectHandler(e, cell) { + const row = cell.getRow(); + + if(row.isSelected()){ + row.deselect(); + } else { + row.select(); + } + + // stop built-in handler + e.stopPropagation(); + return false; + }, + selectAllHandler(e, cell) { + const table = cell.getTable(); + const rows = table.getRows(); + + // custom select all logic + const allowed = rows.filter(r => r.getData().selectable); + const selected = allowed.every(r => r.isSelected()); + + if(selected){ + allowed.forEach(r => r.deselect()); + } else { + allowed.forEach(r => r.select()); + } + + // stop built-in handler + e.stopPropagation(); + return false; + }, + fixTabulatorSelectionFormatter(row) { + // if a row is not selectable, remove the checkbox from the dom + + const data = row.getData() + + const notSelectable = data.pruefungen?.find(p => p.pruefungstyp_kurzbz == 'kommPruef') || data.hoechsterAntritt >= 3 + if(notSelectable) { + row.getElement().children[0]?.children[0]?.remove() + } + }, + terminCalcFunc(entries) { + return entries.reduce((acc, cur) => { + if(cur !== undefined) acc++ + return acc + }, 0) + }, + terminCalcFormatter(cell) { + const cellval = cell.getValue() + return this.$p.t('benotungstool/prueflingSelectionv2')+': ' + cellval + }, + negativeNotenCalcFormatter(cell) { + const cellval = cell.getValue() + return this.$p.t('benotungstool/c4negativ')+': ' + cellval + }, + negativeNotenCalc(entries) { + return entries.reduce((acc, cur) => { + const opt = this.notenOptions.find(opt => opt.note == cur) + if(opt && !opt.positiv) acc++ + return acc + }, 0) + }, + sumCalcFunc(entries) { + return entries.length + }, + notenFilterFunc(filterVal, rowVal) { + // option of the searchterm + const opt = this.notenOptions.find(opt => opt.bezeichnung === filterVal) + // searchterm is not empty fallback and the note finds an option match + if(rowVal !== null && rowVal !== undefined && opt?.note == rowVal) { + return true + } + + // empty searchterm fallback to show all + if(filterVal === "\u00A0" || filterVal === "" || filterVal === null) { + return true + } + + // specific searchterm cases + if(filterVal === this.$p.t('benotungstool/c4positiv')) { + // option of the rowValue + const valOpt = this.notenOptions.find(opt => opt.note == rowVal) + if(!valOpt) return false + return valOpt.positiv + } + if(filterVal === this.$p.t('benotungstool/c4negativ')) { + const valOpt = this.notenOptions.find(opt => opt.note == rowVal) + if(!valOpt) return false + return !valOpt.positiv + } + if(filterVal === this.$p.t('benotungstool/c4noteEmpty') && rowVal === null) { + return true + } + + return false + }, + parseDate(timestamp) { + if(!timestamp) return null + const [datePart, timePart] = timestamp.split(" "); + const [year, month, day] = datePart.split("-").map(Number); + const [hour, minute, second] = timePart.split(":").map(Number); + return new Date(year, month - 1, day, hour, minute, second); + }, + checkFreigabe(freigabedatum, benotungsdatum, uid) { + if(!freigabedatum) { + // check for change -> set freigabe to 'changed' on change + return 'offen' + } else if(benotungsdatum > freigabedatum) { + return 'changed' + } else { + return 'ok' + } + }, + unselectableFormatter(row) { + + }, + notenFormatter(cell) { + const value = cell.getValue() + const field = cell.getField() + let style = 'display: flex; justify-content: center; align-items: center; height: 100%;'; + // Wenn sich die Zeugnisnote von der von Ihnen freigegebenen Note unterscheidet, + // wird erstere rot umrandet markiert. + + + const data = cell.getData() + if(field == 'note' && data.note && data.note != data.lv_note) { + style += 'color:red; border-color:red; border-style:solid; border-width:1px;' + } + + const match = this.notenOptions.find(opt => opt.note == value) + const val = match ? match.bezeichnung : value + if(val) return '
' + val + '
' + else return '' + + }, + freigabeFormatter(cell) { + const value = cell.getValue() + + if(value === 'ok') { + return '
' + + '
' + } else if (value === 'offen') { + return '
' + + '
' + } else if (value === 'changed') { + return '
' + + '
' + } + + return value + }, + saveNote(e, cell) { // Notenvorschlag freigeben + const row = cell.getRow() + const data = row.getData() + + if(!data.note_vorschlag) return + this.$api.call(ApiNoten.saveNotenvorschlag(this.lv_id, this.sem_kurzbz, data.uid, data.note_vorschlag)) + .then((res) => { + if (res.meta.status === 'success') { + const s = this.studenten.find(s => s.uid === data.uid) + this.teilnoten[s.uid].note_lv = data.note_vorschlag + s.freigabedatum = this.parseDate(res.data[0]['freigabedatum']) + s.benotungsdatum = this.parseDate(res.data[0]['benotungsdatum']) + + s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid); + + row.update({ lv_note: data.note_vorschlag, freigegeben: 'changed' }) + // row.update({ freigegeben: 'changed' }) + row.reformat() // trigger reformat of arrow + this.changedNotenCounter++; + } + }) + + + }, + teilnotenFormatter(cell) { + const val = cell.getValue() + return '
'+val+'
' + }, + pruefungFormatter(cell) { + const data = cell.getData() + + const noteDef = data.note ? this.notenOptions.find(n => n.note == data.note) : null + if(!data.note || !noteDef?.lkt_ueberschreibbar) { + return '' + } + + const colDef = cell.getColumn().getDefinition() + + // column is just a date, student can have any of his antritte on this date, so we need to get + // student.pruefungen and look for a pruefung with this cols title as date + + const field = cell.getColumn().getField() + const studentPruefung = field != 'kommPruef' ? data.pruefungen.find(p => p.datum === field) : data['kommPruef'] + + // is this column/cell allowed to have an add pruefung action + const canAdd = field !== 'kommPruef' && data.hoechsterAntritt < 4 && !colDef.originalNote + + // TODO: check for some time limit maybe? old pruefungen can be changed/created + // TODO: it also looks ugly and unprofessional, should at some peoplt disable/hide the change action + + // Create root row div + const rowDiv = document.createElement('div'); + rowDiv.className = 'row flex-nowrap'; + rowDiv.style.display = 'flex'; + rowDiv.style.justifyContent = 'center'; + rowDiv.style.alignItems = 'center'; + rowDiv.style.height = '100%'; + + if(studentPruefung) { + let color = '' + switch(studentPruefung.pruefungstyp_kurzbz) { + case 'Termin1': + color = 'green' + break + case 'Termin2': + color = 'yellow' + break + case 'Termin3': + color = 'orange' + break + case 'kommPruef': + color = 'red' + break + } + + rowDiv.style.borderLeft = `4px solid ${color}`; + rowDiv.style.marginLeft = "6px"; // small indent so text doesn't overlap + rowDiv.style.boxSizing = "border-box"; + } + + function createCol(content, classParam) { + const colDiv = document.createElement('div'); + colDiv.className = classParam ?? 'col-4'; + colDiv.style.justifyContent = 'center'; + colDiv.style.alignItems = 'center'; + colDiv.style.height = '100%'; + + if (typeof content === 'string') { + colDiv.textContent = content; + } else if (content instanceof HTMLElement) { + colDiv.appendChild(content); + } + + return colDiv; + } + + if(data[field]) { + // showing date in + + // const dateParts = data[field].datum.split('-') + // const date = `${dateParts[2]}.${dateParts[1]}.${dateParts[0]}` + // + // // First column (date) + // rowDiv.appendChild(createCol(date, 'col-4 d-flex justify-content-center align-items-center')); + + const noteDefEntry = data.note ? this.notenOptions.find(n => n.note == data[field].note) : null + + // Second column (note_bezeichnung) + rowDiv.appendChild(createCol(noteDefEntry.bezeichnung || '', 'col-auto d-flex justify-content-center align-items-center')); + + // no actions on kommPruef allowed + // no actions on termin1 aka pruefung 0 aka ursprüngliche note erlaubt + if(field === 'kommPruef' || colDef.originalNote) { + // rowDiv.appendChild(createCol('', 'col-4 d-flex justify-content-center align-items-center')); // append empty col4 to have formatting similar + return rowDiv + } + + // Third column (button) + const button = document.createElement('button'); + button.className = 'btn btn-outline-secondary'; + button.textContent = this.$p.t('benotungstool/changePruefungButtonText'); + button.addEventListener('click', () => { + this.openPruefungModal(data, data[field], field); + }); + + rowDiv.appendChild(createCol(button, 'col-4 d-flex justify-content-center align-items-center')); + + return rowDiv; + + } else if (canAdd) { // return new btn action + + // dont render the add button in cells where a younger pruefung exists for the students + const youngerPruefung = data.pruefungen.find(p => p.datum > field) + if(youngerPruefung) return rowDiv + + const button = document.createElement('button'); + button.className = 'btn btn-outline-secondary'; + button.textContent = this.$p.t('benotungstool/addPruefungButtonText'); + button.addEventListener('click', () => { + this.openPruefungModal(data, null, field) + }); + + rowDiv.appendChild(createCol(button), 'col-4 d-flex justify-content-center align-items-center'); + + return rowDiv; + } else return '' + }, + openPruefungModal(student, pruefung = null, field) { + this.pruefungStudent = student + this.pruefung = pruefung + const dateStr = this.pruefung?.datum ?? field + + const pruefungDateParts = dateStr.split('-') + + // does not work correctly + + // new date obj so datepicker picks ob the change by ref + // const newDate = new Date() + // newDate.setFullYear(+pruefungDateParts[0]) + // newDate.setMonth(+pruefungDateParts[1]) + // // newDate.setMonth(newDate.getMonth() - 1) // acount for js date month offset + // newDate.setDate(+pruefungDateParts[2]) + + // works correctly + const newDate = new Date(+pruefungDateParts[0], +pruefungDateParts[1], +pruefungDateParts[2]) + newDate.setMonth(newDate.getMonth() - 1) + this.selectedPruefungDate = newDate + + + if(this.pruefung?.note) { + this.selectedPruefungNote = this.notenOptions.find(n => n.note == this.pruefung.note) + } else { + this.selectedPruefungNote = null + } + + this.$refs.modalContainerPruefung.show() + }, + pruefungTitleFormatter(cell) { + const def = cell.getColumn().getDefinition() + if(def.originalNote) return this.$p.t('benotungstool/c4originalZnote') + return def.title; + }, + arrowFormatter(cell) { + const row = cell.getRow() + const data = row.getData() + + if(!data.note_vorschlag || (data.note_vorschlag == data.lv_note)) { // uncolored arrow + return '
' + + '
' + } + + // can save a notenvorschlag -> colored + return '
' + + '
' + }, + mailFormatter(cell) { + const val = cell.getValue() + return '
' + + '
' + }, + buildMailToLink(student){ + return 'mailto:' + student.uid +'@'+ this.domain + }, + insertSortedDate(arr, dateStr) { + // Binary search to find insertion index + let left = 0, right = arr.length; + while (left < right) { + const mid = (left + right) >> 1; + if (arr[mid] < dateStr) left = mid + 1; + else right = mid; + } + arr.splice(left, 0, dateStr); // insert at index + return arr; + }, + tableResolve(resolve) { + this.tableBuiltResolve = resolve + }, + notenOptionsResolve(resolve) { + this.notenOptionsResolve = resolve + }, + setupData(data){ + this.studenten = data[0] ?? [] + this.studenten.forEach(s => { + s.pruefungen = [] + s.infoString = `${s.vorname} ${s.nachname}`// (${s.semester}${s.verband}${s.gruppe}) Mat.: ${s.matrikelnr}`// used for multiselect + }) + this.pruefungen = data[1] ?? [] + this.domain = data[2] + + // contains notenvorschläge from moodle, lv_note + this.teilnoten = data[3] ?? [] + + // let pruefungenRegularColCount = 0; + this.distinctPruefungsDates = [] + const cols = [...this.notenTableOptions.columns.slice(0, -1)]; + const kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1]; + + this.pruefungen?.forEach(p => { + const dateParts = p.datum.split('-') + p.dateObj = new Date(dateParts[0], +(dateParts[1]) - 1, dateParts[2]) + + const student = this.studenten.find(s => s.uid === p.student_uid) + + if(!student) return + + // TODO: filter kommPruef here? or change kommProf ColDefinition + + if(p.pruefungstyp_kurzbz !== 'kommPruef' && !this.distinctPruefungsDates.includes(p.datum)) this.distinctPruefungsDates.push(p.datum) + + // seperate kommPruefungen from previous pruefungen counts since the column count variability always ends with this + if(p.pruefungstyp_kurzbz == 'kommPruef') { + student['kommPruef'] = p + } else { + student.pruefungen.push(p) + } + + // if(student.pruefungen.length > pruefungenRegularColCount) pruefungenRegularColCount = student.pruefungen.length + }) + + this.studenten?.forEach(s => { + // sort students regular pruefungen by datum + s.pruefungen.sort((p1, p2) => { + if(p1.datum > p2.datum) { + return 1 + } else if (p1.datum < p2.datum) { + return -1 + } else { + return 0 + } + }) + // set the sorted pruefungen to their respective column fields + s.pruefungen.forEach((p, i) => { + s[p.datum] = p + }) + + s.hoechsterAntritt = this.getAntrittCountStudent(s) + s.email = this.buildMailToLink(s) + s.lv_note = this.teilnoten[s.uid].note_lv + s.freigabedatum = this.parseDate(this.teilnoten[s.uid]['freigabedatum']) + s.benotungsdatum = this.parseDate(this.teilnoten[s.uid]['benotungsdatum']) + s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid); + + const grades = this.teilnoten[s.uid].grades + s.teilnote = '' + + grades.forEach(g => { + const notenOption = this.notenOptions.find(n=>n.note == g.grade) + if(notenOption.positiv) s.teilnote += (''+g.text +''+ '
') + else s.teilnote += (''+g.text +''+ '
') + }) + + Object.defineProperty(s, 'selectable', { + get() { + const kP = s.pruefungen?.find(p => p.pruefungstyp_kurzbz == 'kommPruef') + return !(kP || s.hoechsterAntritt >= 3) + }, + set() { + // empty setter so tabulator doesnt scream + }, + enumerable: true, + configurable: true + }) + + }) + + this.distinctPruefungsDates.sort((d1, d2) => { + if(d1 > d2) { + return 1 + } else if (d1 < d2) { + return -1 + } else { + return 0 + } + }) + this.distinctPruefungsDates.forEach((date, index)=>{ + const dateparts = date.split('-') + const titledate = `${dateparts[2]}.${dateparts[1]}.${dateparts[0]}` + + // TODO: should studenten without shadow pruefung Termin have their "ursprüngliche Zeugnisnote" + // col filled for consistency reasons? + + // TODO: test if this holds true + const originalNote = index === 0 + cols.push({ + title: titledate,//this.$p.t('benotungstool/pruefungNr', [index+1]), + field: date, + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 200, + originalNote, + visible: true + }) + }) + + cols.push(kommCol) // keep kommPruef Col as last + + this.loading = false + + this.$refs.notenTable.tabulator.clearSort() + this.$refs.notenTable.tabulator.setColumns(cols) + this.$refs.notenTable.tabulator.setData(this.studenten); + this.$refs.notenTable.tabulator.redraw(true); + }, + loadNoten(lv_id, sem_kurzbz) { + this.loading = true + this.$api.call(ApiNoten.getStudentenNoten(lv_id, sem_kurzbz)) + .then(res => { + if(res?.data) this.setupData(res.data) + }).finally(()=> { + this.loading = false + }) + }, + 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.notenTableOptions.height = window.visualViewport.height - rect.top + this.$refs.notenTable.tabulator.setHeight(this.notenTableOptions.height) + }, + setupCreated() { + this.loading = true + // fetch lva dropdown + this.$api.call(ApiLehre.getZugewieseneLv(this.viewData?.uid, this.sem_kurzbz)).then(res => { + this.lehrveranstaltungen = res.data + + // build dropdown option string + this.lehrveranstaltungen.forEach(lva => { + lva.fullString = `${lva.stg_kurzbz} - ${lva.lv_semester}: ${lva.lv_bezeichnung}` + }) + + this.selectedLehrveranstaltung = this.lehrveranstaltungen.find(lva => lva.lehrveranstaltung_id == this.lv_id) + }) + + LehreinheitenModule.setupContext(this.$.appContext.config.globalProperties) + LehreinheitenModule.bindParams(Vue.ref(Vue.computed(() => this.LeDropdownParams))); + + // fetch sem_kurzbz dropdown + this.$api.call(ApiStudiensemester.getStudiensemester()).then(res => { + this.studiensemester = res.data[0] + this.selectedSemester = this.studiensemester.find(sem => sem.studiensemester_kurzbz === this.sem_kurzbz) + }) + + // fetch noten dropdown + this.$api.call(ApiNoten.getNoten()).then(res => { + this.notenOptions = res.data + this.notenOptionsLehre = res.data.filter(n => n.lehre === true) + this.notenTableOptions = this.getNotenTableOptions() + this.tabulatorCanBeBuilt = true // because promises would be more work and not much better here + }).catch(e => { + this.loading = false + }) + + }, + async setupMounted() { + this.tableBuiltPromise = new Promise(this.tableResolve) + await this.tableBuiltPromise + + this.headerEl = document.getElementById('nav-main') + if(this.headerEl) { + this.headerEl.addEventListener("shown.bs.collapse", this.handleSidebar); + this.headerEl.addEventListener("hidden.bs.collapse", this.handleSidebar); + this.handleSidebar() + } + + + + + this.loadNoten(this.lv_id, this.sem_kurzbz) + // this.calcMaxTableHeight() + + }, + handleSidebar() { + // this.$refs.notenTable.tabulator.blockRedraw(); // stop automatic resizes + + this.offsetLeft = this.headerEl.getBoundingClientRect().width; + + // setTimeout(() => { + // this.$refs.notenTable.tabulator.restoreRedraw(); // re-enable + // this.$refs.notenTable.tabulator.redraw(true); // manual recalculation + // }, 400); // after animation finishes + + }, + lvChanged(e) { + this.$router.push({ + name: "Benotungstool", + params: { + sem_kurzbz: this.sem_kurzbz, + lv_id: e.value.lehrveranstaltung_id + } + }) + + // reload data + this.loadNoten(e.value.lehrveranstaltung_id, this.sem_kurzbz) + }, + ssChanged(e) { + // change url params & write history + this.$router.push({ + name: "Benotungstool", + params: { + sem_kurzbz: e.value.studiensemester_kurzbz, + lv_id: this.lv_id + } + }) + + // diff lv_id -> reload zugewiesene lv + this.$api.call(ApiLehre.getZugewieseneLv(this.viewData?.uid, e.value.studiensemester_kurzbz)).then(res => { + this.lehrveranstaltungen = res.data + + // build dropdown option string + this.lehrveranstaltungen.forEach(lva => { + lva.fullString = `${lva.stg_kurzbz} - ${lva.lv_semester}: ${lva.lv_bezeichnung}` + }) + + this.selectedLehrveranstaltung = this.lehrveranstaltungen.find(lva => lva.lehrveranstaltung_id == this.lv_id) + }).then(()=>{ + + // reload data + this.loadNoten(this.lv_id, e.value.studiensemester_kurzbz) + }) + + }, + getOptionLabel(option) { + return option.studiensemester_kurzbz + }, + getOptionLabelLv(option) { + return option.fullString + }, + getOptionLabelLe(option) { + return option.infoString + }, + getPruefungstypForStudentByAntritt(student) { + // when adding new pruefungen, determine the next pruefungstyp by using the antritt counter + switch (student.hoechsterAntritt) { + case 0: + return "Termin2" + break + case 1: + return "Termin2" + break + case 2: + return "Termin3" + break + default: + return "" + } + }, + savePruefungEingabe() { + const year = this.selectedPruefungDate.getFullYear(); + const month = String(this.selectedPruefungDate.getMonth() + 1).padStart(2, '0'); // Months are 0-based + const day = String(this.selectedPruefungDate.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}`; + + // first pruefung is always "Termin2" since normal note counts as Termin1 + // const pOffset = this.pruefung === null && this.pruefungStudent.pruefungen.length === 0 ? 2 : 1 + + const typ = this.pruefung ? this.pruefung.pruefungstyp_kurzbz : this.getPruefungstypForStudentByAntritt(this.pruefungStudent) + const note = this.selectedPruefungNote?.note ?? 9 // noch nicht eingetragen + this.$api.call(ApiNoten.saveStudentPruefung( + this.pruefungStudent.uid, + note, + this.pruefung?.punkte ?? '', + dateStr, + this.lv_id, + this.pruefungStudent.lehreinheit_id, + this.sem_kurzbz, + typ + )).then(res => { + if(res.meta.status === 'success') { //'Prüfung für Student ' + this.pruefungStudent.uid + ' bearbeitet oder angelegt' + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/pruefungSaveForUid', [this.pruefungStudent.uid])) + + const s = this.studenten.find(s => s.uid === res.data[1]?.student_uid) + + s.freigabedatum = this.parseDate(res.data[1]?.['freigabedatum']) + s.benotungsdatum = this.parseDate(res.data[1]?.['benotungsdatum']) + + s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid); + + s.lv_note = res.data[1]?.note + + // add new pruefung to row + if(!this.pruefung) { + this.handleAddNewTermin(res.data, s) + } else { // update existing + const oldIndex = s.pruefungen.findIndex(p => p.pruefung_id == this.pruefung.pruefung_id) + if(oldIndex !== -1) { + s.pruefungen.splice(oldIndex, 1, res.data[0]) + s[res.data[0].datum] = res.data[0] + } + + // antritte might have changed due to different benotung + s.hoechsterAntritt = this.getAntrittCountStudent(s) + } + + const oldScrollLeft = this.$refs.notenTable.tabulator.rowManager.scrollLeft + const oldScrollTop = this.$refs.notenTable.tabulator.rowManager.scrollTop + this.$refs.notenTable.tabulator.redraw(true) + + Vue.nextTick(()=> { + const table = this.$refs.notenTable.tabulator.element.querySelector('.tabulator-tableholder') + if(table) { + table.scrollLeft = oldScrollLeft; + table.oldScrollTop = oldScrollTop; + } + }) + + } + }).finally(()=> { + this.pruefungStudent = null + this.pruefung = null + }) + + this.$refs.modalContainerPruefung.hide() + }, + handleAddNewTermin(data, student){ + const savedPruefung = data[0] + const extra = data[2] + + // check for extra pruefung (termin1) to add before + if(extra) { + extra.datum = extra.datum.split(' ')[0] + if(!this.distinctPruefungsDates.includes(extra.datum)) { + this.insertSortedDate(this.distinctPruefungsDates, extra.datum) + } + + student.pruefungen.push(extra) + student[extra.datum] = extra + } + + if(!this.distinctPruefungsDates.includes(savedPruefung.datum)) { + this.insertSortedDate(this.distinctPruefungsDates, savedPruefung.datum) + } + + // add pruefung to pruefungen array + student.pruefungen.push(savedPruefung) + + // add pruefung to student via its datum as a field + student[savedPruefung.datum] = savedPruefung + + // usually should be in order naturally, just to be save + student.pruefungen.sort((p1, p2) => { + if(p1.datum > p2.datum) { + return 1 + } else if (p1.datum < p2.datum) { + return -1 + } else { + return 0 + } + }) + + // recalculate student antritte + student.hoechsterAntritt = this.getAntrittCountStudent(student) + + // add col to table + const cols = [...this.notenTableOptions.columns.slice(0, -1)]; + const kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1]; + + + // TODO: could reuse cols instead of recreating all from a variable maybe + this.distinctPruefungsDates.forEach((date, index)=>{ + const dateparts = date.split('-') + const titledate = `${dateparts[2]}.${dateparts[1]}.${dateparts[0]}` + + // TODO: should studenten without shadow pruefung Termin have their "ursprüngliche Zeugnisnote" + // col filled for consistency reasons? + + // TODO: test if this holds true, maybe in case where there are only kommPruef? + const originalNote = index === 0 + cols.push({ + title: titledate,//this.$p.t('benotungstool/pruefungNr', [index+1]), + field: date, + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 200, + originalNote, + visible: true + }) + }) + + cols.push(kommCol) // keep kommPruef Col as last + + // set Cols + this.$refs.notenTable.tabulator.clearSort() + this.$refs.notenTable.tabulator.setColumns(cols) + + // redraw table outside this function + }, + saveNoteneingabe() { + this.$api.call(ApiNoten.saveStudentenNoten(this.password, this.changedNoten, this.lv_id, this.sem_kurzbz)) + .then((res) => { + if(res.meta.status === 'success') { + this.$fhcAlert.alertWarning('Noten gespeichert') + } + + res.data.forEach(d => { + const s = this.studenten.find(s => s.uid === d.uid) + s.freigabedatum = this.parseDate(d.freigabedatum) + s.benotungsdatum = this.parseDate(d.benotungsdatum) + s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid); + }) + this.changedNotenCounter++; + + this.$refs.notenTable.tabulator.redraw(true) + }) + + this.$refs.modalContainerNotenSpeichern.hide() + }, + openSaveModal() { + this.$refs.modalContainerNotenSpeichern.show() + }, + openNewPruefungsdatumModal() { + this.$refs.modalContainerNeuesPruefungsdatum.show() + }, + openNotenImportModal() { + this.$refs.modalContainerNotenImport.show() + }, + getOptionLabelNotePruefung(option) { + return option.bezeichnung + }, + leChanged(e) { + this.selectedLehreinheit = e.value + }, + addPruefung(){ + + this.$refs.modalContainerNeuesPruefungsdatum.hide() + + // filter students that already have a pruefung on datum + + const year = this.selectedPruefungDate.getFullYear(); + const month = String(this.selectedPruefungDate.getMonth() + 1).padStart(2, '0'); // Months are 0-based + const day = String(this.selectedPruefungDate.getDate()).padStart(2, '0'); + const dateStrDb = `${year}-${month}-${day}`; + const dateStrFront = `${day}.${month}.${year}`; + + const uids = this.selectedUids.map(student => { + return { + uid: student.uid, + lehreinheit_id: student.lehreinheit_id, + typ: this.getPruefungstypForStudentByAntritt(student)//student.hoechsterAntritt + } + }) + + this.loading = true; + this.$api.call(ApiNoten.createPruefungen( + uids, + dateStrDb, + this.lv_id, + this.sem_kurzbz, + )).then(res => { + if(res.meta.status === "success") { + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/pruefungAngelegtAn', [dateStrFront])) + + + this.handleAddNewPruefungenResponse(res, uids) + + } + }) + }, + getAntrittCountStudent(student) { + // checks for existence of a prüfung with a note that resolves to a + // "angetretene Prüfung" -> anything except "entschuldigt" & "noch nicht eingetragen" + // and returns the next allowed pruefungstyp from the number of taken pruefungen + + // 1 -> reguläre note + // 2 -> erste Nachprüfung / Termin2 + // 3 -> 2te Nachprüfung / Termin3 + // 4 -> kommPruef + if(student['kommPruef']) return 4 + + let pruefungsAntrittCount = 0 + const pLen = student.pruefungen.length + for(let i = 0; i < pLen; i++) { + const p = student.pruefungen[i] + + if(p.note != 9 && p.note != 17) pruefungsAntrittCount++ + } + + // when student never had to take an exam beyond the original benotung + // aka pruefungsantritt (even though it does not have to have pruefungscharacter) + // it still counts as an antritt, except it is coming from a notenOption like "angerechnet" + // which indicates no participation at all + if(pruefungsAntrittCount === 0 && student.note){ + const noteOption = this.notenOptions.find(note => note.note == student.note) + + if(noteOption.lehre) return 1 + else return 0 + } + + return pruefungsAntrittCount + } + }, + watch: { + selectedUids(newVal, oldVal) { + const table = this.$refs.notenTable?.tabulator + + if (!table) return; + + const allRows = table.getRows(); + + allRows.forEach(row => { + const rowData = row.getData(); + const found = newVal.find(stud => stud.uid == rowData.uid) + if (found) { + row.select(); // ensure row is selected + const cb = row.getElement().children[0]?.children[0] + if(cb) cb.checked = true + } else { + row.deselect(); // ensure row is deselected + const cb = row.getElement().children[0]?.children[0] + if(cb) cb.checked = false + } + }); + }, + selectedLehreinheit(newVal) { + if(!this.$refs.notenTable) return + this.$refs.notenTable.tabulator.clearFilter(); + if(newVal) this.$refs.notenTable.tabulator.setFilter("lehreinheit_id", "=", newVal.lehreinheit_id); + }, + getKommPruefCount(newVal) { + if(this.$refs.notenTable?.tabulator && newVal > 0) { + const kommPruefCol = this.$refs.notenTable?.tabulator.getColumn("kommPruef") + kommPruefCol.show() + } else if(this.$refs.notenTable?.tabulator && newVal == 0) { + const kommPruefCol = this.$refs.notenTable?.tabulator.getColumn("kommPruef") + kommPruefCol.hide() + } + } + }, + computed: { + LehreinheitenModule() { + return LehreinheitenModule; + }, + LeDropdownParams() { + return { + lv_id: this.lv_id, + sem_kurzbz: this.sem_kurzbz + } + }, + getStudentenOptions() { + return this.studenten ? this.studenten.filter(s => s.selectable) : [] + }, + getKommPruefCount(){ + let counter = 0 + this.studenten?.forEach(s => {if(s['kommPruef']){counter++}}) + return counter + }, + getSaveBtnClass() { + return this.changedNoten?.length ? "btn btn-primary ml-2" : "btn btn-secondary ml-2" + }, + getNewBtnClass() { + return "btn btn-primary ml-2" + }, + getNotenImportBtnClass() { + return "btn btn-primary ml-2" + }, + changedNoten() { + const v = this.changedNotenCounter // hack to trigger computed + const cs = this.studenten ? this.studenten.reduce((acc, cur) => { + const teilnote = this.teilnoten[cur.uid] + if(teilnote.note_lv && (cur.benotungsdatum > cur.freigabedatum)) { + + // write noteBezeichnung into changed Note so we can send emails in backend easier... + const opt = this.notenOptions.find(opt => opt.note == cur.lv_note) + cur.noteBezeichnung = opt.bezeichnung + + acc.push(cur) + } + return acc + }, []) : [] + return cs + }, + getNotenfreigabeHinweistext() { + return this.$p.t('benotungstool/notenfreigabeHinweistextv3') + }, + getNotenimportHinweistext() { + return this.$p.t('benotungstool/notenimportHinweistextv3') + } + }, + created() { + this.setupCreated() + }, + mounted() { + this.setupMounted() + }, + unmounted() { + if(this.headerEl) { + this.headerEl.removeEventListener("shown.bs.collapse", this.handleSidebar) + this.headerEl.removeEventListener("hidden.bs.collapse", this.handleSidebar) + this.headerEl = null + } + }, + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+

{{$p.t('benotungstool/benotungstoolTitle')}}

+

{{ lv?.bezeichnung }}

+
+
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+
+
+ +
+ + + +
+ + + `, +}; + +export default Benotungstool; \ No newline at end of file diff --git a/public/js/components/DropdownModes/LehreinheitenModule.js b/public/js/components/DropdownModes/LehreinheitenModule.js new file mode 100644 index 000000000..25686c81b --- /dev/null +++ b/public/js/components/DropdownModes/LehreinheitenModule.js @@ -0,0 +1,71 @@ +import ApiLehre from "../../api/factory/lehre.js"; + +const options = Vue.ref([]); +const params = Vue.ref({}); +let appContext = null; + +export function setupContext(globalProps) { + appContext = globalProps +} + +// bind and watch api params via reference +export function bindParams(paramsRef) { + Vue.watch( + paramsRef, + (newVal) => { + params.value = { ...newVal }; + fetchLehreinheiten(newVal.lv_id, newVal.sem_kurzbz); + }, + { immediate: true, deep: true } + ); +} + +async function fetchLehreinheiten(lv_id, sem_kurzbz) { + appContext.$api.call(ApiLehre.getLeForLv(lv_id, sem_kurzbz)).then(res => { + + const data = [] + // TODO: could be done on server in some shared function, copied from anw extension for now + res.data?.retval?.forEach(entry => { + + const existing = data.find(e => e.lehreinheit_id === entry.lehreinheit_id) + if (existing) { + // supplement info + existing.infoString += ', ' + if (entry.gruppe_kurzbz !== null && entry.direktinskription == false) { + existing.infoString += entry.gruppe_kurzbz + } else { + existing.infoString += entry.kurzbzlang + '-' + entry.semester + + (entry.verband ? entry.verband : '') + + (entry.gruppe ? entry.gruppe : '') + } + } else { + // entries are supposed to be fetched ordered by non null gruppe_kurzbz first + // so a new entry will always start with those groups, others are appended afterwards + entry.infoString = entry.kurzbz + ' - ' + entry.lehrform_kurzbz + ' - ' + if (entry.gruppe_kurzbz !== null && entry.direktinskription == false) { + entry.infoString += entry.gruppe_kurzbz + } else { + entry.infoString += entry.kurzbzlang + '-' + entry.semester + + (entry.verband ? entry.verband : '') + + (entry.gruppe ? entry.gruppe : '') + } + + data.push(entry) + } + }) + + options.value = [...data] + + }) +} + +// export the module and relevant fields via reactive +const LehreinheitenModule = Vue.reactive({ + options, + optionLabel: 'infoString', + placeholder: Vue.computed(()=>appContext?.$p.t('lehre/lehreinheit')), + setupContext, + bindParams +}); + +export default LehreinheitenModule; \ No newline at end of file diff --git a/public/js/components/Mobility/Legende.js b/public/js/components/Mobility/Legende.js new file mode 100644 index 000000000..ce6b7b009 --- /dev/null +++ b/public/js/components/Mobility/Legende.js @@ -0,0 +1,20 @@ + +export const MobilityLegende = { + name: 'MobilityLegende', + template:` +
+ +
Legende
+
(i) ... Incoming
+
(o) ... Outgoing
+
(ar) ... angerechnet
+
(iar) ... intern angerechnet
+
(nz) ... nicht zugelassen
+
(ma) ... MitarbeiterIn
+
(a.o.) ... Außerordentliche/r HörerIn
+
(d.d.) ... Double Degree Program
+
+ ` +}; + +export default MobilityLegende; \ No newline at end of file diff --git a/system/checksystem.php b/system/checksystem.php index 4af8bc56d..1f7968282 100644 --- a/system/checksystem.php +++ b/system/checksystem.php @@ -150,6 +150,7 @@ $berechtigungen = array( array('lehre','Berechtigung fuer CIS-Seite'), array('lehre/abgabetool','Projektabgabetool, Studentenansicht'), array('lehre/abgabetool:download','Download von Projektarbeitsabgaben'), + array('lehre/benotungstool','Cis4 Gesamtnoteneingabe'), array('lehre/freifach','Freifachverwaltung'), array('lehre/lehrfach','Lehrfachverwaltung'), array('lehre/lehrfach:begrenzt','Lehrfachverwaltung - nur aktiv aenderbar, nur aktive LF werden angezeigt'), diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 91a5f12ee..f2dbfb8e9 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -48855,6 +48855,1063 @@ and represent the current state of research on the topic. The prescribed citatio ) ), //**************************** CORE/search end + // CIS4 GESAMTNOTENEINGABE START ----------------------------------------------------------------------------------- + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'benotungstoolTitle', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Gesamtnote', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Final Grade', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4mail', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Kontakt', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Email', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4vorname', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Vorname', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First Name', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4antrittCount', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Anzahl Prüfungsantritte', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Number of exam attempts', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4nachname', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nachname', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Last Name', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4positiv', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Positiv', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'positive', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4negativ', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Negativ', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Negative', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4noteEmpty', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Unbenotet', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Ungraded', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4notenImportieren', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Noten Import', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Import Grades', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4import', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Importieren', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Import', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4teilnoten', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Teilnoten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Partial Grades', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4note', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Punkte/Note', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Points/Grade', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4lvnote', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'LV-Note', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Subject grade', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4freigabe', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Freigabe', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Approval', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4password', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Passwort', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Password', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4zeugnisnote', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Zeugnisnote', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Transcript Grade', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4date', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Datum', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Date', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4grade', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Note', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Grade', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4termin1', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nachprüfung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Re-examination', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4termin2', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '2. Nebenprüfungstermin', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => '2nd subsidiary examination date', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4kommPruef', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Kommissionelle Prüfung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Oral Examination', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4anlegen', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Anlegen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Create', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4change', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Ändern', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Change', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4import', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Importieren', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Import', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4changedGradesAvailable', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Es sind geänderte Noten vorhanden. Geben Sie diese frei, um die Assistenz zu informieren', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'There are changed grades. Please send the changes to the assistant by clicking "Approval"', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4gradeListExcel', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Notenliste für den LV-Noten-Import (Excel)', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Grade list for the subject grade import (Excel)', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'passwort', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Passwort', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Password', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'pruefungNr', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfung Nr. {0}', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Examination No. {0}', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'noteneingabeSpeichern', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Noteneingabe speichern', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Save note entry', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'noteneingabeSpeichern', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Noteneingabe speichern', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Save note entry', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'noteneingabeBestätigen', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Noten bestätigen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Confirm grades', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'createPruefungFor', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfung erstellen für', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Create examination for', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'editPruefungFor', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfung bearbeiten für', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Edit examination for', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'approveGrades', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Noten freigeben', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Approve Grades', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4originalZnote', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Ursprüngliche Zeugnisnote', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Original Transcript Grade', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4addNewPruefung', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Neues Prüfungsdatum anlegen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Create new exam date', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'prueflingSelectionv2', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Studierende', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Examinees', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'notenImportSuccessAlert', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Noten erfolgreich importiert', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Grades successfully imported', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'pruefungImportSuccessAlert', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfungen erfolgreich importiert und gespeichert', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Exams successfully imported and saved', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'changePruefungButtonText', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Ändern', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Change', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'addPruefungButtonText', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Hinzufügen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Create', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'pruefungSaveForUid', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfung für Studierenden {0} gespeichert', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Exam for student {0} has been saved', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'pruefungAngelegtAn', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfung an {0} angelegt', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Exam on {0} has been created', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'noValidLvFoundForId', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Keine gültige Lehrveranstaltung gefunden für ID: {0}', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No valid course found for ID: {0}', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'noValidStudiengangFoundForId', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Kein gültiger Studiengang gefunden für ID: {0}', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No valid study program found for ID: {0}', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'noValidPersonFoundForId', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Keine gültige Person gefunden für ID: {0}', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No Valid Person found for ID: {0}', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'wrongPruefungType', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfung für Studierenden {0} konnte nicht bearbeitet werden, {1} ist keine gültiger Prüfungstyp.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Exam for Student {0} was not saved, {1} is not a valid exam type.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'notenfreigabe', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Notenfreigabe', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Grades Approval', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'notenfreigabeHinweistextv3', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Notenfreigabe +

+ Wenn alle einzutragenden Noten vermerkt sind (Nachtragung jederzeit möglich) können diese per Klick auf Freigabe für die Studiengangsassistenz freigegeben werden. +

+ Aus Gründen der erhöhten Sicherheit ist bei der Freigabe der Noten die Eingabe Ihres Passwortes erforderlich. +

+ Zulässige Noten: 1-5, Nicht beurteilt (nb), Noch nicht eingetragen (), Bestanden (b), Mit Erfolg teilgenommen (met), entschuldigt (en), unentschuldigt (ue) +

+ Bei der Freigabe wird ein Info-Email an Sie und die zuständige Studiengangsassistenz geschickt. Enthalten sind Mat.Nr., Vor- und Zuname sowie die Note der neuen oder geänderten Einträge. +

+ Freigegebene Einträge sind mit einem grün hinterlegten Häkchen gekennzeichnet. +

+ Wenn Sie bereits freigegebene Noten verändern, werden diese mit einem grau hinterlegten Häkchen markiert (als Hinweis für Sie, dass die Assistenz bislang darüber noch nicht per Mail informiert wurde. Sie sieht allerdings diese neue Note sofort in ihrer Oberfläche) +

+ Freigegebene Noten kann die Assistenz nun als Zeugnisnote übernehmen (wird im nächsten Feld für Sie zur Kontrolle angezeigt) +

+ Wenn sich die Zeugnisnote von der von Ihnen freigegebenen Note unterscheidet wird erstere rot umrandet markiert.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Grade Release +

+ Once all grades to be entered have been recorded (additions are possible at any time), they can be released to the program assistant by clicking on "Release." +

+ For increased security reasons, you will be required to enter your password when releasing grades.2) +

+ Permissible grades: 1-5, Not Assessed (nb), Not Yet Entered (), Passed (b), Successfully Participated (met), Excused (en), Unexcused (ue) +

+ When the grade is released, an information email will be sent to you and the responsible program assistant. This email contains the student ID number, first and last name, and the grade for the new or changed entries. +

+ Released entries are marked with a green check mark. +

+ If you change grades that have already been approved, they will be marked with a gray checkmark (indicating that the assistant has not yet been notified by email. However, they will immediately see this new grade in their interface). +

+ The assistant can now adopt approved grades as the grade on their report card (this will be displayed in the next field for your review). +

+ If the report card grade differs from the grade you approved, the former will be highlighted with a red border.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'notenimportHinweistextv3', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Laden Sie sich die Notenliste im Excel-Format unter CIS → Lehrveranstaltungen → Anwesenheits- und Notenlisten → Notenliste herunter. +

+ Tragen Sie die Noten in das Dokument und speichern Sie dieses. +

+ Markieren Sie im Excel-Dokument die Inhalte der Spalten Personenkennzeichen und Note für jene Studierende, deren Noten Sie importieren möchten (ohne Überschrift !) +

+ Kopieren Sie die markierten Inhalte mittels strg + c oder Bearbeiten → Kopieren in die Zwischenablage +

+ Einfügen der Inhalte mittels strg + v oder Bearbeiten → Einfügen +

+ Mit einem Klick auf Import werden die Noten übernommen.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Download the grade list in Excel format from CIS → Courses → Attendance and Grade Lists → Grade List. +

+ Enter the grades into the document and save it. +

+ In the Excel document, select the contents of the Person ID and Grade columns for the students whose grades you want to import (without headings!). +

+ Copy the selected content to the clipboard using Ctrl + c or Edit → Copy. +

+ Paste the content using Ctrl + v or Edit → Paste. +

+ Click Import to import the grades.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + + // CIS4 GESAMTNOTENEINGABE ENDE ------------------------------------------------------------------------------------ + + //**************************** FHC-Core-ElectronicOnboarding array( 'app' => 'core',