mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-03 20:09:29 +00:00
Merge branch 'cis40_2026-05_ma_rc' into cis40_2026-05_ma_rc_routerlink
This commit is contained in:
@@ -3,4 +3,15 @@
|
||||
if (!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
// 'entschuldigt' & 'noch nicht eingetragen' -> wirken sich nicht auf Antritte aus
|
||||
$config['NOTEN_OHNE_ANTRITT'] = [9, 17]; // tbl_note pk
|
||||
$config['NOTEN_OHNE_ANTRITT'] = [9, 17]; // tbl_note pk
|
||||
|
||||
$config['NOTEN_OCCURANCE_LIMIT_MAP'] = [17 => 1]; // across the 4 fixed antritte only one can be entschuldigt
|
||||
|
||||
// tbl_note pk of the 'entschuldigt' note. An entschuldigt Termin is preserved as its own dated
|
||||
// entry when a new pruefung of the same type is created (instead of being overwritten).
|
||||
$config['NOTE_ENTSCHULDIGT'] = 17;
|
||||
|
||||
// availability of the two Benotungstool import flows. When both are true they are shown as
|
||||
// separate buttons/dialogs.
|
||||
$config['CIS_GESAMTNOTE_PRUEFUNGSIMPORT'] = true; // dated import that creates a pruefung per row
|
||||
$config['CIS_GESAMTNOTE_NOTENIMPORT'] = false; // classic note-only import (uid + note, no date)
|
||||
@@ -85,6 +85,8 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
public function getCisConfig() {
|
||||
$NOTEN_OHNE_ANTRITT = $this->config->item('NOTEN_OHNE_ANTRITT');
|
||||
$NOTEN_OCCURANCE_LIMIT_MAP = $this->config->item('NOTEN_OCCURANCE_LIMIT_MAP');
|
||||
$NOTE_ENTSCHULDIGT = $this->config->item('NOTE_ENTSCHULDIGT');
|
||||
|
||||
$this->terminateWithSuccess(
|
||||
array(
|
||||
@@ -117,11 +119,21 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
// 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,
|
||||
|
||||
// availability of the two import flows (application/config/noten.php); when both are
|
||||
// true they are shown separately
|
||||
'CIS_GESAMTNOTE_PRUEFUNGSIMPORT' => $this->config->item('CIS_GESAMTNOTE_PRUEFUNGSIMPORT'),
|
||||
'CIS_GESAMTNOTE_NOTENIMPORT' => $this->config->item('CIS_GESAMTNOTE_NOTENIMPORT'),
|
||||
|
||||
// send a mail when approving grades
|
||||
'CIS_GESAMTNOTE_FREIGABEMAIL_NOTE' => CIS_GESAMTNOTE_FREIGABEMAIL_NOTE,
|
||||
|
||||
'NOTEN_OHNE_ANTRITT' => $NOTEN_OHNE_ANTRITT
|
||||
'NOTEN_OHNE_ANTRITT' => $NOTEN_OHNE_ANTRITT,
|
||||
|
||||
'NOTEN_OCCURANCE_LIMIT_MAP' => $NOTEN_OCCURANCE_LIMIT_MAP,
|
||||
|
||||
// pk of the 'entschuldigt' note; used to preserve excused Termine on new pruefung creation
|
||||
'NOTE_ENTSCHULDIGT' => $NOTE_ENTSCHULDIGT
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -180,7 +192,7 @@ class Noten extends FHCAPI_Controller
|
||||
$grades[$uid]['grades'] = [];
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoten($lv_id, $uid, $sem_kurzbz);
|
||||
$this->addMeta($uid.'getLvGesamtNoten', $result);
|
||||
// $this->addMeta($uid.'getLvGesamtNoten', $result);
|
||||
if(!isError($result) && hasData($result)) {
|
||||
$lvgesamtnote = getData($result)[0];
|
||||
$grades[$uid]['note_lv'] = $lvgesamtnote->note;
|
||||
@@ -209,6 +221,7 @@ class Noten extends FHCAPI_Controller
|
||||
]
|
||||
);
|
||||
} catch (Throwable $t) {
|
||||
// $this->addMeta('throwable', $t->getTrace());
|
||||
$this->addMeta('getExternalGradesError', $t->getMessage());
|
||||
}
|
||||
|
||||
@@ -369,7 +382,7 @@ class Noten extends FHCAPI_Controller
|
||||
foreach($result->noten as $note) {
|
||||
|
||||
$resultLVGes = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz);
|
||||
$this->addMeta($note->uid.'$resultLVGes', $resultLVGes);
|
||||
// $this->addMeta($note->uid.'$resultLVGes', $resultLVGes);
|
||||
if (!isError($resultLVGes) && hasData($resultLVGes))
|
||||
{
|
||||
$lvgesamtnote = getData($resultLVGes)[0];
|
||||
@@ -521,10 +534,12 @@ class Noten extends FHCAPI_Controller
|
||||
$datum = $result->datum;
|
||||
$lva_id = $result->lva_id;
|
||||
$lehreinheit_id = $result->lehreinheit_id;
|
||||
|
||||
// pruefung_id identifies the record being edited; null when a new pruefung is added
|
||||
$pruefung_id = property_exists($result, 'pruefung_id') ? $result->pruefung_id : null;
|
||||
|
||||
$stsem = $result->sem_kurzbz;
|
||||
$typ = $result->typ;
|
||||
|
||||
|
||||
$jetzt = date("Y-m-d H:i:s");
|
||||
|
||||
if(CIS_GESAMTNOTE_PUNKTE && isset($punkte) && $punkte >= 0) {
|
||||
@@ -549,6 +564,14 @@ class Noten extends FHCAPI_Controller
|
||||
$note = getData($result)[0]->note;
|
||||
}
|
||||
|
||||
// validate the edit before any write: the date must stay between the neighbouring exams and,
|
||||
// once a later/higher pruefung exists, the grade may no longer be changed (only the date).
|
||||
// only applies when editing an existing record ($pruefung_id set)
|
||||
$editError = $this->validatePruefungEdit($student_uid, $lva_id, $stsem, $note, $datum, $pruefung_id);
|
||||
if($editError !== null) {
|
||||
$this->terminateWithError($editError, 'general');
|
||||
}
|
||||
|
||||
|
||||
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
|
||||
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
|
||||
@@ -566,6 +589,10 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoten($lva_id, $student_uid, $stsem);
|
||||
|
||||
$origLvNote = null;
|
||||
$origLvPunkte = null;
|
||||
$origBenotungsdatum = null;
|
||||
if(!isError($result) && !hasData($result)) {
|
||||
|
||||
$id = $this->LvgesamtnoteModel->insert(
|
||||
@@ -601,6 +628,11 @@ class Noten extends FHCAPI_Controller
|
||||
{
|
||||
$lvgesamtnote = getData($result)[0];
|
||||
|
||||
$orig = getData($result)[0];
|
||||
$origLvNote = $lvgesamtnote->note;
|
||||
$origLvPunkte = $lvgesamtnote->punkte;
|
||||
$origBenotungsdatum = $lvgesamtnote->benotungsdatum;
|
||||
|
||||
$id = $this->LvgesamtnoteModel->update(
|
||||
[$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id],
|
||||
array(
|
||||
@@ -624,7 +656,7 @@ class Noten extends FHCAPI_Controller
|
||||
}
|
||||
|
||||
// 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);
|
||||
$pruefungenChanged = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum, $origLvNote, $origLvPunkte, $origBenotungsdatum);
|
||||
|
||||
$savedPruefung = $pruefungenChanged['savedPruefung'] ?? null;
|
||||
$extraPruefung = $pruefungenChanged['extraPruefung'] ?? null;
|
||||
@@ -638,17 +670,17 @@ class Noten extends FHCAPI_Controller
|
||||
/**
|
||||
* private helper method to update/insert pruefungstermine
|
||||
*/
|
||||
private function savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte = '', $datum)
|
||||
private function savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte = '', $datum, $origLvNote = null, $origLvPunkte = null, $origBenotungsdatum = null)
|
||||
{
|
||||
|
||||
// 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);
|
||||
// $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
|
||||
@@ -691,7 +723,12 @@ class Noten extends FHCAPI_Controller
|
||||
$resultLV = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lva_id, $student_uid, $stsem);
|
||||
|
||||
// update Termin1 note
|
||||
if (hasData($resultLV))
|
||||
if ($origLvNote !== null) {
|
||||
$pr_note = $origLvNote;
|
||||
$pr_punkte = $origLvPunkte;
|
||||
$benotungsdatum = $origBenotungsdatum ?? $jetzt;
|
||||
}
|
||||
else if (hasData($resultLV))
|
||||
{
|
||||
$lvgesamtnote = getData($resultLV)[0];
|
||||
$pr_note = $lvgesamtnote->note;
|
||||
@@ -736,12 +773,20 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
|
||||
|
||||
// Die Pruefung wird als Termin2 eingetragen
|
||||
// Die Pruefung wird als Termin2 eingetragen.
|
||||
// Ein bestehender "entschuldigt"-Termin2 bleibt erhalten: in diesem Fall wird die neue
|
||||
// Pruefung als eigener Datensatz angelegt statt den entschuldigten zu ueberschreiben.
|
||||
$result2 = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, "Termin2", $lva_id, $stsem);
|
||||
// if there is a termin 2 entry already update it
|
||||
|
||||
$termin2 = null;
|
||||
if(!isError($result2) && hasData($result2)) {
|
||||
// update
|
||||
$termin2 = getData($result2)[0];
|
||||
foreach(getData($result2) as $t2) {
|
||||
if(!$this->isEntschuldigtNote($t2->note)) { $termin2 = $t2; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if($termin2 !== null) {
|
||||
// update existing (non-excused) Termin2
|
||||
$id = $this->LePruefungModel->update(
|
||||
$termin2->pruefung_id,
|
||||
array(
|
||||
@@ -761,8 +806,8 @@ class Noten extends FHCAPI_Controller
|
||||
$this->logLib->logInfoDB(array('termin2 updated',$res, getAuthUID(), getAuthPersonId()));
|
||||
|
||||
|
||||
} else if(!isError($result2) && !hasData($result2)) {
|
||||
// new entry termin 2
|
||||
} else if(!isError($result2)) {
|
||||
// no editable Termin2 (none yet, or only an entschuldigt one) -> insert new, excused stays
|
||||
|
||||
$id = $this->LePruefungModel->insert(
|
||||
array(
|
||||
@@ -790,15 +835,20 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
}
|
||||
|
||||
} else if($typ == "Termin3" && defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN3') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN3)
|
||||
} else if($typ == "Termin3" && defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN3') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN3)
|
||||
{
|
||||
|
||||
// same entschuldigt-preservation handling as Termin2
|
||||
$result3 = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, "Termin3", $lva_id, $stsem);
|
||||
|
||||
$termin3 = null;
|
||||
if(!isError($result3) && hasData($result3)) {
|
||||
// update
|
||||
$termin3 = getData($result3)[0];
|
||||
foreach(getData($result3) as $t3) {
|
||||
if(!$this->isEntschuldigtNote($t3->note)) { $termin3 = $t3; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if($termin3 !== null) {
|
||||
// update existing (non-excused) Termin3
|
||||
$id = $this->LePruefungModel->update(
|
||||
$termin3->pruefung_id,
|
||||
array(
|
||||
@@ -817,8 +867,8 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
$this->logLib->logInfoDB(array('termin3 updated',$res, getAuthUID(), getAuthPersonId()));
|
||||
|
||||
} else if(!isError($result3) && !hasData($result3)) {
|
||||
// insert new termin3
|
||||
} else if(!isError($result3)) {
|
||||
// no editable Termin3 (none yet, or only an entschuldigt one) -> insert new, excused stays
|
||||
|
||||
$id = $this->LePruefungModel->insert(
|
||||
array(
|
||||
@@ -852,6 +902,105 @@ class Noten extends FHCAPI_Controller
|
||||
return $pruefungenChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* ranking of the pruefung attempt types, used to detect whether a "höhere"
|
||||
* (later attempt) pruefung exists for a student
|
||||
*/
|
||||
private function pruefungAttemptRank($typ)
|
||||
{
|
||||
switch($typ) {
|
||||
case 'Termin1': return 1;
|
||||
case 'Termin2': return 2;
|
||||
case 'Termin3': return 3;
|
||||
case 'kommPruef': return 4;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* whether a note is the configured 'entschuldigt' note. Such Termine are preserved as their
|
||||
* own dated entry instead of being overwritten when a new pruefung of the same type is created.
|
||||
*/
|
||||
private function isEntschuldigtNote($note)
|
||||
{
|
||||
$entschuldigt = $this->config->item('NOTE_ENTSCHULDIGT');
|
||||
return $entschuldigt !== null && $note == $entschuldigt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an edit to an existing Termin2/Termin3 pruefung. Returns a localized error
|
||||
* string if the edit is not allowed, or null if it is. Mirrors the frontend guards so a
|
||||
* disallowed edit cannot be forced through the API.
|
||||
*
|
||||
* Rules:
|
||||
* - The new $datum must stay strictly between the dates of the chronologically adjacent
|
||||
* pruefungen so the attempt order is preserved.
|
||||
* - Once a later-dated or higher-attempt pruefung already exists the grade may no longer be
|
||||
* changed (only the datum may be corrected within the bounds above).
|
||||
*
|
||||
* Only guards EDITS: $pruefung_id identifies the record being edited; when it is null (a new
|
||||
* attempt is being added) nothing is restricted here (adds are validated client-side).
|
||||
*
|
||||
* @param string $student_uid
|
||||
* @param int $lva_id
|
||||
* @param string $stsem
|
||||
* @param int $newNote the (already resolved) note being saved
|
||||
* @param string $newDatum the datum being saved (Y-m-d)
|
||||
* @param int $pruefung_id pk of the record being edited, or null for an add
|
||||
* @return string|null
|
||||
*/
|
||||
private function validatePruefungEdit($student_uid, $lva_id, $stsem, $newNote, $newDatum, $pruefung_id)
|
||||
{
|
||||
if($pruefung_id === null || $pruefung_id === '') return null; // add, not an edit
|
||||
|
||||
$result = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, null, $lva_id, $stsem);
|
||||
if(isError($result) || !hasData($result)) return null;
|
||||
|
||||
$pruefungen = getData($result);
|
||||
|
||||
// the record being edited
|
||||
$current = null;
|
||||
foreach($pruefungen as $p) {
|
||||
if($p->pruefung_id == $pruefung_id) { $current = $p; break; }
|
||||
}
|
||||
if($current === null) return null;
|
||||
|
||||
// only the changeable resit grades are guarded
|
||||
if($current->pruefungstyp_kurzbz !== 'Termin2' && $current->pruefungstyp_kurzbz !== 'Termin3') return null;
|
||||
|
||||
$currentRank = $this->pruefungAttemptRank($current->pruefungstyp_kurzbz);
|
||||
$currentDate = substr((string)$current->datum, 0, 10);
|
||||
$new = substr((string)$newDatum, 0, 10);
|
||||
|
||||
// chronological bounds from the immediate date-neighbours + detect later/higher pruefung
|
||||
$lower = null; $upper = null; $hasLaterOrHigher = false;
|
||||
foreach($pruefungen as $p) {
|
||||
if($p->pruefung_id == $current->pruefung_id) continue;
|
||||
|
||||
$d = substr((string)$p->datum, 0, 10);
|
||||
if($d !== '') {
|
||||
if($d < $currentDate) { if($lower === null || $d > $lower) $lower = $d; }
|
||||
elseif($d > $currentDate) { if($upper === null || $d < $upper) $upper = $d; }
|
||||
}
|
||||
|
||||
if($d > $currentDate || $this->pruefungAttemptRank($p->pruefungstyp_kurzbz) > $currentRank) {
|
||||
$hasLaterOrHigher = true;
|
||||
}
|
||||
}
|
||||
|
||||
// grade is locked once a later/higher pruefung exists
|
||||
if($hasLaterOrHigher && $newNote != $current->note) {
|
||||
return $this->p->t('benotungstool', 'pruefungNoteLocked', [$student_uid]);
|
||||
}
|
||||
|
||||
// datum must stay strictly between the neighbouring exam dates
|
||||
if(($lower !== null && $new <= $lower) || ($upper !== null && $new >= $upper)) {
|
||||
return $this->p->t('benotungstool', 'pruefungDatumOutOfRange', [$student_uid]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST METHOD
|
||||
* expects 'sem_kurzbz', 'lv_id', 'student_uid', 'note'
|
||||
@@ -874,7 +1023,7 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $student_uid, $sem_kurzbz);
|
||||
|
||||
$this->addMeta('LvgesamtnoteModelresult', $result);
|
||||
// $this->addMeta('LvgesamtnoteModelresult', $result);
|
||||
|
||||
if(!isError($result) && hasData($result)) {
|
||||
$lvgesamtnote = getData($result)[0];
|
||||
@@ -950,12 +1099,12 @@ class Noten extends FHCAPI_Controller
|
||||
{
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz);
|
||||
$this->addMeta($note->uid.'$result', $result);
|
||||
// $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);
|
||||
// $this->addMeta($note->uid.'note', $note);
|
||||
}
|
||||
|
||||
if(!isError($result) && hasData($result)) {
|
||||
@@ -1074,9 +1223,9 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
if(CIS_GESAMTNOTE_PUNKTE) {
|
||||
$result = $this->NotenschluesselaufteilungModel->getNote($pruefung->punkte, $lv_id, $sem_kurzbz);
|
||||
$this->addMeta($pruefung->uid."result", $result);
|
||||
// $this->addMeta($pruefung->uid."result", $result);
|
||||
$pruefung->note = $this->getDataOrTerminateWithError($result);
|
||||
$this->addMeta($pruefung->uid."note", $pruefung->note);
|
||||
// $this->addMeta($pruefung->uid."note", $pruefung->note);
|
||||
}
|
||||
|
||||
$student_uid = $pruefung->uid;
|
||||
|
||||
@@ -1350,6 +1350,7 @@ class Lehrveranstaltung_model extends DB_Model
|
||||
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_studiengang.studiengang_kz,
|
||||
tbl_lehrveranstaltung.semester as lv_semester,
|
||||
tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung,
|
||||
(SELECT kurzbz FROM public.tbl_mitarbeiter
|
||||
|
||||
@@ -36,6 +36,7 @@ require_once('../../../include/notenschluessel.class.php');
|
||||
require_once('../../../include/Excel/excel.php');
|
||||
require_once('../../../include/phrasen.class.php');
|
||||
require_once('../../../include/pruefung.class.php');
|
||||
require_once('../../../include/benutzerberechtigung.class.php');
|
||||
|
||||
$uid = get_uid();
|
||||
|
||||
@@ -44,7 +45,7 @@ $uid = get_uid();
|
||||
$sprache = getSprache();
|
||||
$p = new phrasen($sprache);
|
||||
|
||||
if(!check_lektor($uid))
|
||||
if (!check_lektor($uid))
|
||||
die('Sie haben keine Berechtigung fuer diese Seite');
|
||||
|
||||
if (!$db = new basis_db())
|
||||
@@ -90,6 +91,21 @@ if(isset($_GET['lehreinheit_id']))
|
||||
else
|
||||
$lehreinheit_id = '';
|
||||
|
||||
// Permissions
|
||||
$berechtigung = new benutzerberechtigung();
|
||||
$berechtigung->getBerechtigungen($uid);
|
||||
|
||||
// LV load
|
||||
$lvobj = new lehrveranstaltung($lvid);
|
||||
|
||||
// Check permissions
|
||||
if (!$berechtigung->isBerechtigt('admin')
|
||||
&& !$berechtigung->isBerechtigt('assistenz')
|
||||
&& !$berechtigung->isBerechtigt('lehre', $lvobj->oe_kurzbz, 's')
|
||||
&& !check_lektor_lehrveranstaltung($uid, $lvid, $stsem)
|
||||
)
|
||||
die('Sie haben keine Berechtigung fuer diese Seite');
|
||||
|
||||
/*
|
||||
* Create Excel File
|
||||
*/
|
||||
@@ -143,7 +159,38 @@ else
|
||||
// let's merge
|
||||
$format_title->setAlign('merge');
|
||||
|
||||
$lvobj = new lehrveranstaltung($lvid);
|
||||
// Importkonfiguration aus dem CI-Config (application/config/noten.php) lesen, damit nur die
|
||||
// Spalten des jeweils aktiven Importtyps hervorgehoben (grau) werden.
|
||||
$CIS_GESAMTNOTE_NOTENIMPORT = false;
|
||||
$CIS_GESAMTNOTE_PRUEFUNGSIMPORT = true;
|
||||
$notenConfigFile = __DIR__.'/../../../application/config/noten.php';
|
||||
if(is_file($notenConfigFile))
|
||||
{
|
||||
if(!defined('BASEPATH')) define('BASEPATH', true);
|
||||
$config = array();
|
||||
include($notenConfigFile);
|
||||
if(isset($config['CIS_GESAMTNOTE_NOTENIMPORT'])) $CIS_GESAMTNOTE_NOTENIMPORT = $config['CIS_GESAMTNOTE_NOTENIMPORT'];
|
||||
if(isset($config['CIS_GESAMTNOTE_PRUEFUNGSIMPORT'])) $CIS_GESAMTNOTE_PRUEFUNGSIMPORT = $config['CIS_GESAMTNOTE_PRUEFUNGSIMPORT'];
|
||||
unset($config);
|
||||
}
|
||||
|
||||
// plain (non-highlighted) variants used when an import type is disabled
|
||||
$format_plain =& $workbook->addFormat();
|
||||
$format_plain->setAlign('left');
|
||||
$format_plain->setNumFormat(49);
|
||||
|
||||
$format_plainright =& $workbook->addFormat();
|
||||
$format_plainright->setAlign('right');
|
||||
$format_plainright->setNumFormat(49);
|
||||
|
||||
// highlight only the cells belonging to an enabled import flow:
|
||||
// - Notenimport -> Personenkennzeichen + Note (main block)
|
||||
// - Prüfungsimport -> Personenkennzeichen + Datum + Note (Termin2/Termin3 blocks)
|
||||
$fmtNoteImportPkz = $CIS_GESAMTNOTE_NOTENIMPORT ? $format_highlight : $format_plain;
|
||||
$fmtNoteImportNote = $CIS_GESAMTNOTE_NOTENIMPORT ? $format_highlightright : $format_plainright;
|
||||
$fmtPruefImportPkz = $CIS_GESAMTNOTE_PRUEFUNGSIMPORT ? $format_highlight : $format_plain;
|
||||
$fmtPruefImportDate = $CIS_GESAMTNOTE_PRUEFUNGSIMPORT ? $format_highlightright_date : $format_plainright;
|
||||
$fmtPruefImportNote = $CIS_GESAMTNOTE_PRUEFUNGSIMPORT ? $format_highlightright : $format_plainright;
|
||||
|
||||
$worksheet->write(0,0,$p->t('anwesenheitsliste/notenliste')." ".($sprache=='English'?$lvobj->bezeichnung_english:$lvobj->bezeichnung),$format_bold);
|
||||
|
||||
@@ -338,13 +385,13 @@ else
|
||||
$worksheet->write($lines,3,$elem->vorname);
|
||||
}
|
||||
$worksheet->write($lines,4,$elem->semester.$elem->verband.$elem->gruppe);
|
||||
$worksheet->write($lines,5,trim($elem->matrikelnr),$format_highlight);
|
||||
$worksheet->write($lines,6, $note, $format_highlightright);
|
||||
$worksheet->write($lines,5,trim($elem->matrikelnr),$fmtNoteImportPkz);
|
||||
$worksheet->write($lines,6, $note, $fmtNoteImportNote);
|
||||
|
||||
// Nachprüfung
|
||||
if (defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN2') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN2)
|
||||
{
|
||||
$worksheet->write($lines,8, trim($elem->matrikelnr), $format_highlight);
|
||||
$worksheet->write($lines,8, trim($elem->matrikelnr), $fmtPruefImportPkz);
|
||||
$pr = new Pruefung();
|
||||
$pr->getPruefungen($elem->uid, "Termin2", $lvid, $stsem);
|
||||
$output2 = $pr->result;
|
||||
@@ -352,23 +399,23 @@ else
|
||||
if ($output2)
|
||||
{
|
||||
$resultPr = $output2[0];
|
||||
$worksheet->write($lines,9, date('d.m.Y', strtotime($resultPr->datum)), $format_highlightright_date);
|
||||
$worksheet->write($lines,9, date('d.m.Y', strtotime($resultPr->datum)), $fmtPruefImportDate);
|
||||
if(defined('CIS_GESAMTNOTE_PUNKTE') && CIS_GESAMTNOTE_PUNKTE==true)
|
||||
$worksheet->write($lines,10, $resultPr->punkte, $format_highlightright);
|
||||
$worksheet->write($lines,10, $resultPr->punkte, $fmtPruefImportNote);
|
||||
else
|
||||
$worksheet->write($lines,10, $resultPr->note, $format_highlightright);
|
||||
$worksheet->write($lines,10, $resultPr->note, $fmtPruefImportNote);
|
||||
}
|
||||
else
|
||||
{
|
||||
$worksheet->write($lines,9, '', $format_highlightright_date);
|
||||
$worksheet->write($lines,10, '', $format_highlightright);
|
||||
$worksheet->write($lines,9, '', $fmtPruefImportDate);
|
||||
$worksheet->write($lines,10, '', $fmtPruefImportNote);
|
||||
}
|
||||
}
|
||||
|
||||
// Nachprüfung
|
||||
if (defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN3') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN3)
|
||||
{
|
||||
$worksheet->write($lines,12, trim($elem->matrikelnr), $format_highlight);
|
||||
$worksheet->write($lines,12, trim($elem->matrikelnr), $fmtPruefImportPkz);
|
||||
$pr = new Pruefung();
|
||||
$pr->getPruefungen($elem->uid, "Termin3", $lvid, $stsem);
|
||||
$output3 = $pr->result;
|
||||
@@ -376,16 +423,16 @@ else
|
||||
if ($output3)
|
||||
{
|
||||
$resultPr = $output3[0];
|
||||
$worksheet->write($lines,13, date('d.m.Y', strtotime($resultPr->datum)), $format_highlightright_date);
|
||||
$worksheet->write($lines,13, date('d.m.Y', strtotime($resultPr->datum)), $fmtPruefImportDate);
|
||||
if(defined('CIS_GESAMTNOTE_PUNKTE') && CIS_GESAMTNOTE_PUNKTE==true)
|
||||
$worksheet->write($lines,14, $resultPr->punkte, $format_highlightright);
|
||||
$worksheet->write($lines,14, $resultPr->punkte, $fmtPruefImportNote);
|
||||
else
|
||||
$worksheet->write($lines,14, $resultPr->note, $format_highlightright);
|
||||
$worksheet->write($lines,14, $resultPr->note, $fmtPruefImportNote);
|
||||
}
|
||||
else
|
||||
{
|
||||
$worksheet->write($lines,13, '', $format_highlightright_date);
|
||||
$worksheet->write($lines,14, '', $format_highlightright);
|
||||
$worksheet->write($lines,13, '', $fmtPruefImportDate);
|
||||
$worksheet->write($lines,14, '', $fmtPruefImportNote);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -373,4 +373,8 @@ define('TESTTOOL_EXTERNE_UEBERWACHUNG_ALLOWED', false);
|
||||
|
||||
//enable tags in StudVW
|
||||
define('STV_TAGS_ENABLED', false);
|
||||
|
||||
//student accounts grace period
|
||||
define('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_PERIOD', '0 days');
|
||||
define('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES', serialize(array('NO_DEFINED_ROLE')));
|
||||
?>
|
||||
|
||||
@@ -340,6 +340,40 @@ class benutzerberechtigung extends basis_db
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function hasPreStudentStatusInGracePeriod($uid)
|
||||
{
|
||||
$period = defined('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_PERIOD')
|
||||
? STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_PERIOD
|
||||
: '0 days';
|
||||
$mapfunc = function($val) {
|
||||
return $this->db_add_param($val);
|
||||
};
|
||||
$roles = defined('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES')
|
||||
? implode(', ', array_map($mapfunc, unserialize(STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES)))
|
||||
: 'NO_DEFINED_ROLE';
|
||||
|
||||
$sql = <<<EOSQL
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
campus.vw_student vs
|
||||
WHERE
|
||||
vs.uid = {$this->db_add_param($uid)}
|
||||
AND
|
||||
vs.aktiv = false
|
||||
AND
|
||||
public.get_rolle_prestudent(vs.prestudent_id, null) IN ({$roles})
|
||||
AND
|
||||
(vs.updateaktivam + INTERVAL {$this->db_add_param($period)})::date >= CURRENT_DATE
|
||||
EOSQL;
|
||||
$result = $this->db_query($sql);
|
||||
if($result && $this->db_num_rows($result) > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Laedt die Berechtigungen eines Users
|
||||
* @param $uid
|
||||
@@ -355,7 +389,8 @@ class benutzerberechtigung extends basis_db
|
||||
if($row = $this->db_fetch_object($result))
|
||||
{
|
||||
// Wenn die Person nicht aktiv ist dann hat diese auch keine Rechte
|
||||
if($this->db_parse_bool($row->aktiv) == false)
|
||||
if($this->db_parse_bool($row->aktiv) == false
|
||||
&& $this->hasPreStudentStatusInGracePeriod($uid) === false)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
/* 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 */
|
||||
/* Sticky identity columns. The four freezable columns (selection/uid/Vorname/Nachname) always carry
|
||||
the .sticky-col class for the opaque background; whether a column is actually pinned is toggled
|
||||
per column via a `sticky-on-<field>` class on #notentable (set in JS from the user's selection).
|
||||
The cumulative left offsets (--sl-<field>) are computed in JS (recomputeStickyOffsets) so that
|
||||
multiple sticky columns stack next to each other instead of overlapping at left:0. */
|
||||
#notentable .tabulator-header .tabulator-col.sticky-col,
|
||||
#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 */
|
||||
background-color: #fff; /* opaque so scrolled cells don't show through */
|
||||
}
|
||||
|
||||
/* 3. Fix for Hover Effects (Optional) */
|
||||
/* If you use hover rows, you need to ensure the sticky cell matches the hover color */
|
||||
/* keep sticky cells opaque on row hover */
|
||||
#notentable .tabulator-row:hover .tabulator-cell.sticky-col {
|
||||
background-color: #ccc; /* Match your existing hover color */
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
/* enable pinning per column only when the container opts the field in.
|
||||
left offset comes from the matching --sl-<field> CSS variable (set on #notentable from JS). */
|
||||
#notentable.sticky-on-selectCol .sticky-col[tabulator-field="selectCol"],
|
||||
#notentable.sticky-on-uid .sticky-col[tabulator-field="uid"],
|
||||
#notentable.sticky-on-vorname .sticky-col[tabulator-field="vorname"],
|
||||
#notentable.sticky-on-nachname .sticky-col[tabulator-field="nachname"] {
|
||||
position: sticky;
|
||||
z-index: 10; /* float above the scrolling pruefungs columns */
|
||||
border-right: 1px solid #ddd; /* separator on the sticky block */
|
||||
}
|
||||
|
||||
#notentable.sticky-on-selectCol .sticky-col[tabulator-field="selectCol"] { left: var(--sl-selectCol, 0); }
|
||||
#notentable.sticky-on-uid .sticky-col[tabulator-field="uid"] { left: var(--sl-uid, 0); }
|
||||
#notentable.sticky-on-vorname .sticky-col[tabulator-field="vorname"] { left: var(--sl-vorname, 0); }
|
||||
#notentable.sticky-on-nachname .sticky-col[tabulator-field="nachname"] { left: var(--sl-nachname, 0); }
|
||||
|
||||
|
||||
/* styling for points input column for notenvorschläge in benotungstool*/
|
||||
#notentable .tabulator-tableholder .tabulator-editable[tabulator-field="punkte"] {
|
||||
@@ -39,9 +46,45 @@
|
||||
#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;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(0deg);
|
||||
transform-origin: center;
|
||||
color: rgba(120, 120, 60, 0.85);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
/* rotate the indicator up while the dropdown editor is open */
|
||||
#notentable .tabulator-tableholder .tabulator-cell.tabulator-editing[tabulator-field="note_vorschlag"]::after {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
.pruefung-badge {
|
||||
position: relative;
|
||||
padding-left: 32px; /* badge width (20) + its left offset (6) + gap (6) */
|
||||
}
|
||||
|
||||
.pruefung-badge::before {
|
||||
content: attr(data-attempt);
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pruefung-badge.attempt-t1::before { background: #0072B2; }
|
||||
.pruefung-badge.attempt-t2::before { background: #E69F00; }
|
||||
.pruefung-badge.attempt-t3::before { background: #CC79A7; }
|
||||
.pruefung-badge.attempt-k::before { background: #D55E00; }
|
||||
@@ -49,11 +49,11 @@ export default {
|
||||
params: { lv_id, sem_kurzbz, student_uid, note, punkte }
|
||||
};
|
||||
},
|
||||
saveStudentPruefung(student_uid, note, punkte, datum, lva_id, lehreinheit_id, sem_kurzbz, typ){
|
||||
saveStudentPruefung(student_uid, note, punkte, datum, lva_id, lehreinheit_id, sem_kurzbz, typ, pruefung_id = null){
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/Noten/saveStudentPruefung',
|
||||
params: { student_uid, note, punkte, datum, lva_id, lehreinheit_id, sem_kurzbz, typ }
|
||||
params: { student_uid, note, punkte, datum, lva_id, lehreinheit_id, sem_kurzbz, typ, pruefung_id }
|
||||
};
|
||||
},
|
||||
createPruefungen(uids, datum, lva_id, sem_kurzbz){
|
||||
|
||||
@@ -8,6 +8,7 @@ import BsOffcanvas from '../../Bootstrap/Offcanvas.js';
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
import LehreinheitenModule from '../../DropdownModes/LehreinheitenModule.js';
|
||||
import MobilityLegende from '../../Mobility/Legende.js';
|
||||
import NotenlisteLinks from "./NotenlisteLinks.js";
|
||||
import FhcOverlay from "../../Overlay/FhcOverlay.js";
|
||||
import {debounce} from "../../../helpers/debounce.js";
|
||||
import {centeredTextFormatter} from "../../../tabulator/formatter/centered.js";
|
||||
@@ -19,6 +20,7 @@ export const Benotungstool = {
|
||||
BsOffcanvas,
|
||||
CoreFilterCmpt,
|
||||
MobilityLegende,
|
||||
NotenlisteLinks,
|
||||
Dropdown: primevue.dropdown,
|
||||
Divider: primevue.divider,
|
||||
InputNumber: primevue.inputnumber,
|
||||
@@ -49,6 +51,11 @@ export const Benotungstool = {
|
||||
sortRestored: false,
|
||||
stateRestored: false,
|
||||
persistenceID: 'notenToolTable2026-02-16',
|
||||
freezePersistenceID: 'notenToolStickyCols',
|
||||
// the identity columns the user may pin to the left while scrolling the pruefungs columns
|
||||
freezableColumnFields: ['selectCol', 'uid', 'vorname', 'nachname'],
|
||||
// which of those are currently sticky (per-column selection, persisted)
|
||||
stickyColumnSelection: JSON.parse(localStorage.getItem('notenToolStickyCols') ?? '["selectCol","uid"]'),
|
||||
debouncedFetchPunkteForPruefung: null,
|
||||
config: null, // cis config
|
||||
neuesPruefungsdatumModalVisible: false,
|
||||
@@ -59,14 +66,19 @@ export const Benotungstool = {
|
||||
selectedPruefungNote: null,
|
||||
selectedPruefungDate: new Date(), // v-model for pruefung edit datepicker
|
||||
selectedPruefungPunkte: null,
|
||||
pruefungNoteLocked: false, // grade read-only when a later/higher pruefung exists (date stays editable)
|
||||
pruefungDateMin: null,
|
||||
pruefungDateMax: null,
|
||||
distinctPruefungsDates: null,
|
||||
pruefungStudent: null,
|
||||
pruefung: null,
|
||||
password: '',
|
||||
changedNotenCounter: 0,
|
||||
tableVersion: 0, // incremented on table sort/filter/data changes so getStudentenOptions mirrors the table
|
||||
tabulatorUuid: Vue.ref(0),
|
||||
domain: '',
|
||||
importString: '',
|
||||
importString: '', // Prüfungsimport textarea (uid + date + note)
|
||||
importStringNoten: '', // legacy Notenimport textarea (uid + note)
|
||||
teilnoten: null,
|
||||
lv: null,
|
||||
studenten: null,
|
||||
@@ -101,7 +113,7 @@ export const Benotungstool = {
|
||||
event: "cellEdited",
|
||||
handler: async (cell) => {
|
||||
const field = cell.getField()
|
||||
|
||||
|
||||
if(field === 'note_vorschlag') {
|
||||
const rowData = cell.getRow().getData();
|
||||
const newValue = cell.getValue();
|
||||
@@ -112,14 +124,52 @@ export const Benotungstool = {
|
||||
// revert value
|
||||
cell.setValue(original, true);
|
||||
}
|
||||
|
||||
|
||||
delete rowData._originalNoteVorschlag; // Clean up
|
||||
|
||||
|
||||
const row = cell.getRow()
|
||||
row.reformat() // trigger reformat of arrow
|
||||
|
||||
this.detachNoteVorschlagToggle()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "cellEditing",
|
||||
handler: (cell) => {
|
||||
if(cell.getField() !== 'note_vorschlag') return
|
||||
const el = cell.getElement()
|
||||
if(!el) return
|
||||
|
||||
this.detachNoteVorschlagToggle() // drop any stale listener
|
||||
|
||||
// clicking the already-open cell again should close the editor. Ignore clicks on the
|
||||
// dropdown options themselves so selecting a value still works.
|
||||
const listener = (ev) => {
|
||||
if(ev.target?.closest && ev.target.closest('.tabulator-edit-list')) return
|
||||
ev.stopPropagation()
|
||||
try { cell.cancelEdit() } catch(e) {}
|
||||
// the options popup is rendered outside the cell and lingers after cancelEdit -> remove it
|
||||
document.querySelectorAll('.tabulator-edit-list').forEach(list => list.remove())
|
||||
}
|
||||
this._nvCloseEl = el
|
||||
this._nvCloseListener = listener
|
||||
// defer so the very click that opened the editor doesn't immediately close it again.
|
||||
// capture phase so it still fires if the editor's input stops propagation.
|
||||
setTimeout(() => {
|
||||
if(this._nvCloseEl === el && this._nvCloseListener === listener) {
|
||||
el.addEventListener('mousedown', listener, true)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "cellEditCancelled",
|
||||
handler: (cell) => {
|
||||
if(cell.getField() !== 'note_vorschlag') return
|
||||
this.detachNoteVorschlagToggle()
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "cellClick",
|
||||
handler: async (e, cell) => {
|
||||
@@ -139,7 +189,8 @@ export const Benotungstool = {
|
||||
handler: (filters, rows) => {
|
||||
this.filteredRows = rows;
|
||||
this.filteredcount = rows.length;
|
||||
|
||||
this.tableVersion++; // keep the "neue Prüfung" dropdown in sync with the filtered table
|
||||
|
||||
if (!this.selectedUids.length) return;
|
||||
|
||||
const visibleData = new Set(rows.map(r => r.getData()));
|
||||
@@ -196,6 +247,48 @@ export const Benotungstool = {
|
||||
|
||||
localStorage.setItem(this.persistenceID, JSON.stringify(state));
|
||||
},
|
||||
stickyClass(field) {
|
||||
// the freezable identity columns always carry the sticky-col class; whether they are
|
||||
// actually sticky is toggled per column via container classes (see applyStickyColumnState),
|
||||
// which avoids re-running updateDefinition on columns that have reactive (Vue.computed) titles
|
||||
return this.freezableColumnFields.includes(field) ? 'sticky-col' : undefined
|
||||
},
|
||||
recomputeStickyOffsets() {
|
||||
// position each sticky column at the cumulative width of the preceding sticky columns
|
||||
// so multiple sticky columns stack next to each other instead of overlapping at left:0
|
||||
const table = this.$refs.notenTable?.tabulator
|
||||
const el = document.getElementById('notentable')
|
||||
if(!table || !el) return
|
||||
|
||||
let offset = 0
|
||||
// iterate in actual display order so column reordering is respected
|
||||
table.getColumns().forEach(col => {
|
||||
const field = col.getField()
|
||||
if(!this.freezableColumnFields.includes(field)) return
|
||||
|
||||
if(this.stickyColumnSelection.includes(field)) {
|
||||
el.style.setProperty('--sl-' + field, offset + 'px')
|
||||
offset += col.getWidth()
|
||||
} else {
|
||||
el.style.setProperty('--sl-' + field, '0px')
|
||||
}
|
||||
})
|
||||
},
|
||||
applyStickyColumnState() {
|
||||
// reflect the current per-column sticky selection onto the table container
|
||||
// (a `sticky-on-<field>` class enables position:sticky for that column in CSS) + offsets
|
||||
const el = document.getElementById('notentable')
|
||||
if(!el) return
|
||||
this.freezableColumnFields.forEach(field => {
|
||||
el.classList.toggle('sticky-on-' + field, this.stickyColumnSelection.includes(field))
|
||||
})
|
||||
this.recomputeStickyOffsets()
|
||||
},
|
||||
onStickySelectionChange() {
|
||||
// MultiSelect change handler: persist + apply (instant, no table rebuild required)
|
||||
try { localStorage.setItem(this.freezePersistenceID, JSON.stringify(this.stickyColumnSelection)) } catch(e) {}
|
||||
this.applyStickyColumnState()
|
||||
},
|
||||
handleTableBuilt() {
|
||||
const table = this.$refs.notenTable.tabulator;
|
||||
|
||||
@@ -214,8 +307,19 @@ export const Benotungstool = {
|
||||
table.on(eventName, () => this.saveState(table));
|
||||
});
|
||||
|
||||
// keep the sticky-column offsets in sync when columns are resized / moved / shown-hidden
|
||||
["columnResized", "columnMoved", "columnVisibilityChanged"].forEach(eventName => {
|
||||
table.on(eventName, () => this.recomputeStickyOffsets());
|
||||
});
|
||||
|
||||
// keep the "neue Prüfung" dropdown order in sync with the table's current sort
|
||||
table.on("dataSorted", () => this.tableVersion++);
|
||||
|
||||
// renderComplete restore state logic
|
||||
table.on("renderComplete", () => {
|
||||
// widths are settled here, so (re)apply the sticky container classes + cumulative offsets
|
||||
this.applyStickyColumnState();
|
||||
|
||||
if (this.stateRestored) return;
|
||||
|
||||
// layout restore should be happening in setupData()
|
||||
@@ -261,6 +365,14 @@ export const Benotungstool = {
|
||||
row.deselect();
|
||||
}
|
||||
},
|
||||
detachNoteVorschlagToggle() {
|
||||
// remove the "click again to close" listener attached while a note_vorschlag editor is open
|
||||
if(this._nvCloseEl && this._nvCloseListener) {
|
||||
this._nvCloseEl.removeEventListener('mousedown', this._nvCloseListener, true)
|
||||
}
|
||||
this._nvCloseEl = null
|
||||
this._nvCloseListener = null
|
||||
},
|
||||
// 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) {
|
||||
@@ -357,11 +469,10 @@ export const Benotungstool = {
|
||||
return
|
||||
}
|
||||
|
||||
if(!student.note) {
|
||||
this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat noch keine Zeugnis Note eingetragen! Es wird keine Prüfung angelegt.')
|
||||
return
|
||||
}
|
||||
|
||||
// note: a missing Zeugnisnote is intentionally allowed here so that a dated import can
|
||||
// create the first attempt for not-yet-graded students (Termin1 falls back to
|
||||
// "noch nicht eingetragen" server-side). Only the LV-note is mandatory.
|
||||
|
||||
// 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.')
|
||||
@@ -409,7 +520,7 @@ export const Benotungstool = {
|
||||
|
||||
// find notenoption and check if its allowed to use in lehre
|
||||
const notenOption = this.notenOptions.find(n => n.note == note)
|
||||
if(!notenOption.lehre) {
|
||||
if(!notenOption?.lehre) {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importNoGradeFoundForIdInRow', [rowParts[0], rowNum]))
|
||||
return
|
||||
}
|
||||
@@ -451,13 +562,13 @@ export const Benotungstool = {
|
||||
let punkte = null
|
||||
let note = null
|
||||
if(this.config?.CIS_GESAMTNOTE_PUNKTE) {
|
||||
punkte = Number.parseFloat(rowParts[1])
|
||||
punkte = Number.parseFloat(rowParts[2])
|
||||
} else {
|
||||
note = rowParts[1]
|
||||
note = rowParts[2]
|
||||
|
||||
// find notenoption and check if its allowed to use in lehre
|
||||
const notenOption = this.notenOptions.find(n => n.note == note)
|
||||
if(!notenOption.lehre) {
|
||||
if(!notenOption?.lehre) {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importNoGradeFoundForIdInRow', [rowParts[0], rowNum]))
|
||||
return
|
||||
}
|
||||
@@ -662,9 +773,15 @@ export const Benotungstool = {
|
||||
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)
|
||||
// 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.
|
||||
// EXCEPTION: an "entschuldigt" Termin must remain as its own dated entry (it was not an
|
||||
// attempt, so the new pruefung is an additional dated record, not an overwrite of it).
|
||||
const oldP = student.pruefungen.find(p =>
|
||||
p.pruefungstyp_kurzbz == saved.pruefungstyp_kurzbz
|
||||
&& p.pruefung_id != saved.pruefung_id
|
||||
&& !(this.config?.NOTE_ENTSCHULDIGT != null && p.note == this.config.NOTE_ENTSCHULDIGT)
|
||||
)
|
||||
if(oldP) {
|
||||
delete student[oldP.datum] // delete the variable col value
|
||||
|
||||
@@ -685,35 +802,50 @@ export const Benotungstool = {
|
||||
}
|
||||
}
|
||||
},
|
||||
importNoten() {
|
||||
importPruefungen() {
|
||||
// Prüfungsimport: every row must carry a date: "UID/Matrikelnr <TAB> Datum <TAB> Note".
|
||||
// Each row creates a dated exam attempt.
|
||||
const rows = this.importString.split('\n')
|
||||
const bulk = []
|
||||
let mode = ''
|
||||
// read the lines
|
||||
rows.forEach((r,i) => {
|
||||
|
||||
rows.forEach((r, i) => {
|
||||
if(r.trim() === '') return // ignore empty/trailing lines
|
||||
const rowParts = r.split('\t')
|
||||
const rowNum = i + 1
|
||||
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'
|
||||
this.parsePruefung(rowParts, bulk, rowNum)
|
||||
} else {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importRowNotDateFormat', [rowNum]))
|
||||
}
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
||||
// parsePruefung validates date + grade and resolves uid/matrikelnr;
|
||||
// validatePruefungBulk additionally checks antritte and that no earlier-dated antritt is created
|
||||
this.validatePruefungBulk(bulk)
|
||||
this.savePruefungBulk(bulk)
|
||||
|
||||
this.$refs.modalContainerPruefungImport.hide()
|
||||
},
|
||||
importNoten() {
|
||||
// classic Notenimport (legacy, config gated): "UID/Matrikelnr <TAB> Note" per row,
|
||||
// writes the LV grade directly without creating a pruefung.
|
||||
const rows = this.importStringNoten.split('\n')
|
||||
const bulk = []
|
||||
|
||||
rows.forEach((r, i) => {
|
||||
if(r.trim() === '') return // ignore empty/trailing lines
|
||||
const rowParts = r.split('\t')
|
||||
const rowNum = i + 1
|
||||
if(rowParts.length === 2) {
|
||||
this.parseNote(rowParts, bulk, rowNum)
|
||||
} else {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importRowNotNoteFormat', [rowNum]))
|
||||
}
|
||||
})
|
||||
|
||||
this.validateNotenBulk(bulk)
|
||||
this.saveNotenBulk(bulk)
|
||||
|
||||
this.$refs.modalContainerNotenImport.hide()
|
||||
},
|
||||
selectionArraysAreEqual(arr1, arr2) {
|
||||
@@ -740,7 +872,7 @@ export const Benotungstool = {
|
||||
return {
|
||||
height: 700,
|
||||
virtualDom: true,
|
||||
virtualDomBuffer: 5000,
|
||||
renderVerticalBuffer: 1000,
|
||||
index: 'uid',
|
||||
layout: 'fitData',
|
||||
placeholder: this.$capitalize(this.$p.t('global/noDataAvailable')),
|
||||
@@ -774,6 +906,9 @@ export const Benotungstool = {
|
||||
let checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
|
||||
// reflect the row's actual selection state so it survives re-renders (e.g. sorting/filtering)
|
||||
checkbox.checked = cell.getRow().isSelected();
|
||||
|
||||
// Handle select manually
|
||||
checkbox.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -791,6 +926,12 @@ export const Benotungstool = {
|
||||
let checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
|
||||
// reflect "all selectable rows selected" so the header box survives re-renders too
|
||||
onRendered(() => {
|
||||
const allowed = (cell.getTable().getRows("active") || []).filter(r => r.getData().selectable);
|
||||
checkbox.checked = allowed.length > 0 && allowed.every(r => r.isSelected());
|
||||
});
|
||||
|
||||
// Handle "select all" manually
|
||||
checkbox.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -812,15 +953,15 @@ export const Benotungstool = {
|
||||
handleClick: this.selectAllHandler
|
||||
},
|
||||
width: 50,
|
||||
cssClass: 'sticky-col',
|
||||
cssClass: this.stickyClass('selectCol'),
|
||||
field: 'selectCol',
|
||||
title: ''
|
||||
})
|
||||
columns.push({title: 'UID', field: 'uid', tooltip: false, topCalc: this.sumCalcFunc, formatter: centeredTextFormatter, cssClass: 'sticky-col'})
|
||||
columns.push({title: 'UID', field: 'uid', tooltip: false, topCalc: this.sumCalcFunc, formatter: centeredTextFormatter, cssClass: this.stickyClass('uid')})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mail'))), field: 'email', formatter: this.mailFormatter, tooltip: false, visible: false, variableHeight: true})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4antrittCountv2'))), field: 'hoechsterAntritt', formatter: centeredTextFormatter, tooltip: false})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4vorname'))), field: 'vorname', formatter: centeredTextFormatter, headerFilter: true, tooltip: false})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4nachname'))), field: 'nachname', formatter: centeredTextFormatter, headerFilter: true})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4vorname'))), field: 'vorname', formatter: centeredTextFormatter, headerFilter: true, tooltip: false, cssClass: this.stickyClass('vorname')})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4nachname'))), field: 'nachname', formatter: centeredTextFormatter, headerFilter: true, cssClass: this.stickyClass('nachname')})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4anwesenheitsquote'))), field: 'anwquote', formatter: this.percentFormatter})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mobility'))), field: 'mobility_zusatz', formatter: centeredTextFormatter, headerFilter: true, visible: false})
|
||||
if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE) {
|
||||
@@ -1115,11 +1256,37 @@ export const Benotungstool = {
|
||||
},
|
||||
teilnotenFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
|
||||
let style = 'white-space: pre-line;'
|
||||
|
||||
|
||||
return '<div style="">'+val+'</div>'
|
||||
},
|
||||
pruefungAttemptRank(typ) {
|
||||
// ranking of the attempt types so we can compare "höhere" pruefungen
|
||||
switch(typ) {
|
||||
case 'Termin1': return 1
|
||||
case 'Termin2': return 2
|
||||
case 'Termin3': return 3
|
||||
case 'kommPruef': return 4
|
||||
default: return 0
|
||||
}
|
||||
},
|
||||
hasLaterOrHigherPruefung(student, pruefung) {
|
||||
// a Termin2/3 grade may no longer be changed once the student already has a
|
||||
// later-dated exam OR an exam of a higher attempt (e.g. a Termin3/kommPruef
|
||||
// exists while looking at the Termin2). Mirrors the add-button's youngerPruefung guard.
|
||||
if(!pruefung) return false
|
||||
const currentRank = this.pruefungAttemptRank(pruefung.pruefungstyp_kurzbz)
|
||||
const currentDate = (pruefung.datum ?? '').slice(0, 10)
|
||||
|
||||
return (student.pruefungen ?? []).some(p => {
|
||||
if(p === pruefung) return false
|
||||
if(p.pruefung_id != null && pruefung.pruefung_id != null && p.pruefung_id === pruefung.pruefung_id) return false
|
||||
const laterDate = (p.datum ?? '').slice(0, 10) > currentDate
|
||||
const higherRank = this.pruefungAttemptRank(p.pruefungstyp_kurzbz) > currentRank
|
||||
return laterDate || higherRank
|
||||
})
|
||||
},
|
||||
pruefungFormatter(cell) {
|
||||
const data = cell.getData()
|
||||
|
||||
@@ -1152,8 +1319,12 @@ export const Benotungstool = {
|
||||
rowDiv.style.alignItems = 'center';
|
||||
rowDiv.style.height = '100%';
|
||||
|
||||
let attemptLabel = ''
|
||||
let attemptClass = ''
|
||||
if(studentPruefung) {
|
||||
let color = ''
|
||||
|
||||
|
||||
switch(studentPruefung.pruefungstyp_kurzbz) {
|
||||
case 'Termin1':
|
||||
color = 'green'
|
||||
@@ -1169,9 +1340,16 @@ export const Benotungstool = {
|
||||
break
|
||||
}
|
||||
|
||||
rowDiv.style.borderLeft = `4px solid ${color}`;
|
||||
// rowDiv.style.borderLeft = `4px solid ${color}`;
|
||||
rowDiv.style.marginLeft = "6px"; // small indent so text doesn't overlap
|
||||
rowDiv.style.boxSizing = "border-box";
|
||||
|
||||
switch(studentPruefung.pruefungstyp_kurzbz) {
|
||||
case 'Termin1': attemptLabel = '1'; attemptClass = 'attempt-t1'; break
|
||||
case 'Termin2': attemptLabel = '2'; attemptClass = 'attempt-t2'; break
|
||||
case 'Termin3': attemptLabel = '3'; attemptClass = 'attempt-t3'; break
|
||||
case 'kommPruef': attemptLabel = 'K'; attemptClass = 'attempt-k'; break
|
||||
}
|
||||
}
|
||||
|
||||
function createCol(content, classParam) {
|
||||
@@ -1195,6 +1373,9 @@ export const Benotungstool = {
|
||||
|
||||
const noteDefEntry = data.note ? this.notenOptions.find(n => n.note == data[field].note) : null
|
||||
|
||||
if(attemptClass) rowDiv.classList.add('pruefung-badge', attemptClass);
|
||||
if(attemptLabel) rowDiv.setAttribute('data-attempt', attemptLabel);
|
||||
|
||||
// Second column (note_bezeichnung)
|
||||
rowDiv.appendChild(createCol(noteDefEntry.bezeichnung || '', 'col-auto d-flex justify-content-center align-items-center'));
|
||||
|
||||
@@ -1205,10 +1386,18 @@ export const Benotungstool = {
|
||||
}
|
||||
|
||||
if(data[field]?.pruefungstyp_kurzbz !== 'Termin1') {
|
||||
// once a later-dated or higher-attempt pruefung exists (e.g. a Termin3/kommPruef has
|
||||
// already been entered) the GRADE is locked, but the exam DATE may still be corrected
|
||||
// (within the neighbouring exam dates) -> keep the button enabled, lock the note in the modal
|
||||
const noteLocked = this.hasLaterOrHigherPruefung(data, data[field])
|
||||
|
||||
// Third column (button)
|
||||
const button = document.createElement('button');
|
||||
button.className = 'btn btn-outline-secondary';
|
||||
button.textContent = this.$capitalize(this.$p.t('benotungstool/changePruefungButtonText'));
|
||||
if(noteLocked) {
|
||||
button.title = this.$capitalize(this.$p.t('benotungstool/pruefungNoteLockedHint'))
|
||||
}
|
||||
button.addEventListener('click', () => {
|
||||
this.openPruefungModal(data, data[field], field);
|
||||
});
|
||||
@@ -1237,25 +1426,60 @@ export const Benotungstool = {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
parseISODate(iso) {
|
||||
const parts = (iso ?? '').slice(0, 10).split('-')
|
||||
if(parts.length !== 3) return null
|
||||
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))
|
||||
},
|
||||
addDays(date, days) {
|
||||
const d = new Date(date)
|
||||
d.setDate(d.getDate() + days)
|
||||
return d
|
||||
},
|
||||
getPruefungDateBounds(student, pruefung, fallbackDate) {
|
||||
// the exam date must stay strictly between the dates of the chronologically adjacent
|
||||
// pruefungen so the attempt order is preserved. Returns inclusive datepicker bounds.
|
||||
const refDate = (pruefung?.datum ?? fallbackDate ?? '').slice(0, 10)
|
||||
let lower = null, upper = null;
|
||||
(student.pruefungen ?? []).forEach(p => {
|
||||
if(pruefung && p.pruefung_id != null && pruefung.pruefung_id != null && p.pruefung_id === pruefung.pruefung_id) return
|
||||
const d = (p.datum ?? '').slice(0, 10)
|
||||
if(!d) return
|
||||
if(d < refDate) { if(lower === null || d > lower) lower = d }
|
||||
else if(d > refDate) { if(upper === null || d < upper) upper = d }
|
||||
})
|
||||
return {
|
||||
min: lower ? this.addDays(this.parseISODate(lower), 1) : null,
|
||||
max: upper ? this.addDays(this.parseISODate(upper), -1) : null
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
|
||||
|
||||
// grade is locked once a later/higher pruefung exists; only the date may be corrected
|
||||
this.pruefungNoteLocked = !!(pruefung && this.hasLaterOrHigherPruefung(student, pruefung))
|
||||
|
||||
// constrain the date to stay between the neighbouring exam dates
|
||||
const bounds = this.getPruefungDateBounds(student, pruefung, field)
|
||||
this.pruefungDateMin = bounds.min
|
||||
this.pruefungDateMax = bounds.max
|
||||
|
||||
|
||||
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) {
|
||||
@@ -1326,7 +1550,8 @@ export const Benotungstool = {
|
||||
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
|
||||
// fallback label; the full label incl. Antritte is set once hoechsterAntritt is known (below)
|
||||
s.infoString = `${s.uid} – ${s.nachname} ${s.vorname}`// used for multiselect (uid first); full label incl. Antritte set below
|
||||
})
|
||||
this.pruefungen = data[1] ?? []
|
||||
this.domain = data[2]
|
||||
@@ -1384,6 +1609,9 @@ export const Benotungstool = {
|
||||
})
|
||||
|
||||
s.hoechsterAntritt = this.getAntrittCountStudent(s)
|
||||
// multiselect label, uid first: "uid – Nachname Vorname – Antritte: n"
|
||||
// (order in the dropdown mirrors the table, see getStudentenOptions)
|
||||
s.infoString = `${s.uid} – ${s.nachname} ${s.vorname} – ${this.$capitalize(this.$p.t('benotungstool/c4antrittCountv2'))}: ${s.hoechsterAntritt}`
|
||||
s.email = this.buildMailToLink(s)
|
||||
s.lv_note = this.teilnoten[s.uid].note_lv
|
||||
s.freigabedatum = this.parseDate(this.teilnoten[s.uid]['freigabedatum'])
|
||||
@@ -1396,7 +1624,8 @@ export const Benotungstool = {
|
||||
s.teilnote = ''
|
||||
s.mobility_zusatz = this.teilnoten[s.uid].mobility_zusatz
|
||||
grades.forEach(g => {
|
||||
const notenOption = this.notenOptions.find(n=>n.note == g.grade)
|
||||
// some moodle noten are numeric, some are strings like "Sehr Gut", "Bestanden" etc...
|
||||
const notenOption = this.notenOptions.find(n=>n.note == g.grade || n.bezeichnung == g.grade)
|
||||
if(notenOption.positiv) s.teilnote += ('<span>'+g.text +'</span>'+ '<br/>')
|
||||
else s.teilnote += ('<span style="color: red;">'+g.text +'</span>'+ '<br/>')
|
||||
})
|
||||
@@ -1503,19 +1732,33 @@ export const Benotungstool = {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// reflect the sticky-column selection on the freshly (re)built columns
|
||||
this.applyStickyColumnState()
|
||||
|
||||
// refresh the "neue Prüfung" dropdown options for the freshly loaded data
|
||||
this.tableVersion++
|
||||
|
||||
// keep the loading overlay up until the browser has actually painted the rebuilt table
|
||||
// (the caller, loadNoten, awaits setupData before clearing `loading`)
|
||||
await new Promise(requestAnimationFrame)
|
||||
},
|
||||
loadNoten(lv_id, sem_kurzbz) {
|
||||
if (!lv_id || !sem_kurzbz) return
|
||||
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)
|
||||
.then(async res => {
|
||||
if(res?.data) await this.setupData(res.data)
|
||||
else {
|
||||
this.$fhcAlert.alertError('no data found')
|
||||
this.$refs.notenTable.tabulator.setData([]);
|
||||
this.$refs.notenTable.tabulator.redraw(true);
|
||||
}
|
||||
if(res?.meta?.getExternalGradesError) this.$fhcAlert.alertError(this.$p.t('benotungstool/c4moodleTeilnotenError', [res.meta.getExternalGradesError]))
|
||||
}).finally(()=> {
|
||||
this.loading = false
|
||||
})
|
||||
@@ -1582,7 +1825,7 @@ export const Benotungstool = {
|
||||
LehreinheitenModule.bindParams(Vue.ref(Vue.computed(() => this.LeDropdownParams)));
|
||||
|
||||
// fetch noten dropdown
|
||||
this.$api.call(ApiNoten.getNoten()).then(async res => {
|
||||
await this.$api.call(ApiNoten.getNoten()).then(async res => {
|
||||
this.notenOptions = res.data
|
||||
this.notenOptionsLehre = res.data.filter(n => n.lehre === true)
|
||||
|
||||
@@ -1634,7 +1877,7 @@ export const Benotungstool = {
|
||||
if (lvId) {
|
||||
this.loadNoten(lvId, sem)
|
||||
} else if (this.$refs.notenTable?.tabulator) {
|
||||
this.$refs.notenTable.tabulator.setData([]) // no matching LV -> clear
|
||||
this.$refs.notenTable.tabulator.setData([])
|
||||
}
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
@@ -1664,18 +1907,30 @@ export const Benotungstool = {
|
||||
}
|
||||
},
|
||||
savePruefungEingabe() {
|
||||
// keep the date within the neighbouring exam dates (a typed value can bypass the picker bounds)
|
||||
if((this.pruefungDateMin && this.selectedPruefungDate < this.pruefungDateMin) ||
|
||||
(this.pruefungDateMax && this.selectedPruefungDate > this.pruefungDateMax)) {
|
||||
this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('benotungstool/pruefungDatumOutOfRangeHint')))
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// when the grade is locked (later/higher pruefung exists) keep the existing note untouched
|
||||
const note = this.pruefungNoteLocked && this.pruefung
|
||||
? this.pruefung.note
|
||||
: (this.selectedPruefungNote?.note ?? 9) // noch nicht eingetragen
|
||||
const punkte = this.pruefungNoteLocked && this.pruefung
|
||||
? (this.pruefung.punkte ?? 0)
|
||||
: (this.selectedPruefungPunkte ?? 0)
|
||||
|
||||
this.loading = true
|
||||
this.$api.call(ApiNoten.saveStudentPruefung(
|
||||
this.pruefungStudent.uid,
|
||||
@@ -1685,7 +1940,8 @@ export const Benotungstool = {
|
||||
this.lv_id,
|
||||
this.pruefungStudent.lehreinheit_id,
|
||||
this.sem_kurzbz,
|
||||
typ
|
||||
typ,
|
||||
this.pruefung?.pruefung_id ?? null // null = adding a new pruefung, otherwise edit of this record
|
||||
)).then(res => {
|
||||
if(res.meta.status === 'success') { //'Prüfung für Student ' + this.pruefungStudent.uid + ' bearbeitet oder angelegt'
|
||||
this.$fhcAlert.alertDefault(
|
||||
@@ -1873,11 +2129,13 @@ export const Benotungstool = {
|
||||
return !(kP || student.hoechsterAntritt >= vueThis.maxAntrittCount)
|
||||
},
|
||||
set() {
|
||||
// empty setter so tabulator doesnt scream
|
||||
// empty setter so tabulator doesnt scream
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
})
|
||||
// a student's selectability may have changed -> refresh the "neue Prüfung" dropdown options
|
||||
this.tableVersion++
|
||||
},
|
||||
saveNoteneingabe() {
|
||||
this.loading = true
|
||||
@@ -1913,6 +2171,9 @@ export const Benotungstool = {
|
||||
openNewPruefungsdatumModal() {
|
||||
this.$refs.modalContainerNeuesPruefungsdatum.show()
|
||||
},
|
||||
openPruefungImportModal() {
|
||||
this.$refs.modalContainerPruefungImport.show()
|
||||
},
|
||||
openNotenImportModal() {
|
||||
this.$refs.modalContainerNotenImport.show()
|
||||
},
|
||||
@@ -2033,6 +2294,7 @@ export const Benotungstool = {
|
||||
for(let i = 0; i < pLen; i++) {
|
||||
const p = student.pruefungen[i]
|
||||
|
||||
// TODO: filter for limit here but in a perfect world they should never be able to exceed that anyway
|
||||
const isDefinedAsAntrittsloseNote = this.config.NOTEN_OHNE_ANTRITT.find(n_pk => n_pk == p.note)
|
||||
if(!isDefinedAsAntrittsloseNote) pruefungsAntrittCount++
|
||||
}
|
||||
@@ -2074,11 +2336,18 @@ export const Benotungstool = {
|
||||
});
|
||||
},
|
||||
selectedLehreinheit(newVal) {
|
||||
if(!this.$refs.notenTable) return
|
||||
this.$refs.notenTable.tabulator.getFilters()
|
||||
.filter(f => f.field === 'lehreinheit_id')
|
||||
.forEach(() => table.removeFilter("lehreinheit_id", "=", undefined))
|
||||
if(newVal) this.$refs.notenTable.tabulator.setFilter("lehreinheit_id", "=", newVal.lehreinheit_id);
|
||||
const table = this.$refs.notenTable?.tabulator
|
||||
if (!table) return
|
||||
|
||||
const others = table.getFilters().filter(f => f.field !== 'lehreinheit_id')
|
||||
|
||||
table.clearFilter()
|
||||
|
||||
const next = newVal
|
||||
? [...others, { field: 'lehreinheit_id', type: '=', value: newVal.lehreinheit_id }]
|
||||
: others
|
||||
|
||||
if (next.length) table.setFilter(next)
|
||||
},
|
||||
selectedLehrveranstaltung(newVal, oldVal) {
|
||||
if (this.selectedLehreinheit) {
|
||||
@@ -2094,6 +2363,39 @@ export const Benotungstool = {
|
||||
const kommPruefCol = this.$refs.notenTable?.tabulator.getColumn("kommPruef")
|
||||
kommPruefCol.hide()
|
||||
}
|
||||
},
|
||||
selectedPruefungNote(newVal, oldVal) {
|
||||
if (!newVal || !this.pruefungStudent) return
|
||||
|
||||
const limitMap = this.config?.NOTEN_OCCURANCE_LIMIT_MAP
|
||||
if (!limitMap) return
|
||||
console.log(limitMap)
|
||||
|
||||
const note = newVal.note
|
||||
const limit = limitMap[note]
|
||||
if (limit == null) return
|
||||
|
||||
// all fixed antritte: Termin1/2/3 sit in pruefungen, kommPruef is separate
|
||||
const allPruefungen = [...this.pruefungStudent.pruefungen]
|
||||
if (this.pruefungStudent.kommPruef) allPruefungen.push(this.pruefungStudent.kommPruef)
|
||||
|
||||
// count existing occurrences of this note, excluding the pruefung being edited
|
||||
// (its note is about to be replaced by this very selection)
|
||||
const existingCount = allPruefungen.reduce((acc, p) => {
|
||||
if (this.pruefung && p.pruefung_id === this.pruefung.pruefung_id) return acc
|
||||
if (p.note == note) acc++
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
// this selection adds one more occurrence -> would it cross the limit?
|
||||
if (existingCount + 1 > limit) {
|
||||
// TODO: phrase
|
||||
this.$fhcAlert.alertWarning(
|
||||
'Note "' + newVal.bezeichnung + '" darf bei ' + this.pruefungStudent.uid +
|
||||
' maximal ' + limit + ' mal vergeben werden. Auswahl wurde zurückgesetzt.'
|
||||
)
|
||||
this.selectedPruefungNote = oldVal // revert to last valid choice
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -2133,7 +2435,20 @@ export const Benotungstool = {
|
||||
}
|
||||
},
|
||||
getStudentenOptions() {
|
||||
return this.studenten ? this.studenten.filter(s => s.selectable) : []
|
||||
// the "neue Prüfung" multiselect mirrors the table: same row order (current sort + filter)
|
||||
// and the same selectable set. tableVersion is bumped on sort/filter/data changes so this
|
||||
// recomputes whenever the table order changes.
|
||||
const _ = this.tableVersion // reactive dependency
|
||||
const table = this.$refs.notenTable?.tabulator
|
||||
|
||||
if(!table) {
|
||||
return this.studenten ? this.studenten.filter(s => s.selectable) : []
|
||||
}
|
||||
|
||||
// getRows("active") returns the rows in their current display order (after sort + filter)
|
||||
return table.getRows("active")
|
||||
.map(r => r.getData())
|
||||
.filter(s => s.selectable)
|
||||
},
|
||||
getKommPruefCount(){
|
||||
let counter = 0
|
||||
@@ -2168,8 +2483,20 @@ export const Benotungstool = {
|
||||
getNotenfreigabeHinweistext() {
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenfreigabeHinweistextv4'))
|
||||
},
|
||||
getPruefungimportHinweistext() {
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv6'))
|
||||
},
|
||||
getNotenimportHinweistext() {
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv5'))
|
||||
},
|
||||
freezableColumnOptions() {
|
||||
// the identity columns the user may pin to the left (matches freezableColumnFields)
|
||||
return [
|
||||
{ field: 'selectCol', label: this.$capitalize(this.$p.t('benotungstool/c4selection')) },
|
||||
{ field: 'uid', label: 'UID' },
|
||||
{ field: 'vorname', label: this.$capitalize(this.$p.t('benotungstool/c4vorname')) },
|
||||
{ field: 'nachname', label: this.$capitalize(this.$p.t('benotungstool/c4nachname')) }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -2179,6 +2506,31 @@ export const Benotungstool = {
|
||||
this.setupMounted()
|
||||
},
|
||||
template: `
|
||||
<bs-modal ref="modalContainerPruefungImport" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4">
|
||||
<template v-slot:title>{{$capitalize($p.t('benotungstool/c4pruefungImportieren'))}}</template>
|
||||
<template v-slot:default>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12" v-html="getPruefungimportHinweistext"></div>
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-12">
|
||||
<Textarea v-model="importString" rows="5" class="w-100" :placeholder="$p.t('benotungstool/c4importPlaceholder')"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-12">
|
||||
<NotenlisteLinks
|
||||
:lehrveranstaltung="selectedLehrveranstaltung"
|
||||
:sem_kurzbz="selectedSemester?.studiensemester_kurzbz"
|
||||
:selected-lehreinheit="selectedLehreinheit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button type="button" class="btn btn-primary" @click="importPruefungen">{{ $capitalize($p.t('benotungstool/c4import')) }}</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
|
||||
<bs-modal ref="modalContainerNotenImport" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4">
|
||||
<template v-slot:title>{{$capitalize($p.t('benotungstool/c4notenImportieren'))}}</template>
|
||||
<template v-slot:default>
|
||||
@@ -2187,7 +2539,15 @@ export const Benotungstool = {
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-12">
|
||||
<Textarea v-model="importString" rows="5" class="w-100"></Textarea>
|
||||
<Textarea v-model="importStringNoten" rows="5" class="w-100" :placeholder="$p.t('benotungstool/c4importNotePlaceholder')"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-12">
|
||||
<NotenlisteLinks
|
||||
:lehrveranstaltung="selectedLehrveranstaltung"
|
||||
:sem_kurzbz="selectedSemester?.studiensemester_kurzbz"
|
||||
:selected-lehreinheit="selectedLehreinheit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2208,6 +2568,7 @@ export const Benotungstool = {
|
||||
v-model="selectedPruefungDate"
|
||||
:clearable="false"
|
||||
format="dd.MM.yyyy"
|
||||
placeholder="TT.MM.JJJJ"
|
||||
:enableTimePicker="false"
|
||||
:text-input="true"
|
||||
:auto-apply="true">
|
||||
@@ -2262,17 +2623,24 @@ export const Benotungstool = {
|
||||
:clearable="false"
|
||||
:enableTimePicker="false"
|
||||
format="dd.MM.yyyy"
|
||||
placeholder="TT.MM.JJJJ"
|
||||
:min-date="pruefungDateMin"
|
||||
:max-date="pruefungDateMax"
|
||||
:text-input="true"
|
||||
:auto-apply="true">
|
||||
</datepicker>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="pruefungNoteLocked" class="row mt-2 justify-content-center">
|
||||
<div class="col-9 text-center text-muted small">{{$capitalize($p.t('benotungstool/pruefungNoteLockedHint'))}}</div>
|
||||
</div>
|
||||
<div v-if="config?.CIS_GESAMTNOTE_PUNKTE == true" class="row mt-3 align-items-center justify-content-center">
|
||||
<div class="col-3 text-center">{{$capitalize($p.t('benotungstool/c4punkte'))}}:</div>
|
||||
<div class="col-6">
|
||||
<InputNumber
|
||||
v-model="selectedPruefungPunkte"
|
||||
@input="debouncedFetchPunkteForPruefung"
|
||||
:disabled="pruefungNoteLocked"
|
||||
inputId="selectedPruefungInput" :min="0" :max="100000"
|
||||
class="w-100">
|
||||
</InputNumber>
|
||||
@@ -2282,7 +2650,7 @@ export const Benotungstool = {
|
||||
<div class="col-3 text-center">{{$capitalize($p.t('lehre/note'))}}:</div>
|
||||
<div class="col-6">
|
||||
<Dropdown :placeholder="$capitalize($p.t('lehre/note'))"
|
||||
:disabled="config?.CIS_GESAMTNOTE_PUNKTE == true"
|
||||
:disabled="config?.CIS_GESAMTNOTE_PUNKTE == true || pruefungNoteLocked"
|
||||
:style="{'width': '100%'}" :optionLabel="getOptionLabelNotePruefung"
|
||||
v-model="selectedPruefungNote" :options="notenOptionsLehre" showClear>
|
||||
<template #optionsgroup="slotProps">
|
||||
@@ -2316,10 +2684,10 @@ export const Benotungstool = {
|
||||
|
||||
<FhcOverlay :active="loading"></FhcOverlay>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="row">
|
||||
<div class="col-auto me-auto">
|
||||
<h2>{{$capitalize($p.t('benotungstool/benotungstoolTitle'))}}</h2>
|
||||
<h4>{{ selectedLehrveranstaltung?.lv_bezeichnung }}</h4>
|
||||
<h5>{{ selectedLehrveranstaltung?.lv_bezeichnung }}</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-none d-xxl-flex">
|
||||
@@ -2339,7 +2707,7 @@ export const Benotungstool = {
|
||||
<label class="col-form-label">{{$capitalize($p.t('lehre/lehreinheit'))}}:</label>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<Dropdown @change="leChanged" :style="{'width': '100%'}" v-bind="LehreinheitenModule"
|
||||
<Dropdown :style="{'width': '100%'}" v-bind="LehreinheitenModule"
|
||||
v-model="selectedLehreinheit" showClear appendTo="self">
|
||||
<template #option="slotProps">
|
||||
<div>
|
||||
@@ -2365,7 +2733,6 @@ export const Benotungstool = {
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div id="notentable" class="row" :style="'overflow-x: auto;'">
|
||||
<core-filter-cmpt
|
||||
@@ -2382,14 +2749,27 @@ export const Benotungstool = {
|
||||
:sideMenu="false"
|
||||
>
|
||||
<template #actions>
|
||||
|
||||
|
||||
<Multiselect
|
||||
v-model="stickyColumnSelection"
|
||||
:options="freezableColumnOptions"
|
||||
optionLabel="label" optionValue="field"
|
||||
:placeholder="$capitalize($p.t('benotungstool/freezeColumnsToggle'))"
|
||||
:maxSelectedLabels="0"
|
||||
:selectedItemsLabel="$capitalize($p.t('benotungstool/freezeColumnsLabel'))"
|
||||
showToggleAll
|
||||
@change="onStickySelectionChange"
|
||||
class="ml-2"
|
||||
style="min-width: 12rem" />
|
||||
|
||||
<button @click="openNewPruefungsdatumModal" role="button" :class="getNewBtnClass">
|
||||
{{$capitalize($p.t('benotungstool/c4addNewPruefung'))}} <i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
||||
<Divider layout="vertical" style="transform: translateY(12px)"/>
|
||||
|
||||
<button @click="openNotenImportModal" role="button" :class="getNotenImportBtnClass">
|
||||
<button v-if="config?.CIS_GESAMTNOTE_PRUEFUNGSIMPORT" @click="openPruefungImportModal" role="button" :class="getNotenImportBtnClass">
|
||||
{{$capitalize($p.t('benotungstool/c4pruefungImportieren'))}} <i class="fa fa-file-import"></i>
|
||||
</button>
|
||||
<button v-if="config?.CIS_GESAMTNOTE_NOTENIMPORT" @click="openNotenImportModal" role="button" :class="getNotenImportBtnClass">
|
||||
{{$capitalize($p.t('benotungstool/c4notenImportieren'))}} <i class="fa fa-file-import"></i>
|
||||
</button>
|
||||
<button @click="openSaveModal" role="button" :class="getSaveBtnClass">
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// components/Lehre/Benotungstool/NotenlisteLinks.js
|
||||
import LehreinheitenModule from '../../DropdownModes/LehreinheitenModule.js';
|
||||
|
||||
export const NotenlisteLinks = {
|
||||
name: "NotenlisteLinks",
|
||||
props: {
|
||||
lehrveranstaltung: { type: Object, default: null },
|
||||
sem_kurzbz: { type: String, default: null },
|
||||
selectedLehreinheit: { type: Object, default: null }
|
||||
},
|
||||
computed: {
|
||||
LehreinheitenModule() {
|
||||
return LehreinheitenModule;
|
||||
},
|
||||
lehreinheiten() {
|
||||
// reuse the already-loaded LE options from the shared module
|
||||
const all = LehreinheitenModule.options ?? [];
|
||||
// when a single Lehreinheit is selected, restrict the list to just that one
|
||||
if (this.selectedLehreinheit?.lehreinheit_id != null) {
|
||||
return all.filter(le => le.lehreinheit_id === this.selectedLehreinheit.lehreinheit_id);
|
||||
}
|
||||
return all;
|
||||
},
|
||||
baseUrl() {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/lehre/notenliste.xls.php';
|
||||
},
|
||||
ready() {
|
||||
const lv = this.lehrveranstaltung;
|
||||
return !!(lv && lv.studiengang_kz != null && lv.lv_semester != null
|
||||
&& lv.lehrveranstaltung_id != null && this.sem_kurzbz);
|
||||
},
|
||||
gesamtUrl() {
|
||||
if (!this.ready) return null;
|
||||
return this.buildUrl();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildUrl(lehreinheit_id = null) {
|
||||
const lv = this.lehrveranstaltung;
|
||||
const params = new URLSearchParams({
|
||||
stg: lv.studiengang_kz,
|
||||
sem: lv.lv_semester,
|
||||
lvid: lv.lehrveranstaltung_id,
|
||||
stsem: this.sem_kurzbz
|
||||
});
|
||||
if (lehreinheit_id != null) params.set('lehreinheit_id', lehreinheit_id);
|
||||
return this.baseUrl + '?' + params.toString();
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div v-if="ready">
|
||||
<div class="fw-bold mb-2">{{ $capitalize($p.t('benotungstool/c4notenlisten')) }}</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<a class="Item" :href="gesamtUrl" target="_blank" rel="noopener">
|
||||
{{ $capitalize($p.t('benotungstool/c4gesamtliste')) }} {{ lehrveranstaltung.lv_bezeichnung }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-for="le in lehreinheiten" :key="le.lehreinheit_id" class="mb-1" style="padding-left: 1.5rem;">
|
||||
<a class="Item" :href="buildUrl(le.lehreinheit_id)" target="_blank" rel="noopener">
|
||||
{{ le.infoString }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-muted">
|
||||
{{ $capitalize($p.t('benotungstool/c4keineStudentenGefunden')) }}
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
export default NotenlisteLinks;
|
||||
+355
-5
@@ -59829,6 +59829,26 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4pruefungImportieren',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Prüfungsimport',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Import Exams',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
@@ -60609,6 +60629,226 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'pruefungNoteLocked',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die Note für Studierenden {0} kann nicht mehr geändert werden, da bereits eine spätere Prüfung oder eine Prüfung mit höherem Antritt existiert. Das Prüfungsdatum kann weiterhin angepasst werden.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The grade for student {0} can no longer be changed because a later exam or an exam of a higher attempt already exists. The exam date can still be adjusted.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'pruefungNoteLockedHint',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Note gesperrt (spätere Prüfung bzw. höherer Antritt vorhanden) – nur das Prüfungsdatum kann angepasst werden.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Grade locked (a later exam or higher attempt exists) – only the exam date can be adjusted.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'pruefungDatumOutOfRange',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Das Prüfungsdatum für Studierenden {0} muss zwischen dem vorherigen und dem nachfolgenden Prüfungstermin liegen.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The exam date for student {0} must be between the previous and the following exam date.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'pruefungDatumOutOfRangeHint',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Das Prüfungsdatum muss zwischen dem vorherigen und dem nachfolgenden Prüfungstermin liegen.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The exam date must be between the previous and the following exam date.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'freezeColumnsToggle',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Spalten fixieren',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Freeze columns',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'freezeColumnsLabel',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '{0} Spalten fixiert',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => '{0} columns frozen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4selection',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Auswahl',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Selection',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4importRowNotDateFormat',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Zeile {0} wurde übersprungen: erwartet wird Personenkennzeichen, Datum (TT.MM.JJJJ) und Note (Tab-getrennt).',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Row {0} was skipped: expected Person ID, date (DD.MM.YYYY) and grade (tab separated).',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4importPlaceholder',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Personenkennzeichen ⇥ TT.MM.JJJJ ⇥ Note',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Person ID ⇥ DD.MM.YYYY ⇥ Grade',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4importNotePlaceholder',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Personenkennzeichen ⇥ Note',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Person ID ⇥ Grade',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4importRowNotNoteFormat',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Zeile {0} wurde übersprungen: erwartet wird Personenkennzeichen und Note (Tab-getrennt).',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Row {0} was skipped: expected Person ID and grade (tab separated).',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
@@ -60664,19 +60904,19 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '• Laden Sie sich die Notenliste im Excel-Format unter CIS → Lehrveranstaltungen → Anwesenheits- und Notenlisten → Notenliste herunter.<br>
|
||||
• Tragen Sie die Noten in das Dokument und speichern Sie dieses.<br>
|
||||
• Markieren Sie im Excel-Dokument die Inhalte der Spalten Personenkennzeichen und Note für jene Studierende, deren Noten Sie importieren möchten (ohne Überschrift !)<br>
|
||||
• Tragen Sie die Noten in das Dokument ein.<br>
|
||||
• Markieren Sie die Inhalte der Spalten Personenkennzeichen und Note für jene Studierende, deren Noten Sie importieren möchten (ohne Überschrift !)<br>
|
||||
• Kopieren Sie die markierten Inhalte mittels strg + c oder Bearbeiten → Kopieren in die Zwischenablage<br>
|
||||
• Einfügen der Inhalte mittels strg + v oder Bearbeiten → Einfügen<br>
|
||||
• Mit einem Klick auf Import werden die Noten übernommen.<br>',
|
||||
• Mit einem Klick auf Importieren werden die Noten übernommen.<br>',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => '• Download the grade list in Excel format from CIS → Courses → Attendance and Grade Lists → Grade List.<br>
|
||||
• Enter the grades into the document and save it.<br>
|
||||
• 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!).<br>
|
||||
• Enter the grades into the document.<br>
|
||||
• Select the contents of the Person ID and Grade columns for the students whose grades you want to import (without headings!).<br>
|
||||
• Copy the selected content to the clipboard using Ctrl + c or Edit → Copy.<br>
|
||||
• Paste the content using Ctrl + v or Edit → Paste.<br>
|
||||
• Click Import to import the grades.<br>',
|
||||
@@ -60685,6 +60925,36 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'notenimportHinweistextv6',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '• Laden Sie sich die Notenliste im Excel-Format unter CIS → Lehrveranstaltungen → Anwesenheits- und Notenlisten → Notenliste herunter.<br>
|
||||
• Tragen Sie pro Studierendem das Prüfungsdatum und die Note ein.<br>
|
||||
• Jeder Import legt eine datierte Prüfung an (Prüfungsimport) – das Datum ist daher verpflichtend.<br>
|
||||
• Format pro Zeile (Tab-getrennt, ohne Überschrift): Personenkennzeichen ⇥ Datum (TT.MM.JJJJ) ⇥ Note<br>
|
||||
• Markieren Sie die entsprechenden Spalten, kopieren Sie sie (Strg + c) und fügen Sie sie oben ein (Strg + v).<br>
|
||||
• Mit einem Klick auf Importieren werden die Prüfungen übernommen.<br>',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => '• Download the grade list in Excel format from CIS → Courses → Attendance and Grade Lists → Grade List.<br>
|
||||
• For each student, enter the exam date and the grade.<br>
|
||||
• Every import creates a dated exam (exam import) – the date is therefore mandatory.<br>
|
||||
• Format per line (tab separated, without headings): Person ID ⇥ date (DD.MM.YYYY) ⇥ grade<br>
|
||||
• Select the relevant columns, copy them (Ctrl + c) and paste them above (Ctrl + v).<br>
|
||||
• Click Import to import the exams.<br>',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
@@ -60845,6 +61115,86 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4moodleTeilnotenError',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Moodle Addon Teilnotenimport fehlgeschlagen: {0}',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Moodle Addon partial grades import failed: {0}',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4notenlisten',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Notenlisten',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Grading Sheets',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4gesamtliste',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Gesamtliste',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Full Course Grading Sheet',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4keineStudentenGefunden',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Keine Studenten gefunden.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'No students found.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
// CIS4 GESAMTNOTENEINGABE ENDE ------------------------------------------------------------------------------------
|
||||
//**************************** Pruefungsprotokolle start
|
||||
array(
|
||||
|
||||
Reference in New Issue
Block a user