diff --git a/application/config/noten.php b/application/config/noten.php new file mode 100644 index 000000000..0eedde2ae --- /dev/null +++ b/application/config/noten.php @@ -0,0 +1,6 @@ + wirken sich nicht auf Antritte aus +$config['NOTEN_OHNE_ANTRITT'] = [9, 17]; // tbl_note pk \ No newline at end of file diff --git a/application/config/routes.php b/application/config/routes.php index 8024f449a..0eb2839cd 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -67,6 +67,7 @@ $route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1'; $route['Cis/OtherLvPlan/.*'] = 'Cis/OtherLvPlan/index/$1'; //Route for LV Plan Stg/Semester/Verband/Gruppe $route['Cis/StgOrgLvPlan/.*'] = 'Cis/StgOrgLvPlan/index/$1'; +$route['Cis/Benotungstool/.*'] = 'Cis/Benotungstool/index/$1'; $route['Abgabetool/Assistenz'] = 'Cis/Abgabetool/Assistenz'; $route['Abgabetool/Assistenz/(:any)'] = 'Cis/Abgabetool/Assistenz/$1'; diff --git a/application/controllers/Cis/Benotungstool.php b/application/controllers/Cis/Benotungstool.php new file mode 100644 index 000000000..94faeff98 --- /dev/null +++ b/application/controllers/Cis/Benotungstool.php @@ -0,0 +1,37 @@ + self::PERM_LOGGED + ]); + + $this->_ci =& get_instance(); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Public methods + + /** + * @return void + */ + public function index() + { + $viewData = array( + 'uid'=>getAuthUID(), + ); + + $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 c2ebeb10b..fae2f2f3b 100644 --- a/application/controllers/api/frontend/v1/Lehre.php +++ b/application/controllers/api/frontend/v1/Lehre.php @@ -18,18 +18,9 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); -//require_once('../../../include/studiengang.class.php'); -//require_once('../../../include/student.class.php'); -//require_once('../../../include/datum.class.php'); -//require_once('../../../include/mail.class.php'); -//require_once('../../../include/benutzerberechtigung.class.php'); -//require_once('../../../include/phrasen.class.php'); -//require_once('../../../include/projektarbeit.class.php'); -//require_once('../../../include/projektbetreuer.class.php'); - class Lehre extends FHCAPI_Controller { - + /** * Object initialization */ @@ -40,38 +31,56 @@ class Lehre extends FHCAPI_Controller 'LV' => self::PERM_LOGGED, 'Pruefungen' => self::PERM_LOGGED, 'semesterAverageGrade' => self::PERM_LOGGED, + 'getZugewieseneLv' => self::PERM_LOGGED, + 'getLeForLv' => self::PERM_LOGGED ]); - + + $this->load->library('PhrasesLib'); + + $this->loadPhrases( + array( + 'global', + 'ui', + 'abgabetool' + ) + ); + + $this->load->helper('hlp_sancho_helper'); + + require_once(FHCPATH . 'include/studiengang.class.php'); + require_once(FHCPATH . 'include/student.class.php'); + require_once(FHCPATH . 'include/projektarbeit.class.php'); + require_once(FHCPATH . 'include/projektbetreuer.class.php'); } //------------------------------------------------------------------------------------------------------------------ // Public methods - /** + /** * constructs the emails of the groups from a lehrveranstaltung */ - public function lvStudentenMail() + public function lvStudentenMail() { - $lehreinheit_id = $this->input->get("lehreinheit_id",TRUE); - - // return early if the required parameter is missing - if(!isset($lehreinheit_id)) - { - $this->terminateWithError('Missing required parameter', self::ERROR_TYPE_GENERAL); - } + $lehreinheit_id = $this->input->get("lehreinheit_id",TRUE); - $this->load->model('education/Lehreinheit_model', 'LehreinheitModel'); - - $studentenMails = $this->LehreinheitModel->getStudentenMail($lehreinheit_id); + // return early if the required parameter is missing + if(!isset($lehreinheit_id)) + { + $this->terminateWithError('Missing required parameter', self::ERROR_TYPE_GENERAL); + } - $studentenMails = $this->getDataOrTerminateWithError($studentenMails); + $this->load->model('education/Lehreinheit_model', 'LehreinheitModel'); + + $studentenMails = $this->LehreinheitModel->getStudentenMail($lehreinheit_id); + + $studentenMails = $this->getDataOrTerminateWithError($studentenMails); //convert array of objects into array of strings $studentenMails = array_map(function($element){ return $element->mail; }, $studentenMails); - $this->terminateWithSuccess($studentenMails); + $this->terminateWithSuccess($studentenMails); } public function LV($studiensemester_kurzbz, $lehrveranstaltung_id) @@ -81,13 +90,13 @@ class Lehre extends FHCAPI_Controller $result = $this->LehrveranstaltungModel->getLvsByStudentWithGrades(getAuthUID(), $studiensemester_kurzbz, getUserLanguage(), $lehrveranstaltung_id); $result = current($this->getDataOrTerminateWithError($result)); - + $this->terminateWithSuccess($result); } /** * fetches all Pruefungen of a student for a specific lehrveranstaltung - * if the student passed the Pruefung on the first attempt, no information about the Pruefungen is stored in the database + * if the student passed the Pruefung on the first attempt, no information about the Pruefungen is stored in the database * @param mixed $lehrveranstaltung_id * @return void */ @@ -145,5 +154,46 @@ class Lehre extends FHCAPI_Controller $this->terminateWithSuccess(['average_grade' => $averageGrade, 'weighted_average_grade' => $weightedAverageGrade]); } -} + /** + * 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..0d385a5d2 --- /dev/null +++ b/application/controllers/api/frontend/v1/Noten.php @@ -0,0 +1,1144 @@ +. + */ + +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'), + 'getCisConfig' => array('lehre/benotungstool:rw'), + 'getNoteByPunkte' => array('lehre/benotungstool:rw') + ]); + + $this->load->library('AuthLib', null, 'AuthLib'); + $this->load->library('PhrasesLib'); + + // Loads LogLib with different debug trace levels to get data of the job that extends this class + // It also specify parameters to set database fields + $this->load->library('LogLib', array( + 'classIndex' => 5, + 'functionIndex' => 5, + 'lineIndex' => 4, + 'dbLogType' => 'API', // required + 'dbExecuteUser' => 'RESTful API', + 'requestId' => 'API', + 'requestDataFormatter' => function ($data) { + return json_encode($data); + } + ), 'logLib'); + + // Loads phrases system + $this->loadPhrases([ + 'global', + 'person', + 'benotungstool', + 'lehre', + 'ui' + ]); + + $this->load->model('education/LePruefung_model', 'LePruefungModel'); + $this->load->model('education/Lvgesamtnote_model', 'LvgesamtnoteModel'); + $this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); + $this->load->model('education/Notenschluesselaufteilung_model', 'NotenschluesselaufteilungModel'); + $this->load->model('person/Person_model', 'PersonModel'); + $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); + $this->load->model('crm/Student_model', 'StudentModel'); + $this->load->model('codex/Mobilitaet_model', 'MobilitaetModel'); + $this->load->model('organisation/Erhalter_model', 'ErhalterModel'); + + $this->load->config('noten'); + $this->load->helper('hlp_sancho_helper'); + + } + + public function getCisConfig() { + $NOTEN_OHNE_ANTRITT = $this->config->item('NOTEN_OHNE_ANTRITT'); + + $this->terminateWithSuccess( + array( + // Punkte bei der Noteneingabe anzeigen + 'CIS_GESAMTNOTE_PUNKTE' => CIS_GESAMTNOTE_PUNKTE, + + // basically on/of toggle for the points/grade col and the arrow button + 'CIS_GESAMTNOTE_UEBERSCHREIBEN' => CIS_GESAMTNOTE_UEBERSCHREIBEN, + + // only relevant in punkte calculation in backend + // 'CIS_GESAMTNOTE_GEWICHTUNG' => CIS_GESAMTNOTE_GEWICHTUNG, + + // this one should always be set true since fh prüfungsordnung requires at least 3 antritte (t1+t2+kP) + // send it anyway to use in maxAntritte calculation + 'CIS_GESAMTNOTE_PRUEFUNG_TERMIN2' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN2, + + // should in 99% of cases be kept true to enable 4 antritte in total, but if a certain + // fh still works with 3 antritte per note this can limit the max number of antritte accordingly + 'CIS_GESAMTNOTE_PRUEFUNG_TERMIN3' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN3, + + // used to toggle availability of kommPruef type pruefungen + 'CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF' => CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF, + + //technically exists but is never used, could be LE pendant to next flag + // 'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_NOTE, + + // basically a toggle for "use teilnoten" and the source is always moodle + // setting this to false breaks legacy tool and if that was fixed it wouldnt render any table at all + // anyway so not sure why this even is a config at all. placebo at best + + // toggles availability of the teilnoten column... existas but do we really need this? + 'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE, + + // send a mail when approving grades + 'CIS_GESAMTNOTE_FREIGABEMAIL_NOTE' => CIS_GESAMTNOTE_FREIGABEMAIL_NOTE, + + 'NOTEN_OHNE_ANTRITT' => $NOTEN_OHNE_ANTRITT + ) + ); + } + + /** + * 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); + + if(count($studentenData) == 0) { + $this->terminateWithError('No students found for lva and semester'); + } + + $func = function ($value) { + return $value->uid; + }; + + $grades = array(); + $student_uids = array_map($func, $studentenData); + + $funcpre = function ($value) { + return $value->prestudent_id; + }; + + $prestudent_ids = array_map($funcpre, $studentenData); + + if(count($student_uids) > 0) { + $mobres = $this->MobilitaetModel->getMobilityZusatzForUids($student_uids); + $mobData = $this->getDataOrTerminateWithError($mobres); + + $result = $this->ErhalterModel->load(); + $erhalter = getData($result)[0]; + + $erhalter_kz = '9' . sprintf("%03s", $erhalter->erhalter_kz); + foreach($mobData as $mob) { + $grades[$mob->uid]['mobility_zusatz'] = $this->MobilitaetModel->formatZusatz($mob, $erhalter_kz); + } + } + + foreach($student_uids as $uid) { + $grades[$uid]['grades'] = []; + + $result = $this->LvgesamtnoteModel->getLvGesamtNoten($lv_id, $uid, $sem_kurzbz); + $this->addMeta($uid.'getLvGesamtNoten', $result); + 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 + try { + Events::trigger( + 'getExternalGrades', + function & () use (&$grades) + { + return $grades; + }, + [ + 'lvid' => $lv_id, + 'stsem' => $sem_kurzbz + ] + ); + } catch (Throwable $t) { + $this->addMeta('getExternalGradesError', $t->getMessage()); + } + + // assign the anw% to the students in the studentData loop + $anwresult = $this->getAnwesenheiten($prestudent_ids, $lv_id, $sem_kurzbz); + + // calculate notenvorschläge from teilnoten + foreach($studentenData as $student) { + + $student->anwquote = $anwresult[$student->prestudent_id]; + + $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; + } + } + + if (CIS_GESAMTNOTE_PUNKTE) { + if (defined('CIS_GESAMTNOTE_GEWICHTUNG') && CIS_GESAMTNOTE_GEWICHTUNG) { + // Lehreinheitsgewichtung + $punkte_vorschlag = round($punktesumme_gewichtet / $gewichtsumme, 2); + $note_vorschlag_result = $this->NotenschluesselaufteilungModel->getNote($punkte_vorschlag, $lv_id, $sem_kurzbz); + $note_vorschlag = $this->getDataOrTerminateWithError($note_vorschlag_result); + } else { + $punkte_vorschlag = round($punktesumme / $anzahlnoten, 2); + $note_vorschlag_result = $this->NotenschluesselaufteilungModel->getNote($punkte_vorschlag, $lv_id, $sem_kurzbz); + $note_vorschlag = $this->getDataOrTerminateWithError($note_vorschlag_result); + } + } 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, $anwresult)); + } + + /** + * 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"; + if(defined(CIS_GESAMTNOTE_PUNKTE) && CIS_GESAMTNOTE_PUNKTE) { + $studlist .= "\n"; + } + $studlist .= "\n"; + $studlist .= "\n"; + } else { + $studlist .= "\n"; + } + + foreach($result->noten as $note) { + + $resultLVGes = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz); + $this->addMeta($note->uid.'$resultLVGes', $resultLVGes); + 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' => $lvgesamtnote->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 .= ""; + + if(defined(CIS_GESAMTNOTE_PUNKTE) && CIS_GESAMTNOTE_PUNKTE) { + $studlist .= ""; + } + $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','c4punkte') . "" . $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) . "" . trim($lvgesamtnote->punkte) . "" .$note->noteBezeichnung. "" . $lvgesamtnote->mitarbeiter_uid; + if ($lvgesamtnote->updatevon != '') + $studlist .= " (" . $lvgesamtnote->updatevon . ")"; + $studlist .= "
" . trim($note->uid) . "
"; + + $this->logLib->logInfoDB(array('saveStudentenNoten', array( + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId(), array($result->noten, $lv_id, $sem_kurzbz))); + + // always send the mail, config toggles data contents + $this->sendFreigabeEmail($lektorFullName, $lvaFullName, count($result->noten), $emails, $studlist, $betreff); + + $this->terminateWithSuccess($ret); + } + + + private function sendFreigabeEmail($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 used anywhere but required as per + * https://openproject.technikum-wien.at/projects/fh-complete/work_packages/60873/activity + */ + public function getNotenvorschlagStudent() { + $uid = $this->input->get("uid",TRUE); + + // if uid is missing or empty, fall back to getAuthUID() + if ($uid === NULL || trim((string)$uid) === '') { + $uid = getAuthUID(); + } + + $sem_kurzbz = $this->input->get("sem_kurzbz",TRUE); + $lv_id = $this->input->get("lv_id",TRUE); + + if ($uid === NULL || trim((string)$uid) === '' + || $sem_kurzbz === NULL || trim((string)$sem_kurzbz) === '' + || $lv_id === NULL || trim((string)$lv_id) === '') { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + // TODO: we need a zuordnungscheck here? any lektor can get any grades? + // what about assistenz with different rights doing lectors job once again? + // students checking their own grades? + + $result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $uid, $sem_kurzbz); + $data = $this->getDataOrTerminateWithError($result); + + // TODO: moodle teilnote but it seems they only work for a whole course? + + // get anw% of student by prestudent_id +// $anwresult = $this->getAnwesenheiten($prestudent_ids, $lv_id, $sem_kurzbz); + + + + $this->terminateWithSuccess($data); + } + + /** + * 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 = $result->punkte; + $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"); + + if(CIS_GESAMTNOTE_PUNKTE && isset($punkte) && $punkte >= 0) { + // Bei Punkteeingabe wird die Note nochmals geprueft und ggf korrigiert + $resultNote = $this->NotenschluesselaufteilungModel->getNote($punkte, $lva_id, $stsem); + if(isError($resultNote)) { + $this->terminateWithError(getError($resultNote)); + } else { + $data = getData($resultNote); + if($data != $note) + { + $note = $data; + } + } + + } + + // TODO: more sophisticated empty check + if($note=='') { + $this->load->model('education/Note_model', 'NoteModel'); + $result = $this->NoteModel->getNochNichtEingetragenNote(); + $note = getData($result)[0]->note; + } + + + $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); + } + + + $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]; + } + + $this->logLib->logInfoDB(array('saveStudentPruefung insert lvnote', $res, array( + 'insertvon' => getAuthUID(), + 'insertamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId(), $student_uid, $lva_id, $stsem, $note,$punkte, $jetzt)); + + } + 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]; + } + + $this->logLib->logInfoDB(array('saveStudentPruefung update lvnote', $res, array( + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId(), $student_uid, $lva_id, $stsem, $note,$punkte, $jetzt)); + } + + // save pruefung after updating lvnote, since pruefungspunkte get loaded by lv punkte + $pruefungenChanged = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum); + + $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) + { + + // extra check if the student has lvnote and a zeugnisnote in the relevant lva + $resultLV = $this->LvgesamtnoteModel->getLvGesamtNoten($lva_id, $student_uid, $stsem); + $lvgesamtnoteData = getData($resultLV); + $this->addMeta('lvgesamtnoteData', $lvgesamtnoteData); + + // allocating pruefungen before lv note is forbidden + if($lvgesamtnoteData == null) return $this->p->t('benotungstool', 'c4keineLvNoteEingetragen'); + + $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) { + $this->load->model('education/Note_model', 'NoteModel'); + $result = $this->NoteModel->getEntschuldigtNote(); + $note = getData($result)[0]->note; + } + + $jetzt = date("Y-m-d H:i:s"); + + $pruefungenChanged = []; + + 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->getLvGesamtNoteVorschlag($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" + { + $this->load->model('education/Note_model', 'NoteModel'); + $result = $this->NoteModel->getNochNichtEingetragenNote(); + $pr_note = getData($result)[0]->note; + $pr_punkte = ''; + $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); + } + + $this->logLib->logInfoDB(array('termin1 created',$res, getAuthUID(), getAuthPersonId())); + + } + + + + + // 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); + } + + $this->logLib->logInfoDB(array('termin2 updated',$res, getAuthUID(), getAuthPersonId())); + + + } 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' => $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); + } + + $this->logLib->logInfoDB(array('termin2 inserted',$res, getAuthUID(), getAuthPersonId())); + + } + + } 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); + } + + $this->logLib->logInfoDB(array('termin3 updated',$res, getAuthUID(), getAuthPersonId())); + + } 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' => $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); + } + + $this->logLib->logInfoDB(array('termin3 inserted',$res, getAuthUID(), getAuthPersonId())); + + } + } 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; + $punkte = $result->punkte; + + $result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $student_uid, $sem_kurzbz); + + $this->addMeta('LvgesamtnoteModelresult', $result); + + 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' => 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]; + } + + $this->logLib->logInfoDB(array('saveNotenvorschlag update lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); + + } 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' => $punkte, + '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->logLib->logInfoDB(array('saveNotenvorschlag insert lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); + } + + $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->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz); + $this->addMeta($note->uid.'$result', $result); + + if(CIS_GESAMTNOTE_PUNKTE) { + $resultNote = $this->NotenschluesselaufteilungModel->getNote($note->punkte, $lv_id, $sem_kurzbz); + $note->note = $this->getDataOrTerminateWithError($resultNote); + $this->addMeta($note->uid.'note', $note); + } + + 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' => $note->punkte, + '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]; + } + + $this->logLib->logInfoDB(array('saveNotenvorschlagBulk update lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); + + } 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' => $note->punkte, + '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->logLib->logInfoDB(array('saveNotenvorschlagBulk insert lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); + } + + $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 = []; + + $this->load->model('education/Note_model', 'NoteModel'); + $result = $this->NoteModel->getNochNichtEingetragenNote(); + $note = getData($result)[0]->note; + + foreach ($uids as $student) { + $student_uid = $student->uid; + $typ = $student->typ; + $punkte = null; // new pruefungen never have punkte, + + $lehreinheit_id = $student->lehreinheit_id; + $ret[$student->uid] = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum); + } + + $this->logLib->logInfoDB(array('createPruefungen',$ret, getAuthUID(), getAuthPersonId())); + + $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) { + + if(CIS_GESAMTNOTE_PUNKTE) { + $result = $this->NotenschluesselaufteilungModel->getNote($pruefung->punkte, $lv_id, $sem_kurzbz); + $this->addMeta($pruefung->uid."result", $result); + $pruefung->note = $this->getDataOrTerminateWithError($result); + $this->addMeta($pruefung->uid."note", $pruefung->note); + } + + $student_uid = $pruefung->uid; + $typ = $pruefung->typ; + $note = $pruefung->note; // TODO: parameterize for import maybe + $datum = $pruefung->datum; + $punkte = $pruefung->punkte; + + $lehreinheit_id = $pruefung->lehreinheit_id; + $ret[$student_uid] = $this->savePruefungstermin($typ, $student_uid, $lv_id, $sem_kurzbz, $lehreinheit_id, $note, $punkte, $datum); + } + + $this->logLib->logInfoDB(array('savePruefungenBulk',$ret, getAuthUID(), getAuthPersonId())); + + $this->terminateWithSuccess($ret); + } + + private function getAnwesenheiten($prestudent_ids, $lv_id, $sem_kurzbz) { + + $anwesenheiten = []; + try { + $downloadFunc = function ($anwesenheitenResult) use (&$anwesenheiten) { + // map result rows by prestudent_uid to retrieve them by that key later on + foreach ($anwesenheitenResult as $anw) { + $anwesenheiten[$anw->prestudent_id] = $anw->sum; + } + }; + + Events::trigger( + 'getAnwesenheitenForLvAndSemester', + $prestudent_ids, + $lv_id, + $sem_kurzbz, + $downloadFunc + ); + } catch (Throwable $t) { + $this->addMeta('getAnwesenheitenForLvAndSemester', $t->getMessage()); + } + + return $anwesenheiten; + + } + + public function getNoteByPunkte() { + $result = $this->getPostJSON(); + + if(!property_exists($result, 'punkte') + || !property_exists($result, 'lv_id') + || !property_exists($result, 'sem_kurzbz')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $punkte = $result->punkte; + $lv_id = $result->lv_id; + $sem_kurzbz = $result->sem_kurzbz; + + $result = $this->NotenschluesselaufteilungModel->getNote($punkte, $lv_id, $sem_kurzbz); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + + } + +} + 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/codex/Mobilitaet_model.php b/application/models/codex/Mobilitaet_model.php index 13f966d50..107c1ebb1 100644 --- a/application/models/codex/Mobilitaet_model.php +++ b/application/models/codex/Mobilitaet_model.php @@ -11,4 +11,73 @@ class Mobilitaet_model extends DB_Model $this->dbTable = 'bis.tbl_mobilitaet'; $this->pk = 'mobilitaet_id'; } + + public function getMobilityZusatzForUids($uids) { + $qry = "SELECT distinct on(nachname, vorname, public.tbl_benutzer.person_id) uid, + tbl_mitarbeiter.mitarbeiter_uid, + tbl_note.lkt_ueberschreibbar, tbl_note.anmerkung, + tbl_mobilitaet.mobilitaetstyp_kurzbz, + (CASE WHEN bis.tbl_mobilitaet.studiensemester_kurzbz = vw_student_lehrveranstaltung.studiensemester_kurzbz THEN 1 ELSE 0 END) as doubledegree, + public.tbl_prestudent.gsstudientyp_kurzbz as ddtype, + (SELECT status_kurzbz FROM public.tbl_prestudentstatus + WHERE prestudent_id=tbl_student.prestudent_id + ORDER BY datum DESC, insertamum DESC, ext_id DESC LIMIT 1) as studienstatus + FROM + campus.vw_student_lehrveranstaltung + JOIN public.tbl_benutzer USING(uid) + JOIN public.tbl_person USING(person_id) + LEFT JOIN public.tbl_student ON(uid=student_uid) + LEFT JOIN public.tbl_mitarbeiter ON(uid=mitarbeiter_uid) + LEFT JOIN public.tbl_studentlehrverband USING(student_uid,studiensemester_kurzbz) + LEFT JOIN lehre.tbl_zeugnisnote on(vw_student_lehrveranstaltung.lehrveranstaltung_id=tbl_zeugnisnote.lehrveranstaltung_id + AND tbl_zeugnisnote.student_uid=tbl_student.student_uid + AND tbl_zeugnisnote.studiensemester_kurzbz=tbl_studentlehrverband.studiensemester_kurzbz) + LEFT JOIN lehre.tbl_note USING (note) + LEFT JOIN bis.tbl_bisio ON(uid=tbl_bisio.student_uid) + LEFT JOIN public.tbl_studiengang ON(tbl_student.studiengang_kz=tbl_studiengang.studiengang_kz) + LEFT JOIN bis.tbl_mobilitaet USING(prestudent_id) + LEFT JOIN public.tbl_prestudent USING(prestudent_id) + WHERE uid IN ?"; + + return $this->execReadOnlyQuery($qry, [$uids]); + } + + public function formatZusatz($entry, $erhalter_kz) { + $zusatz = ''; + + if (isset($entry->studienstatus) && $entry->studienstatus === 'Incoming') { + $zusatz = '(i)'; + } + + if (isset($entry->lkt_ueberschreibbar) && $entry->lkt_ueberschreibbar === false) { + $zusatz .= ' (' . ($entry->anmerkung ?? '') . ')'; + } + + if (isset($entry->mitarbeiter_uid) && $entry->mitarbeiter_uid !== null) { + $zusatz .= ' (ma)'; + } + + if (isset($entry->stg_kz_student) && $entry->stg_kz_student == $erhalter_kz) { + $zusatz .= ' (a.o.)'; + } + + if ( + isset($entry->mobilitaetstyp_kurzbz) && $entry->mobilitaetstyp_kurzbz && + isset($entry->doubledegree) && $entry->doubledegree === 1 + ) { + $zusatz .= ' (d.d.'; + + $ddtype = $entry->ddtype ?? null; + + if ($ddtype == 'Intern') { + $zusatz .= 'i.)'; + } elseif ($ddtype == 'Extern') { + $zusatz .= 'o.)'; + } else { + $zusatz .= ')'; + } + } + + return $zusatz; + } } diff --git a/application/models/education/LePruefung_model.php b/application/models/education/LePruefung_model.php index 6e51f1975..35cedc324 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 lehre.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 lehre.tbl_pruefung.lehreinheit_id=tbl_lehreinheit.lehreinheit_id + AND tbl_lehreinheit.lehrveranstaltung_id=tbl_lehrveranstaltung.lehrveranstaltung_id + AND lehre.tbl_pruefung.note = tbl_note.note + AND lehre.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 2f955c295..fed5b9d23 100644 --- a/application/models/education/Lehreinheit_model.php +++ b/application/models/education/Lehreinheit_model.php @@ -739,4 +739,26 @@ EOSQL; )"; } + + 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]); + } } diff --git a/application/models/education/Lehrveranstaltung_model.php b/application/models/education/Lehrveranstaltung_model.php index 5422c290e..f6b54098b 100644 --- a/application/models/education/Lehrveranstaltung_model.php +++ b/application/models/education/Lehrveranstaltung_model.php @@ -317,7 +317,7 @@ 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) @@ -1346,4 +1346,24 @@ 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)); + } } diff --git a/application/models/education/Lvgesamtnote_model.php b/application/models/education/Lvgesamtnote_model.php index c30045ff0..44645e75b 100644 --- a/application/models/education/Lvgesamtnote_model.php +++ b/application/models/education/Lvgesamtnote_model.php @@ -14,7 +14,7 @@ class Lvgesamtnote_model extends DB_Model } /** - * Laedt die Noten + * Laedt die Noten - lvgesamtnote (Vorschlag) JOIN tbl.note (zeugnisnote) * * @param integer $lehrveranstaltung_id * @param string $student_uid @@ -46,4 +46,19 @@ class Lvgesamtnote_model extends DB_Model return $this->loadWhere($where); } + + public function getLvGesamtNoteVorschlag($lehrveranstaltung_id, $student_uid, $studiensemester_kurzbz) + { + $qry = "SELECT * FROM campus.tbl_lvgesamtnote + WHERE campus.tbl_lvgesamtnote.student_uid = ? + AND campus.tbl_lvgesamtnote.studiensemester_kurzbz = ?"; + $params = [$student_uid, $studiensemester_kurzbz]; + + if ($lehrveranstaltung_id) { + $qry .= "AND campus.tbl_lvgesamtnote.lehrveranstaltung_id = ?"; + $params[] = $lehrveranstaltung_id; + } + + return $this->execReadOnlyQuery($qry, $params); + } } diff --git a/application/models/education/Note_model.php b/application/models/education/Note_model.php index 87a1501e0..e92bb8452 100644 --- a/application/models/education/Note_model.php +++ b/application/models/education/Note_model.php @@ -11,12 +11,33 @@ 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); + } + + // used to determine the primary key of note "entschuldigt" to avoid hardcoded magic numbers + // that might differ in a different installation of fhcomplete + public function getEntschuldigtNote() { + $qry ="SELECT * + FROM lehre.tbl_note + WHERE bezeichnung = 'entschuldigt'"; return $this->execReadOnlyQuery($qry); } + + // used to determine the primary key of note "noch nicht eingetragen" to avoid hardcoded magic numbers + // that might differ in a different installation of fhcomplete + public function getNochNichtEingetragenNote() { + $qry ="SELECT * + FROM lehre.tbl_note + WHERE bezeichnung = 'Noch nicht eingetragen'"; + + return $this->execReadOnlyQuery($qry); + } + } \ No newline at end of file diff --git a/application/models/education/Notenschluesselaufteilung_model.php b/application/models/education/Notenschluesselaufteilung_model.php index d48e16b0b..f9031de47 100644 --- a/application/models/education/Notenschluesselaufteilung_model.php +++ b/application/models/education/Notenschluesselaufteilung_model.php @@ -26,6 +26,9 @@ class Notenschluesselaufteilung_model extends DB_Model $this->load->model('education/Notenschluesselzuordnung_model', 'NotenschluesselzuordnungModel'); $notenschluessel_kurzbz = $this->NotenschluesselzuordnungModel->getKurzbzForLv($lehrveranstaltung_id, $studiensemester_kurzbz); + if($notenschluessel_kurzbz == null) + return success(null); + $this->addSelect("note"); $this->addOrder("punkte", "DESC"); $this->addLimit(1); 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 4a5f87832..66c23de0a 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 c51ca40c3..e0b0e2bb3 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', @@ -23,7 +24,8 @@ $includesArray = array( 'public/css/components/FormUnderline.css', 'public/css/components/abgabetool/abgabe.css', 'public/css/Cis4/Cms.css', - 'public/css/Cis4/Studium.css' + 'public/css/Cis4/Studium.css', + 'public/css/Cis4/Benotungstool.css' ), 'customJSs' => array( 'vendor/npm-asset/primevue/accordion/accordion.min.js', @@ -37,11 +39,15 @@ $includesArray = array( 'vendor/npm-asset/primevue/timeline/timeline.min.js', 'vendor/npm-asset/primevue/inplace/inplace.min.js', 'vendor/npm-asset/primevue/message/message.min.js', + 'vendor/npm-asset/primevue/divider/divider.min.js', + 'vendor/npm-asset/primevue/password/password.js', + 'vendor/npm-asset/primevue/multiselect/multiselect.js', 'vendor/npm-asset/primevue/tieredmenu/tieredmenu.js', 'vendor/moment/luxonjs/luxon.min.js' ), 'customJSModules' => array( 'public/js/apps/Cis/Cis.js', + 'vendor/olifolkerd/tabulator5/src/js/modules/ColumnCalcs/ColumnCalcs.js' ), ); diff --git a/public/css/Cis4/Benotungstool.css b/public/css/Cis4/Benotungstool.css new file mode 100644 index 000000000..dc11d81a7 --- /dev/null +++ b/public/css/Cis4/Benotungstool.css @@ -0,0 +1,47 @@ +/* 1. Stick the Header */ +#notentable .tabulator-header .tabulator-col.sticky-col { + position: sticky; + left: 0; + z-index: 10; /* Must be higher than other headers */ + background-color: #fff; /* Opaque background is required */ + border-right: 2px solid #ddd; /* Optional: Separator line */ +} + +/* 2. Stick the Data Cells */ +#notentable .tabulator-tableholder .tabulator-row .tabulator-cell.sticky-col { + position: sticky; + left: 0; + z-index: 10; /* Ensure it floats above other cells */ + background-color: #fff; /* Match your row background color */ + border-right: 2px solid #ddd; /* Optional: Separator line */ +} + +/* 3. Fix for Hover Effects (Optional) */ +/* If you use hover rows, you need to ensure the sticky cell matches the hover color */ +#notentable .tabulator-row:hover .tabulator-cell.sticky-col { + background-color: #ccc; /* Match your existing hover color */ +} + + +/* styling for points input column for notenvorschläge in benotungstool*/ +#notentable .tabulator-tableholder .tabulator-editable[tabulator-field="punkte"] { + position: relative; + background-color: rgba(255, 255, 157, 0.73); +} + +/* styling for editable dropdown column of notenvorschläge in benotungstool*/ +#notentable .tabulator-tableholder .tabulator-editable[tabulator-field="note_vorschlag"] { + position: relative; + background-color: rgba(255, 255, 157, 0.73); + cursor: pointer; +} + +#notentable .tabulator-tableholder .tabulator-editable[tabulator-field="note_vorschlag"]::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 4f32a1e55..d6f301200 100644 --- a/public/js/api/factory/lehre.js +++ b/public/js/api/factory/lehre.js @@ -41,5 +41,19 @@ export default { method: 'get', url: `/api/frontend/v1/Lehre/semesterAverageGrade/${semester}` } + }, + 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..701aed199 --- /dev/null +++ b/public/js/api/factory/noten.js @@ -0,0 +1,87 @@ +/** + * 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 { + getCisConfig(){ + return { + method: 'get', + url: '/api/frontend/v1/Noten/getCisConfig' + }; + }, + 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, punkte = null) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/saveNotenvorschlag', + params: { lv_id, sem_kurzbz, student_uid, note, punkte } + }; + }, + 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 } + }; + }, + getNoteByPunkte(punkte, lv_id, sem_kurzbz) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/getNoteByPunkte', + params: { punkte, lv_id, sem_kurzbz } + }; + } +} \ No newline at end of file diff --git a/public/js/api/factory/studiensemester.js b/public/js/api/factory/studiensemester.js index 7fe4a22d3..9cf4e82d0 100644 --- a/public/js/api/factory/studiensemester.js +++ b/public/js/api/factory/studiensemester.js @@ -29,5 +29,11 @@ export default { url: 'api/frontend/v1/organisation/studiensemester/getAll', params: { order, start } }; - } + }, + getStudiensemester() { + return { + method: 'get', + url: '/api/frontend/v1/Studiensemester/getStudiensemester' + }; + }, }; \ No newline at end of file diff --git a/public/js/apps/Cis/Cis.js b/public/js/apps/Cis/Cis.js index ee373886f..1ab87090d 100644 --- a/public/js/apps/Cis/Cis.js +++ b/public/js/apps/Cis/Cis.js @@ -20,6 +20,7 @@ import Studium from "../../components/Cis/Studium/Studium.js"; import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js"; import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js"; import PaabgabeUebersicht from "../../components/Cis/ProjektabgabeUebersicht/ProjektabgabeUebersicht.js"; +import Benotungstool from "../../components/Cis/Benotungstool/Benotungstool.js"; import ApiRouteInfo from '../../api/factory/routeinfo.js'; import {capitalize} from "../../helpers/StringHelpers.js"; @@ -84,6 +85,12 @@ const router = VueRouter.createRouter({ component: PaabgabeUebersicht, 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", @@ -159,7 +166,7 @@ const router = VueRouter.createRouter({ path: `/Cis/MyLv/:studiensemester?`, name: 'MyLv', component: MylvStudent, - props: true, + props: true }, { path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`, diff --git a/public/js/components/Bootstrap/Offcanvas.js b/public/js/components/Bootstrap/Offcanvas.js index aab2c55f5..461d80821 100644 --- a/public/js/components/Bootstrap/Offcanvas.js +++ b/public/js/components/Bootstrap/Offcanvas.js @@ -148,4 +148,4 @@ export default { ` -} +} \ No newline at end of file diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 0d6ec1ab0..d65f7222b 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -522,41 +522,20 @@ export const AbgabetoolAssistenz = { const table = this.$refs.abgabeTable.tabulator this.tableBuiltResolve() - - table.on("columnMoved", () => { - this.saveState(table); - }); - - table.on("columnResized", () => { - this.saveState(table); - }); - - table.on("columnVisibilityChanged", () => { - this.saveState(table); - }); - - table.on("filterChanged", () => { - this.saveState(table); - }); - - table.on("headerFilterChanged", () => { - this.saveState(table); - }); - - table.on("dataSorted", () => { - this.saveState(table); - }); - - table.on("columnSorted", () => { - this.saveState(table); - }); - - table.on("sortersChanged", () => { - this.saveState(table); - }); const saved = this.loadState(); + // setup change eventlisteners + const events = [ + "columnMoved", "columnResized", "columnVisibilityChanged", + "filterChanged", "headerFilterChanged", "dataSorted", + "columnSorted", "sortersChanged" + ]; + + events.forEach(eventName => { + table.on(eventName, () => this.saveState(table)); + }); + table.on("renderComplete", () => { if(!this.stateRestored) { @@ -999,7 +978,6 @@ export const AbgabetoolAssistenz = { this.tableData = this.mapProjekteToTableData(this.projektarbeiten) await this.tableBuiltPromise - this.$refs.abgabeTable.tabulator.setData(this.tableData); }, loadProjektarbeiten(all = false, callback) { diff --git a/public/js/components/Cis/Benotungstool/Benotungstool.js b/public/js/components/Cis/Benotungstool/Benotungstool.js new file mode 100644 index 000000000..3e3374164 --- /dev/null +++ b/public/js/components/Cis/Benotungstool/Benotungstool.js @@ -0,0 +1,2347 @@ +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 BsOffcanvas from '../../Bootstrap/Offcanvas.js'; +import VueDatePicker from '../../vueDatepicker.js.php'; +import LehreinheitenModule from '../../DropdownModes/LehreinheitenModule'; +import MobilityLegende from '../../Mobility/Legende.js'; +import FhcOverlay from "../../Overlay/FhcOverlay.js"; +import {debounce} from "../../../helpers/debounce.js"; +import {centeredFormatter} from "../../../tabulator/formatter/centered.js"; + +export const Benotungstool = { + name: "Benotungstool", + components: { + BsModal, + BsOffcanvas, + CoreFilterCmpt, + MobilityLegende, + Dropdown: primevue.dropdown, + Divider: primevue.divider, + InputNumber: primevue.inputnumber, + Password: primevue.password, + Textarea: primevue.textarea, + Datepicker: VueDatePicker, + Multiselect: primevue.multiselect, + FhcOverlay + }, + 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 { + headerFiltersRestored: false, + filtersRestored: false, + colLayoutRestored: false, + sortRestored: false, + stateRestored: false, + persistenceID: 'notenToolTable2026-02-16', + debouncedFetchPunkteForPruefung: null, + config: null, // cis config + neuesPruefungsdatumModalVisible: false, + loading: false, + selectedUids: [], // shared selection state + selectedLehreinheit: null, + tabulatorCanBeBuilt: false, + selectedPruefungNote: null, + selectedPruefungDate: new Date(), // v-model for pruefung edit datepicker + selectedPruefungPunkte: null, + 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: "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 + } + } + }, + { + event: "cellClick", + handler: async (e, cell) => { + const field = cell.getField() + + if(field == "mobility_zusatz") { + this.$refs.drawer.show() + e.stopPropagation() + this.undoSelection(cell) + } else if (field == "punkte" || field == "note_vorschlag" || field == "übernehmen") { + this.undoSelection(cell) + } + } + } + ]}; + }, + methods: { + loadState() { + return JSON.parse(localStorage.getItem(this.persistenceID) || "null"); + }, + saveState(table) { + // Only save if we have finished the initial restoration + // AND the table actually has columns (to avoid saving empty states) + if (!this.stateRestored) return; + + const rawLayout = table.getColumnLayout(); + const filteredLayout = rawLayout.filter(col => { + if(this.notenTableOptions.columns.some(colDef => colDef.field === col.field)) return col + return null + }) + + // TODO: if dynamic cols have sort/filter/headerfilter functionality filter them here before persisting + // into local storage + const rawSorters = table.getSorters() + + const rawFilters = table.getFilters() + + const rawHeaderFilters = table.getHeaderFilters() + const state = { + columns: filteredLayout.map(col => ({ + field: col.field, + visible: col.visible, + width: col.width, + })), + sort: rawSorters.map(s => ({ + field: s.field, + dir: s.dir, + })), + filters: rawFilters, + headerFilters: rawHeaderFilters + }; + + localStorage.setItem(this.persistenceID, JSON.stringify(state)); + }, + handleTableBuilt() { + const table = this.$refs.notenTable.tabulator; + + this.tableBuiltResolve() + + const saved = this.loadState(); + + // setup change eventlisteners + const events = [ + "columnMoved", "columnResized", "columnVisibilityChanged", + "filterChanged", "headerFilterChanged", "dataSorted", + "columnSorted", "sortersChanged" + ]; + + events.forEach(eventName => { + table.on(eventName, () => this.saveState(table)); + }); + + // renderComplete restore state logic + table.on("renderComplete", () => { + if (this.stateRestored) return; + + // layout restore should be happening in setupData() + + if (saved?.filters && !this.filtersRestored) { + this.filtersRestored = true; + table.setFilter(saved.filters); + } + + if (saved?.headerFilters && !this.headerFiltersRestored) { + this.headerFiltersRestored = true; + saved.headerFilters.forEach(hf => { + table.setHeaderFilterValue(hf.field, hf.value); + }); + } + + if (saved?.sort?.length && !this.sortRestored) { + this.sortRestored = true; + setTimeout(() => { + const sortList = saved.sort.map(s => { + const col = table.columnManager.findColumn(s.field); + return col ? { column: col, dir: s.dir } : null; + }).filter(Boolean); + + if (sortList.length) { + table.setSort(sortList); + } + }, 100); + } + + this.stateRestored = true; + }); + + // finalize the promise + if (this.tableResolve) this.tableResolve(); + }, + undoSelection(cell) { + // checks if cells row is selected and unselects -> imitates columns which dont trigger row selection + // but actually just revert it after the fact + + const row = cell.getRow() + if(row.isSelected()) { + row.deselect(); + } + }, + // using this to expose input event of editor element properly, tabulator makes it hard to access on default editor + // implemented after tabulator/src/js/modules/edit/defaults/editors/number.js + liveNumberEditor(cell, onRendered, success, cancel) { + const editor = document.createElement("input"); + editor.setAttribute("type", "number"); + editor.value = cell.getValue(); + + const row = cell.getRow() + const rowData = row.getData() + + rowData._debouncedFetchNoteForPunkte = debounce(this.fetchNoteForPunkte, 500) + editor.addEventListener("input", (e) => { + rowData._debouncedFetchNoteForPunkte(e.target.value, row) + }); + + onRendered(() => { + editor.focus(); + editor.style.height = "100%"; + }); + + editor.addEventListener("change", () => success(editor.value)); + editor.addEventListener("blur", () => success(editor.value)); + editor.addEventListener("keydown", (e) => { + if (e.keyCode === 13) success(editor.value); + if (e.keyCode === 27) cancel(); + }); + + return editor; + }, + fetchNoteForPunkte(valueParam, row) { + const value = valueParam == '' ? null : valueParam + this.$api.call(ApiNoten.getNoteByPunkte(value, this.lv_id, this.sem_kurzbz)).then(res => { + if(res?.meta?.status === 'success' && res.data >= 0) { + row.update({note_vorschlag: res.data}) + row.reformat() + } + }) + }, + fetchNoteForPunktePruefung(event) { + const value = event.value == '' ? null : event.value + this.$api.call(ApiNoten.getNoteByPunkte(value, this.lv_id, this.sem_kurzbz)).then(res => { + if(res?.meta?.status === 'success' && res.data >= 0) { + this.selectedPruefungNote = this.notenOptions.find(n => n.note == res.data) + } + }) + }, + 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) + + // if a student doesnt have a lv_note entered definitely dont allow to create pruefung + if(!student.lv_note) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat noch keine LV-Note eingetragen! Es wird keine Prüfung angelegt.') + return + } + + if(!student.note) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat noch keine Zeugnis Note eingetragen! Es wird keine Prüfung angelegt.') + return + } + + // check if student antrittCount is too high already + if(student.hoechsterAntritt >= this.maxAntrittCount) { + 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]) + const idTrimmed = rowParts[0].trim() + let student = null + + if(id === 'matrikelnr') { // find student by matrnr and use uid later on + student = this.studenten.find(s => s.matrikelnr?.trim() === idTrimmed) + } else if(id === 'uid') { + student = this.studenten.find(s => s.uid?.trim() === idTrimmed) + } + if(!student) { + this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importNoStudentFoundForIdInRow', [rowParts[0], rowNum])) + return + } + + let punkte = null + let note = null + if(this.config?.CIS_GESAMTNOTE_PUNKTE) { + punkte = Number.parseFloat(rowParts[1]) + } else { + 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(this.$p.t('benotungstool/c4importNoGradeFoundForIdInRow', [rowParts[0], rowNum])) + return + } + } + + notenbulk.push({uid: student.uid, note, punkte}) + }, + parsePruefung(rowParts, pruefungbulk, rowNum) { + const id = this.identifyUid(rowParts[0]) + const idTrimmed = rowParts[0].trim() + let student = null + if(id === 'matrikelnr') { // find student by matrnr and use uid later on + student = this.studenten.find(s => s.matrikelnr?.trim() === idTrimmed) + } else if(id === 'uid') { + student = this.studenten.find(s => s.uid?.trim() === idTrimmed) + } + if(!student) { + this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importNoStudentFoundForIdInRow', [rowParts[0], rowNum])) + return + } + + const datum = rowParts[1] // should be in 'dd.MM.yyyy' + if(!this.isValidDate_ddmmyyyy(datum)) { + this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importInvalidDateFoundForIdInRow', [rowParts[0], rowNum])) + 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) + + + let punkte = null + let note = null + if(this.config?.CIS_GESAMTNOTE_PUNKTE) { + punkte = Number.parseFloat(rowParts[1]) + } else { + 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(this.$p.t('benotungstool/c4importNoGradeFoundForIdInRow', [rowParts[0], rowNum])) + return + } + } + + const typ = this.getPruefungstypForStudentByAntritt(student) + + pruefungbulk.push({uid: student.uid, datum: dateStr, note, punkte, 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.alertDefault( + 'success', + 'Info', + this.$capitalize(this.$p.t('benotungstool/notenImportSuccessAlert')), + true + ) + 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 + if(this.config?.CIS_GESAMTNOTE_PUNKTE) { + s.punkte = lvn.punkte + } + + 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 = true + this.$api.call(ApiNoten.saveStudentPruefungBulk(this.lv_id, this.sem_kurzbz, pruefungenbulk)) + .then((res)=> { + if(res.meta.status === 'success') { + this.$fhcAlert.alertDefault( + 'success', + 'Info', + this.$capitalize(this.$p.t('benotungstool/pruefungImportSuccessAlert')), + true + ) + this.handleAddNewPruefungenResponse(res, pruefungenbulk) + } + }).finally(()=>{this.loading = false}) + }, + handleAddNewPruefungenResponse(res, uids) { + // in case we reload when changing lva_id or stsem to always consider local storage layout + this.colLayoutRestored = false; + + 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) { + student["Termin1"] = extra + student.pruefungen.push(extra) // push to pruefungen array for antritt evaluation + } + + this.correctOldTerminTypenForStudent(student, saved) + + 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) + this.recalculateSelectable(student) + this.reformatStudentRow(student) + }) + + // add col to table + const cols = [...this.notenTableOptions.columns.slice(0, -1)]; + let kommCol = null + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1]; + + if(this.distinctPruefungsDates.length) { + cols.push({ + title: this.$capitalize(this.$p.t('benotungstool/c4originalZnote')), + field: "Termin1", + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 200, + width: 250, + visible: true, + tooltip: false + }) + } + + this.distinctPruefungsDates.forEach((date)=>{ + const dateparts = date.split('-') + const titledate = `${dateparts[2]}.${dateparts[1]}.${dateparts[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, + width: 250, + visible: true, + tooltip: false + }) + }) + + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) cols.push(kommCol) // keep kommPruef Col as last + // redraw table + + + // preemptively do the persistence part of columns already here so we get the merged definition before + // tabulator internal go to war with vue reactives + let colsUsed = null + const saved = this.loadState(); + if (saved && saved.columns) { + // new columns map for lookup + const colMap = new Map(cols.map(c => [c.field, c])); + + const restoredCols = []; + + // add columns in the SAVED order, applying saved width/visibility + saved.columns.forEach(savedCol => { + const originalDef = colMap.get(savedCol.field); + if (originalDef) { + restoredCols.push({ + ...originalDef, // Keep formatters, calcs, etc. + width: savedCol.width, + visible: savedCol.visible + }); + colMap.delete(savedCol.field); // Remove so we don't double-add + } + }); + + // append any NEW columns aka pruefungs cols + colMap.forEach((def) => { + restoredCols.push(def); + }); + + colsUsed = restoredCols; + this.colLayoutRestored = true; + } + + this.loading = false + + const colsFinal = colsUsed ?? cols + this.$refs.notenTable.tabulator.setColumns(colsFinal) + this.$refs.notenTable.tabulator.setData(this.studenten); + this.$refs.notenTable.tabulator.redraw(true); + }, + reformatStudentRow(student) { + const table = this.$refs.notenTable.tabulator + if(!table) return + + const row = table.rowManager.getRowFromDataObject(student) + + const rowComponent = row.getComponent() + rowComponent.reformat() + }, + correctOldTerminTypenForStudent(student, saved) { + // check if student has a preceding pruefung from same type and remove it since in this case + // the new pruefung will have overwritten the old one + const oldP = student.pruefungen.find(p => p.pruefungstyp_kurzbz == saved.pruefungstyp_kurzbz) + if(oldP) { + delete student[oldP.datum] // delete the variable col value + + const pIndex = student.pruefungen.indexOf(oldP) + if (pIndex > -1) { + student.pruefungen.splice(pIndex, 1) + } + // student.pruefungen.splice(student.pruefungen.indexOf(oldP), 1) // delete from pruefungen array + + // check if on that date any other student has a pruefung, if not delete it from distinctPruefungsDates + const other = this.studenten.find(s => s[oldP.datum] !== undefined) + if(!other) { + const dateIndex = this.distinctPruefungsDates.indexOf(oldP.datum); + if (dateIndex > -1) { + this.distinctPruefungsDates.splice(dateIndex, 1); + } + // this.distinctPruefungsDates.splice(this.distinctPruefungsDates.findIndex(oldP.datum), 1) + } + } + }, + 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 { + height: 700, + virtualDom: true, + virtualDomBuffer: 5000, + index: 'uid', + layout: 'fitDataStretch', + placeholder: this.$capitalize(this.$p.t('global/noDataAvailable')), + selectable: true, + selectableRangeMode: "click", // shift+click + selectablePersistence: false, // reset selection on table reload + selectableCheck: this.selectableCheck, + rowFormatter: this.fixTabulatorSelectionFormatter, + columns: this.getColumnsDefinition(), + persistence: false, + } + + }, + selectableCheck(row, e) { + const data = row.getData(); + + if(data['kommPruef']) return false + else if(data.hoechsterAntritt >= this.maxAntrittCount) return false // 2 or 3 pruefungen counted + + return true; // student can be selected to add pruefung + + }, + getColumnsDefinition() { + const columns = [] + + + columns.push({ + 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, + cssClass: 'sticky-col', + field: 'selectCol', + title: '' + }) + columns.push({title: 'UID', field: 'uid', tooltip: false, widthGrow: 1, topCalc: this.sumCalcFunc, formatter: centeredFormatter, cssClass: 'sticky-col'}) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mail'))), field: 'email', formatter: this.mailFormatter, tooltip: false, visible: false, widthGrow: 1, variableHeight: true}) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4antrittCountv2'))), field: 'hoechsterAntritt', formatter: centeredFormatter, tooltip: false, widthGrow: 1}) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4vorname'))), field: 'vorname', formatter: centeredFormatter, headerFilter: true, tooltip: false, widthGrow: 1}) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4nachname'))), field: 'nachname', formatter: centeredFormatter, headerFilter: true, widthGrow: 1}) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4anwesenheitsquote'))), field: 'anwquote', widthGrow: 1, formatter: this.percentFormatter}) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mobility'))), field: 'mobility_zusatz', formatter: centeredFormatter, headerFilter: true, widthGrow: 1, visible: false}) + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE) { + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4teilnoten'))), field: 'teilnote', widthGrow: 1, formatter: this.teilnotenFormatter, variableHeight: true}) + } + if(this.config?.CIS_GESAMTNOTE_PUNKTE) { + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4punkte'))), field: 'punkte', widthGrow: 1, + editor: this.liveNumberEditor, + editable: (cell) => { + const rowData = cell.getRow().getData(); + if(this.pruefungen?.find(p => p.student_uid == rowData.uid)) return false + + return true + }, + variableHeight: true + }) + } + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4notenvorschlag'))), 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) => { + // punkte features enables mapping but unable to set note directly + if(this.config?.CIS_GESAMTNOTE_PUNKTE) return false + 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 + }) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4notenvorschlagUebernehmen'))), field: 'übernehmen', width: 150, hozAlign: 'center', formatter: this.arrowFormatter, + cellClick: this.saveNote, + variableHeight: true}) + columns.push({title: Vue.computed(() => this.$capitalize(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 + }) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4freigabe'))), field: 'freigegeben', widthGrow: 1, formatter: this.freigabeFormatter, variableHeight: true}) + columns.push({title: Vue.computed(() => this.$capitalize(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 + }) + columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4kommPruef'))), + field: 'kommPruef', widthGrow: 1, + formatter: this.pruefungFormatter, + sorter: this.pruefungSorter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", minWidth: 150, visible: false, + tooltip: false + }) + + return columns + }, + pruefungSorter(a, b, aRow, bRow, column, dir, params) { + if (a === null || typeof a === "undefined" || a === '') return -1; + if (b === null || typeof b === "undefined" || b === '') return 1; + + // sort by notenvalue since pruefungen are in same date by column + return a.note - b.note + }, + 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 >= this.maxAntrittCount + if(notSelectable) { + const el = row.getElement() + el.children[0]?.children[0]?.remove() + + el.classList.remove("tabulator-selectable"); + el.classList.add("tabulator-unselectable"); + } else { + const el = row.getElement() + + el.classList.add("tabulator-selectable"); + el.classList.remove("tabulator-unselectable"); + } + }, + terminCalcFunc(entries) { + return entries.reduce((acc, cur) => { + if(cur !== undefined) acc++ + return acc + }, 0) + }, + terminCalcFormatter(cell) { + const cellval = cell.getValue() + return this.$capitalize(this.$p.t('benotungstool/prueflingSelectionv2'))+': ' + cellval + }, + negativeNotenCalcFormatter(cell) { + const cellval = cell.getValue() + return this.$capitalize(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.$capitalize(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.$capitalize(this.$p.t('benotungstool/c4negativ'))) { + const valOpt = this.notenOptions.find(opt => opt.note == rowVal) + if(!valOpt) return false + return !valOpt.positiv + } + if(filterVal === this.$capitalize(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() + + let style = 'display: flex; justify-content: center; align-items: center; height: 100%;' + + 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 + + // if vorschlag is the same as lv_note do nothing + if(data.note_vorschlag == data.lv_note) return + + // if the student already has pruefungen disable this part + if(data.pruefungen.length) return + + this.loading = true + this.$api.call(ApiNoten.saveNotenvorschlag(this.lv_id, this.sem_kurzbz, data.uid, data.note_vorschlag, data.punkte)) + .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++; + } + }).finally(()=>this.loading = false) + + + }, + teilnotenFormatter(cell) { + const val = cell.getValue() + + let style = 'white-space: pre-line;' + + 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() + let studentPruefung = null + if(field === 'kommPruef') { studentPruefung = data['kommPruef'] } + else if(field === "Termin1") { studentPruefung = data['Termin1'] } + else { studentPruefung = data.pruefungen.find(p => p.datum === field) } + + // is this column/cell allowed to have an add pruefung action + const canAdd = field !== 'kommPruef' && field !== "Termin1" && data.hoechsterAntritt < this.maxAntrittCount + + // TODO: check for some time limit maybe? old pruefungen can be changed/created + + // 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.alignContent = '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]) { + + 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' || field === "Termin1") { + return rowDiv + } + + if(data[field]?.pruefungstyp_kurzbz !== 'Termin1') { + // Third column (button) + const button = document.createElement('button'); + button.className = 'btn btn-outline-secondary'; + button.textContent = this.$capitalize(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.$capitalize(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-content-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('-') + + const newDate = new Date(Number(pruefungDateParts[0]), Number(pruefungDateParts[1]) - 1, Number(pruefungDateParts[2])) + this.selectedPruefungDate = newDate + + + if(this.pruefung?.note) { + this.selectedPruefungNote = this.notenOptions.find(n => n.note == this.pruefung.note) + } else { + this.selectedPruefungNote = null + } + + this.selectedPruefungPunkte = this.pruefung?.punkte ?? null + + this.$refs.modalContainerPruefung.show() + }, + pruefungTitleFormatter(cell) { + const def = cell.getColumn().getDefinition() + return def.title; + }, + arrowFormatter(cell) { + const row = cell.getRow() + const data = row.getData() + + let style = 'display: flex; justify-content: center; align-items: center; height: 100%;' + + if(!data.note_vorschlag || (data.note_vorschlag == data.lv_note) || data.pruefungen.length) { + // arrow to ambiguous in meaning, use str8 forward worded button here instead + // uncolored arrow + // return '
' + + // '
' + + return '' + } + + const button = document.createElement('button'); + button.className = 'btn btn-outline-secondary'; + button.textContent = this.$capitalize(this.$p.t('benotungstool/c4notenvorschlagUebernehmen')); + return button; + + // // can save a notenvorschlag -> colored + // return '
' + + // '
' + }, + mailFormatter(cell) { + const val = cell.getValue() + + let style = 'display: flex; justify-content: center; align-items: center; height: 100%;' + + return '
' + + '
' + }, + percentFormatter(cell) { + const data = cell.getData() + const val = data.anwquote ?? '-' + return '
'+ val + ' %
' + }, + 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 + }, + async setupData(data){ + // in case we reload when changing lva_id or stsem to always consider local storage layout + this.colLayoutRestored = false; + + 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)]; + let kommCol = null + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) 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 + + if(p.pruefungstyp_kurzbz !== 'kommPruef' + && p.pruefungstyp_kurzbz !== 'Termin1' + && !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) + } + + }) + + 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) => { + // Termin1 aka originale Zeugnisnote should note create variable columns + if(p.pruefungstyp_kurzbz == "Termin1") { + s["Termin1"] = p + } else { + 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); + + s.punkte = this.teilnoten[s.uid].punkte_lv + + const grades = this.teilnoten[s.uid].grades + s.teilnote = '' + s.mobility_zusatz = this.teilnoten[s.uid].mobility_zusatz + 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 +''+ '
') + }) + + const vueThis = this + Object.defineProperty(s, 'selectable', { + get() { + const kP = s.pruefungen?.find(p => p.pruefungstyp_kurzbz == 'kommPruef') + const maxAntrittReached = s.hoechsterAntritt >= vueThis.maxAntrittCount + return !(kP || maxAntrittReached) + + }, + set() { + // empty setter so tabulator doesnt scream + }, + enumerable: true, + configurable: true + }) + + }) + + // if we have any pruefungen at all someone must have original note/termin1 pruefung + if(this.distinctPruefungsDates.length) { + cols.push({ + title: this.$capitalize(this.$p.t('benotungstool/c4originalZnote')), + field: "Termin1", + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + sorter: this.pruefungSorter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 200, + width: 250, + visible: true, + tooltip: false + }) + } + + this.distinctPruefungsDates.sort((d1, d2) => { + if(d1 > d2) { + return 1 + } else if (d1 < d2) { + return -1 + } else { + return 0 + } + }) + this.distinctPruefungsDates.forEach((date)=>{ + const dateparts = date.split('-') + const titledate = `${dateparts[2]}.${dateparts[1]}.${dateparts[0]}` + + cols.push({ + title: titledate, + field: date, + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + sorter: this.pruefungSorter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 200, + width: 250, + visible: true, + tooltip: false + }) + }) + + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) cols.push(kommCol) // keep kommPruef Col as last + + + // preemptively do the persistence part of columns already here so we get the merged definition before + // tabulator internal go to war with vue reactives + let colsUsed = null + const saved = this.loadState(); + if (saved && saved.columns) { + // new columns map for lookup + const colMap = new Map(cols.map(c => [c.field, c])); + + const restoredCols = []; + + // add columns in the SAVED order, applying saved width/visibility + saved.columns.forEach(savedCol => { + const originalDef = colMap.get(savedCol.field); + if (originalDef) { + restoredCols.push({ + ...originalDef, // Keep formatters, calcs, etc. + width: savedCol.width, + visible: savedCol.visible + }); + colMap.delete(savedCol.field); // Remove so we don't double-add + } + }); + + // append any NEW columns aka pruefungs cols + colMap.forEach((def) => { + restoredCols.push(def); + }); + + colsUsed = restoredCols; + this.colLayoutRestored = true; + } + + const colsFinal = colsUsed ?? cols + this.loading = false + + this.$refs.notenTable.tabulator.setColumns(colsFinal) + 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) + if(res?.meta?.getExternalGradesError) this.$fhcAlert.alertError(res.meta.getExternalGradesError) + }).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 - 50 + this.$refs.notenTable.tabulator.setHeight(this.notenTableOptions.height) + }, + async setupCreated() { + this.loading = true + + this.debouncedFetchPunkteForPruefung = debounce(this.fetchNoteForPunktePruefung, 500) + + // fetch cis config regarding gesamtnoteneingabe, needs to be fetched before setup can finish + const configPromise = this.$api.call(ApiNoten.getCisConfig()).then(res => { + this.config = res.data + }) + + // 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(async res => { + this.notenOptions = res.data + this.notenOptionsLehre = res.data.filter(n => n.lehre === true) + + await configPromise + 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.loadNoten(this.lv_id, this.sem_kurzbz) + this.calcMaxTableHeight() + + }, + 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 + } + }) + + this.loading = true + // 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) + }).finally( () => this.loading = false) + + }, + 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 + const punkte = this.selectedPruefungPunkte ?? 0 + this.$api.call(ApiNoten.saveStudentPruefung( + this.pruefungStudent.uid, + note, + 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.alertDefault( + 'success', + 'Info', + this.$capitalize(this.$p.t('benotungstool/pruefungSaveForUid', [this.pruefungStudent.uid])), + true + ) + 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 + const oldScrollLeft = this.$refs.notenTable.tabulator.rowManager.scrollLeft + const oldScrollTop = this.$refs.notenTable.tabulator.rowManager.scrollTop + + // 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) + this.recalculateSelectable(s) + this.reformatStudentRow(s) + } + + this.$refs.notenTable.tabulator.redraw(true) + Vue.nextTick(()=> { + const table = this.$refs.notenTable.tabulator.element.querySelector('.tabulator-tableholder') + if(table) { + table.scrollLeft = oldScrollLeft; + table.scrollTop = oldScrollTop; + } + }) + + } + }).finally(()=> { + this.pruefungStudent = null + this.pruefung = null + }) + + this.$refs.modalContainerPruefung.hide() + }, + handleAddNewTermin(data, student){ + this.colLayoutRestored = false; // always keep persistence options in mind when modifying table cols dynamically + + const savedPruefung = data[0] + const extra = data[2] + + // check for extra pruefung (termin1) to add before + if(extra) { + student["Termin1"] = extra + student.pruefungen.push(extra) // push to pruefungen array for antritt evaluation + } + + this.correctOldTerminTypenForStudent(student, savedPruefung) + + 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) + this.recalculateSelectable(student) + this.reformatStudentRow(student) + + // add col to table + const cols = [...this.notenTableOptions.columns.slice(0, -1)]; + let kommCol = null + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1]; + + if(this.distinctPruefungsDates.length) { + cols.push({ + title: this.$capitalize(this.$p.t('benotungstool/c4originalZnote')), + field: "Termin1", + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + sorter: this.pruefungSorter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 200, + width: 250, + visible: true, + tooltip: false + }) + } + + this.distinctPruefungsDates.forEach((date)=>{ + const dateparts = date.split('-') + const titledate = `${dateparts[2]}.${dateparts[1]}.${dateparts[0]}` + + cols.push({ + title: titledate,//this.$p.t('benotungstool/pruefungNr', [index+1]), + field: date, + formatter: this.pruefungFormatter, + titleFormatter: this.pruefungTitleFormatter, + sorter: this.pruefungSorter, + topCalc: this.terminCalcFunc, + topCalcFormatter: this.terminCalcFormatter, + hozAlign:"center", + widthGrow: 1, + minWidth: 200, + width: 250, + visible: true, + tooltip: false + }) + }) + + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) cols.push(kommCol) // keep kommPruef Col as last + + + // preemptively do the persistence part of columns already here so we get the merged definition before + // tabulator internal go to war with vue reactives + let colsUsed = null + const saved = this.loadState(); + if (saved && saved.columns) { + // new columns map for lookup + const colMap = new Map(cols.map(c => [c.field, c])); + + const restoredCols = []; + + // add columns in the SAVED order, applying saved width/visibility + saved.columns.forEach(savedCol => { + const originalDef = colMap.get(savedCol.field); + if (originalDef) { + restoredCols.push({ + ...originalDef, // Keep formatters, calcs, etc. + width: savedCol.width, + visible: savedCol.visible + }); + colMap.delete(savedCol.field); // Remove so we don't double-add + } + }); + + // append any NEW columns aka pruefungs cols + colMap.forEach((def) => { + restoredCols.push(def); + }); + + colsUsed = restoredCols; + this.colLayoutRestored = true; + } + + + const colsFinal = colsUsed ?? cols + this.$refs.notenTable.tabulator.setColumns(colsFinal) + // this.$refs.notenTable.tabulator.redraw(true) + // redraw table outside this function + }, + recalculateSelectable(student) { + const vueThis = this + Object.defineProperty(student, 'selectable', { + get() { + const kP = student.pruefungen?.find(p => p.pruefungstyp_kurzbz == 'kommPruef') + return !(kP || student.hoechsterAntritt >= vueThis.maxAntrittCount) + }, + set() { + // empty setter so tabulator doesnt scream + }, + enumerable: true, + configurable: true + }) + }, + 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.alertDefault( + 'success', + 'Info', + 'Noten gespeichert', + true + ) + } + + 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 = [] + // const uids = this.selectedUids.map(student => { + this.selectedUids.forEach(student => { + + // if a student doesnt have a lv_note entered definitely dont allow to create pruefung + if(!student.lv_note) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat noch keine LV-Note eingetragen! Es wird keine Prüfung angelegt.') + return + } + + if(!student.note) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat noch keine Zeugnis Note eingetragen! Es wird keine Prüfung angelegt.') + return + } + + // check if student antrittCount is too high already + if(student.hoechsterAntritt >= this.maxAntrittCount) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat bereits ' + student.hoechsterAntritt + ' Prüfungsantritte abgelegt. Es wird keine Prüfung angelegt.') + 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 >= this.selectedPruefungDate + }) + if(youngerPruefung) { + this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat bereits eine Prüfung am '+ youngerPruefung.datum +' eingetragen. Es wird keine Prüfung angelegt.') + return + } + + uids.push({ + 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") { + + // iterate over response data + // -> alert successful pruefungen + // -> alert denied pruefungen + reason + + let uidListSuccess = '' + let uidListError = '' + const successData = [] + Object.keys(res.data).forEach(student_uid => { + const student = res.data[student_uid] + // actual pruefung has been allocated + if(student.savedPruefung || student.extraPruefung) { + uidListSuccess += ' ' + student_uid + + // keep res.data format intact for handleResponse method + successData[student_uid] = student + } else { // there should be an error message why no pruefungen where allocated for this person, many reasons possible + uidListError += student_uid + ' - ' + student +'\n'// student variable is the error message here + } + }) + + if(uidListError != '') { + this.$fhcAlert.alertError( + this.$capitalize(this.$p.t('benotungstool/c4pruefungAnlageError', [dateStrFront])) + ': ' + uidListError + ' ' + ) + } + + if(uidListSuccess != '') { + this.$fhcAlert.alertDefault( + 'success', + 'Info', + this.$capitalize(this.$p.t('benotungstool/pruefungAngelegtAn', [dateStrFront])) + ': ' + uidListSuccess, + true + ) + + this.handleAddNewPruefungenResponse({data: successData}, uids) + } + + } + }).finally(()=> this.loading = false) + }, + 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] + + const isDefinedAsAntrittsloseNote = this.config.NOTEN_OHNE_ANTRITT.find(n_pk => n_pk == p.note) + if(!isDefinedAsAntrittsloseNote) 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.config.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) return 0 + 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: { + maxAntrittCount() { + let maxAntritte = 1; + + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_TERMIN2) maxAntritte++ + if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_TERMIN3) maxAntritte++ + + return maxAntritte; + }, + getFreigabeCounter() { + return this.studenten ? this.studenten.reduce((acc, cur) => { + if(cur.freigegeben == 'changed') { + acc++ + } + return acc + }, 0) : 0 + }, + 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.$capitalize(this.$p.t('benotungstool/notenfreigabeHinweistextv4')) + }, + getNotenimportHinweistext() { + return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv5')) + } + }, + created() { + this.setupCreated() + }, + mounted() { + this.setupMounted() + }, + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

