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 .= "| " . $this->p->t('person','personenkennzeichen') . " | \n
+ " . $this->p->t('lehre','studiengang') . " | \n
+ " . $this->p->t('benotungstool','c4nachname') . " | \n
+ " . $this->p->t('benotungstool','c4vorname') . " | \n";
+ if(defined(CIS_GESAMTNOTE_PUNKTE) && CIS_GESAMTNOTE_PUNKTE) {
+ $studlist .= "" . $this->p->t('benotungstool','c4punkte') . " | \n";
+ }
+ $studlist .= "" . $this->p->t('benotungstool','c4grade') . " | \n";
+ $studlist .= "" . $this->p->t('ui','bearbeitetVon') . " |
\n";
+ } else {
+ $studlist .= "" . $this->p->t('person','uid') . " | \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 .= "| " . trim($note->matrikelnr) . " | ";
+ $studlist .= "" . trim($note->kuerzel) . " | ";
+ $studlist .= "" . trim($note->nachname) . " | ";
+ $studlist .= "" . trim($note->vorname) . " | ";
+
+ if(defined(CIS_GESAMTNOTE_PUNKTE) && CIS_GESAMTNOTE_PUNKTE) {
+ $studlist .= "" . trim($lvgesamtnote->punkte) . " | ";
+ }
+ $studlist .= "" .$note->noteBezeichnung. " | ";
+
+ $studlist .= "" . $lvgesamtnote->mitarbeiter_uid;
+ if ($lvgesamtnote->updatevon != '')
+ $studlist .= " (" . $lvgesamtnote->updatevon . ")";
+ $studlist .= " |
";
+ } else {
+ $studlist .= "| " . trim($note->uid) . " |
\n";
+ }
+ }
+ }
+ }
+ $studlist .= "
";
+
+ $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/c4notenImportieren'))}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$capitalize($p.t('benotungstool/c4addNewPruefung'))}}
+
+
+
{{$capitalize($p.t('benotungstool/c4date'))}}:
+
+
+
+
+
+
+
+
{{$capitalize($p.t('benotungstool/prueflingSelectionv2'))}}:
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $p.t('benotungstool/noteneingabeSpeichern') }}
+
+
+
+
+
+
+
+
+
+
+ {{ pruefung ? $capitalize($p.t('benotungstool/editPruefungFor')) : $capitalize($p.t('benotungstool/createPruefungFor')) }} {{pruefungStudent?.vorname}} {{pruefungStudent?.nachname}}
+
+
+
{{$capitalize($p.t('benotungstool/c4date'))}}:
+
+
+
+
+
+
+
{{$capitalize($p.t('benotungstool/c4punkte'))}}:
+
+
+
+
+
+
+
{{$capitalize($p.t('lehre/note'))}}:
+
+
+
+ {{ option.bezeichnung }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{$capitalize($p.t('benotungstool/benotungstoolTitle'))}}
+ {{ lv?.bezeichnung }}
+
+
+
+
+
+ {{ option.fullString }}
+
+
+
+
+
+
+
+
+
+
+ {{ slotProps.option.infoString }}
+
+ {{ slotProps.option.studentcount }}
+
+ {{ slotProps.option.termincount }}
+
+
+
+
+
+
+
+
+
+
+ {{ option.studiensemester_kurzbz }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+};
+
+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 ------------------------------------------------------------------------------------
);