{{$capitalize($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..8bea0ac77 --- /dev/null +++ b/public/js/components/DropdownModes/LehreinheitenModule.js @@ -0,0 +1,72 @@ +import ApiLehre from "../../api/factory/lehre.js"; +import {capitalize} from "../../helpers/StringHelpers.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: capitalize(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/public/js/helpers/StringHelpers.js b/public/js/helpers/StringHelpers.js index a67d0138f..16f8c3dbf 100644 --- a/public/js/helpers/StringHelpers.js +++ b/public/js/helpers/StringHelpers.js @@ -1,4 +1,19 @@ export function capitalize(string) { if (!string) return ''; - return string[0].toUpperCase() + string.slice(1); + + // ref unwrap if we receive such + if (Vue.isRef(string)) { + return Vue.computed(() => { + const val = Vue.unref(string); + return (val && typeof val === 'string') ? val.charAt(0).toUpperCase() + val.slice(1) : ''; + }); + } + + // just a plain string, return a plain string + if (typeof string === 'string') { + return string.charAt(0).toUpperCase() + string.slice(1); + } + + return ''; + } \ No newline at end of file diff --git a/public/js/helpers/debounce.js b/public/js/helpers/debounce.js new file mode 100644 index 000000000..09529b07c --- /dev/null +++ b/public/js/helpers/debounce.js @@ -0,0 +1,9 @@ +export function debounce(fn, delay) { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fn(...args) + }, delay); + }; +} diff --git a/public/js/tabulator/formatter/centered.js b/public/js/tabulator/formatter/centered.js new file mode 100644 index 000000000..3c38f4d4d --- /dev/null +++ b/public/js/tabulator/formatter/centered.js @@ -0,0 +1,5 @@ +export function centeredFormatter (cell) +{ + const val = cell.getValue() + return '
'+val+'
' +} \ 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/dbupdate_3.4/60873_gesamtnoteneingabe_cis4.php b/system/dbupdate_3.4/60873_gesamtnoteneingabe_cis4.php new file mode 100644 index 000000000..4ae2e38d5 --- /dev/null +++ b/system/dbupdate_3.4/60873_gesamtnoteneingabe_cis4.php @@ -0,0 +1,17 @@ +db_query("SELECT 1 FROM public.tbl_vorlage WHERE vorlage_kurzbz = 'Notenfreigabe'")) +{ + if($db->db_num_rows($result) === 0) + { + $qry = "INSERT INTO public.tbl_vorlage (vorlage_kurzbz, bezeichnung, anmerkung, mimetype) + VALUES ('Notenfreigabe', 'Notenfreigabe', null, 'text/html') + ON CONFLICT (vorlage_kurzbz) DO NOTHING;"; + + if(!$db->db_query($qry)) + echo 'system.tbl_vorlage: '.$db->db_last_error().'
'; + else + echo "
system.tbl_vorlage Notenfreigabe hinzugefuegt"; + } +} \ No newline at end of file diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 6c88d692f..001c85b8b 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -59528,6 +59528,1224 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), //**************************** CIS Projektabgabeuebersicht ende + // 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' => 'c4antrittCountv2', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Antritte', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => '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' => 'c4mobility', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Mobilität', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Mobility', + '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' => 'c4notenvorschlag', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Notenvorschlag', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Grade Suggestion', + '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' => 'freigabecounterPositiv', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sie können {0} Noten freigeben!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'You are able to approve {0} grades!', + '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' => 'approveGradesv2', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '{0} Noten freigeben', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Approve {0} 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' => 'notenfreigabeHinweistextv4', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => ' + 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.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => ' + 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 "Confirm Grades" +
+ For increased security reasons, you will be required to enter your password when releasing grades.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'notenimportHinweistextv5', + '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' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4anwesenheitsquote', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Anwesenheit', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Attendance', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4keineLvNoteEingetragen', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Keine LV Note eingetragen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No Subject Grade entered', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4pruefungAnlageError', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prüfungstermin anlegen für {0} fehlgeschlagen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Not possible to allocate exam on {0}', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4punkte', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Punkte', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Points', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4notenvorschlagUebernehmen', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Vorschlag übernehmen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'apply suggestion', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4importNoStudentFoundForIdInRow', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Keine StudentIn gefunden für ID {0} in Zeile Nr. {1}. Die Zeile wurde übersprungen.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No student found for ID {0} in row no. {1}. The row has been skipped.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4importNoGradeFoundForIdInRow', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Keine Note gefunden für ID {0} in Zeile Nr. {1}. Die Zeile wurde übersprungen.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No grade found for ID {0} in row no. {1}. The row has been skipped.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4importInvalidDateFoundForIdInRow', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Ungültiges Datumformat für ID {0} in Zeile Nr. {1}. Bitte verwenden Sie das Format "DD.MM.YYYY". Die Zeile wurde übersprungen.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Invalid date found for ID {0} in row no. {1}. Please use the format "DD.MM.YYYY". The row has been skipped.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // CIS4 GESAMTNOTENEINGABE ENDE ------------------------------------------------------------------------------------ );