mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 06:54:04 +00:00
adapt import to also parse note.anmerkung (entschuldigt - en, nicht beurteilt - nb, etc) if configured; new config if the benotungstool is allowed to create kommissionelle prüfungen or if that is a job for assistenz in stv; coded a kommPruef as the last pruefungsattempt and not a special case so it follows the same ordering rules in the table; changed headers from "Antritte" to "Prüfungstermine" to keep the wording aligned with the domain objects definitions; tooltip explaining the current antritt while hovering over the badge (1,2,3-K) (maybe make the badges contents configurable?); changed the notenOption sort to resemble stv: order 1-5 by value and afterwards alphabetically; actually removed redundant configs for defining which noten count for antritte -> only go by bezeichnung and fetch the matching primary keys from each db as they can have mixed pk's; vorschlag übernehmen now upserts the first prüfungsattempt, aswell as the notenfreigabe(if no other pruefungen exist) which should fix the possibility of creating a pruefungseries that starts with termin2, vorschlag also carries a datepicker writing the date of that first prüfung but not the benotungsdatum -> that needs to stay the entry date since the freigabemechanismus hinges on it; added a flag handling the password inputs browser prompt to not appear on other inputs; WIP coming up with even more configurability in order to cover uncertain workflow this tool is supposed to represent;
This commit is contained in:
@@ -2,26 +2,18 @@
|
||||
|
||||
if (!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
// Noten die keinen Prüfungsantritt verbrauchen. Fallback-PKs; massgeblich sind die per Bezeichnung
|
||||
// aufgelösten Noten (NOTEN_OHNE_ANTRITT_BEZEICHNUNGEN), da die PKs je Installation abweichen.
|
||||
$config['NOTEN_OHNE_ANTRITT'] = [9, 17]; // tbl_note pk
|
||||
// Noten, die keinen Prüfungsantritt verbrauchen.
|
||||
$config['NOTEN_OHNE_ANTRITT_BEZEICHNUNGEN'] = ['Noch nicht eingetragen', 'entschuldigt'];
|
||||
|
||||
// 'Nicht beurteilt' ist eine Altlast: formal eine Note, inhaltlich keine Leistungsfeststellung.
|
||||
// Wird weiterhin häufig vergeben und darf daher keinen Antritt kosten.
|
||||
$config['NOTEN_OHNE_ANTRITT_BEZEICHNUNGEN'] = ['Noch nicht eingetragen', 'entschuldigt', 'Nicht beurteilt'];
|
||||
|
||||
// Anrechnungsnoten: die Leistung wurde vorab anerkannt, der/die Studierende ist zwar Teil der
|
||||
// Lehrveranstaltung, tritt aber zu keiner Prüfung an. Solange die ZEUGNISNOTE eine davon ist, sind
|
||||
// für diese Lehrveranstaltung keine Prüfungen möglich. Auflösung wie oben über die Bezeichnung,
|
||||
// die PKs sind nur Fallback.
|
||||
// Anrechnungsnoten: die Leistung wurde vorab anerkannt.
|
||||
$config['NOTEN_ANRECHNUNG_BEZEICHNUNGEN'] = ['angerechnet', 'intern angerechnet'];
|
||||
$config['NOTEN_ANRECHNUNG'] = [6, 16]; // tbl_note pk
|
||||
|
||||
$config['NOTEN_OCCURANCE_LIMIT_MAP'] = [17 => 1]; // über alle Antritte hinweg nur ein entschuldigt
|
||||
// Wie oft eine Note über alle Antritte hinweg vorkommen darf. Schlüssel ist die Bezeichnung.
|
||||
$config['NOTEN_OCCURANCE_LIMIT_MAP'] = ['entschuldigt' => 1];
|
||||
|
||||
// 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;
|
||||
// Die Note 'entschuldigt'. Ein entschuldigter Termin bleibt als eigene datierte Zeile erhalten,
|
||||
// wenn eine neue Prüfung desselben Typs entsteht.
|
||||
$config['NOTE_ENTSCHULDIGT_BEZEICHNUNG'] = 'entschuldigt';
|
||||
|
||||
// The maximum number of attempts that count, the first attempt and the kommissionelle attempt
|
||||
// included. null derives the number from the old flags:
|
||||
@@ -36,6 +28,12 @@ $config['PRUEFUNG_KOMMISSIONELL_TYPEN'] = ['kommPruef', 'zusKommPruef'];
|
||||
// The type that the tool writes for the last attempt. The last attempt is always kommissionell.
|
||||
$config['PRUEFUNG_TYP_KOMMISSIONELL'] = 'kommPruef';
|
||||
|
||||
// The Benotungstool may create the kommissionelle Prüfung itself. Some installations enter it in
|
||||
// another tool. The tool SHOWS an existing kommissionelle Prüfung either way; false blocks the
|
||||
// creation only: the cell offers no button, and the dialog, the bulk entry and the import refuse
|
||||
// the row. The chain then stops one attempt earlier for this tool.
|
||||
$config['CIS_GESAMTNOTE_ALLOW_CREATE_KOMMPRUEF'] = true;
|
||||
|
||||
// Exam types that never use an attempt. A zusKommPruef repeats a kommissionelle Prüfung that had
|
||||
// a procedural fault. It stands outside the attempt chain, and the student administration enters
|
||||
// it. The examination rules do not describe it.
|
||||
@@ -55,6 +53,11 @@ $config['CIS_GESAMTNOTE_ERSTANTRITT_BEI_UEBERNAHME'] = true;
|
||||
$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)
|
||||
|
||||
// The grade column of an imported row. false accepts the note itself (the primary key of
|
||||
// lehre.tbl_note) only. true also accepts the shorthand from lehre.tbl_note.anmerkung, which the
|
||||
// Excel grade list uses for the special grades ('nb', 'ea', 'en').
|
||||
$config['CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL'] = false;
|
||||
|
||||
// Noteneintragungsfrist (Prüfungsordnung §1): grade/pruefung entry is only permitted up to this
|
||||
// deadline. The month/day below is applied to the studiensemester's year:
|
||||
// Sommersemester (SSyyyy) -> deadline in the SAME calendar year (default 15th November)
|
||||
|
||||
@@ -93,7 +93,8 @@ class Noten extends FHCAPI_Controller
|
||||
}
|
||||
|
||||
public function getCisConfig() {
|
||||
// resolved from tbl_note (Bezeichnung) with config fallback -> single source of truth
|
||||
// The configuration names the special grades, tbl_note gives their keys. The client compares
|
||||
// keys, therefore the answer carries the resolved keys, never a Bezeichnung.
|
||||
$special = $this->VerlaufLib->getSpecialNotes();
|
||||
$NOTEN_OHNE_ANTRITT = $special['ohneAntritt'];
|
||||
$NOTEN_OCCURANCE_LIMIT_MAP = $special['limitMap'];
|
||||
@@ -117,8 +118,9 @@ class Noten extends FHCAPI_Controller
|
||||
// the default column layout ('antritt' or 'datum'); the user can change it in the tool
|
||||
'CIS_GESAMTNOTE_PRUEFUNGSSPALTEN' => $this->config->item('CIS_GESAMTNOTE_PRUEFUNGSSPALTEN'),
|
||||
|
||||
// used to toggle availability of kommPruef type pruefungen
|
||||
'CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF' => CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF,
|
||||
// may this tool CREATE the kommissionelle Prüfung (application/config/noten.php)?
|
||||
// It always shows one that exists.
|
||||
'CIS_GESAMTNOTE_ALLOW_CREATE_KOMMPRUEF' => $this->VerlaufLib->darfKommPruefAnlegen(),
|
||||
|
||||
//technically exists but is never used, could be LE pendant to next flag
|
||||
// 'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_NOTE,
|
||||
@@ -134,6 +136,9 @@ class Noten extends FHCAPI_Controller
|
||||
// true they are shown separately
|
||||
'CIS_GESAMTNOTE_PRUEFUNGSIMPORT' => $this->config->item('CIS_GESAMTNOTE_PRUEFUNGSIMPORT'),
|
||||
'CIS_GESAMTNOTE_NOTENIMPORT' => $this->config->item('CIS_GESAMTNOTE_NOTENIMPORT'),
|
||||
|
||||
// does an imported row accept the shorthand from tbl_note.anmerkung as the grade?
|
||||
'CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL' => (bool) $this->config->item('CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL'),
|
||||
|
||||
// send a mail when approving grades
|
||||
'CIS_GESAMTNOTE_FREIGABEMAIL_NOTE' => CIS_GESAMTNOTE_FREIGABEMAIL_NOTE,
|
||||
@@ -975,6 +980,8 @@ class Noten extends FHCAPI_Controller
|
||||
'terminal' => $verlauf->terminal,
|
||||
'erstantrittMoeglich' => $verlauf->erstantrittMoeglich,
|
||||
'naechsteRolle' => $verlauf->naechsteRolle,
|
||||
// the next attempt is kommissionell and this tool may not create it
|
||||
'kommPruefGesperrt' => $verlauf->kommPruefGesperrt,
|
||||
// credited: the row is visible, but you cannot select it and it gets no exams
|
||||
'angerechnet' => $verlauf->angerechnet,
|
||||
'hatLvNote' => $hatLvNote // ungefiltert, also inklusive noch nicht freigegebener
|
||||
@@ -1003,7 +1010,9 @@ class Noten extends FHCAPI_Controller
|
||||
$pruefungen = $this->VerlaufLib->getPruefungen($student_uid, $lva_id, $stsem);
|
||||
if(count($pruefungen) === 0) return null;
|
||||
|
||||
$verlauf = $this->VerlaufLib->buildVerlauf($pruefungen, null, $zeugnisNote);
|
||||
// the same input as every other caller, so the same rows give the same history
|
||||
$lvRow = $this->getLvGesamtnoteRow($lva_id, $student_uid, $stsem);
|
||||
$verlauf = $this->VerlaufLib->buildVerlauf($pruefungen, $lvRow ? $lvRow->note : null, $zeugnisNote);
|
||||
|
||||
// the record being edited
|
||||
$current = null;
|
||||
@@ -1050,8 +1059,9 @@ class Noten extends FHCAPI_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Guards a NEW attempt. Rule A is the attempt limit, and nothing follows a final exam. Rule B
|
||||
* is the chronological order. Rule C is the occurrence limit of a grade.
|
||||
* Guards a NEW attempt. A blocked kommissionelle Prüfung is refused first. Rule A is the
|
||||
* attempt limit, and nothing follows a final exam. Rule B is the chronological order. Rule C is
|
||||
* the occurrence limit of a grade.
|
||||
*
|
||||
* @return string|null lokalisierte Fehlermeldung oder null
|
||||
*/
|
||||
@@ -1070,6 +1080,11 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
$verlauf = $this->VerlaufLib->buildVerlauf($pruefungen, $lvNote, $zeugnisNote);
|
||||
|
||||
// the next attempt is kommissionell, and this tool may not create it
|
||||
if($verlauf->kommPruefGesperrt) {
|
||||
return $this->p->t('benotungstool', 'kommPruefNichtErlaubt', [$student_uid]);
|
||||
}
|
||||
|
||||
// A: the first attempt only materialises the course grade, therefore it adds no attempt
|
||||
if($verlauf->naechsteRolle !== PruefungsverlaufLib::ROLLE_ERSTANTRITT && !$verlauf->canAdd) {
|
||||
return $this->p->t('benotungstool', 'maxAntritteReached', [$student_uid, $verlauf->maxAntritte]);
|
||||
@@ -1129,8 +1144,9 @@ class Noten extends FHCAPI_Controller
|
||||
return $this->p->t('benotungstool', 'c4noteNichtInLehre', [$student_uid]);
|
||||
}
|
||||
|
||||
// as soon as an exam exists the grade belongs to the attempt history and follows its rules
|
||||
if(count($this->VerlaufLib->getPruefungen($student_uid, $lva_id, $stsem)) > 0) {
|
||||
// Antritt 1 und die LV-Note sind dieselbe Leistung, deshalb schreibt dieser Weg sie weiter.
|
||||
// Ab der ersten Wiederholung gehört die Note zum Antritt: dann über den Prüfungsdialog.
|
||||
if($this->VerlaufLib->hatWiederholung($this->VerlaufLib->getPruefungen($student_uid, $lva_id, $stsem))) {
|
||||
return $this->p->t('benotungstool', 'c4notenvorschlagGesperrt', [$student_uid]);
|
||||
}
|
||||
|
||||
@@ -1144,6 +1160,46 @@ class Noten extends FHCAPI_Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guards the date the user picked for the course grade. The release turns the benotungsdatum
|
||||
* into the date of the first attempt, therefore the date follows the same rules as an exam
|
||||
* date. No date at all is permitted: the current moment then applies.
|
||||
*
|
||||
* @return string|null a translated error message, or null if the date is permitted
|
||||
*/
|
||||
private function validateBenotungsdatum($datum, $student_uid, $sem_kurzbz)
|
||||
{
|
||||
if($datum === null || $datum === '') return null;
|
||||
|
||||
$tag = substr((string) $datum, 0, 10);
|
||||
$geprueft = DateTime::createFromFormat('Y-m-d', $tag);
|
||||
|
||||
if($geprueft === false || $geprueft->format('Y-m-d') !== $tag) {
|
||||
return $this->p->t('benotungstool', 'benotungsdatumUngueltig', [$student_uid]);
|
||||
}
|
||||
|
||||
// an assessment that did not happen yet has no date
|
||||
if($tag > date('Y-m-d')) {
|
||||
return $this->p->t('benotungstool', 'benotungsdatumInZukunft', [$student_uid]);
|
||||
}
|
||||
|
||||
return $this->pruefungsdatumNachFrist($sem_kurzbz, $tag, $student_uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Lehreinheit of one student in this course. Resolve it once and hand it to every consumer:
|
||||
* the grading person and the exam row both need it.
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
private function lehreinheitFuerStudent($lva_id, $student_uid, $stsem)
|
||||
{
|
||||
$resLe = $this->LehrveranstaltungModel->getLeByStudent($student_uid, $stsem, $lva_id);
|
||||
if(isError($resLe) || !hasData($resLe)) return null;
|
||||
|
||||
return current(getData($resLe))->lehreinheit_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the DATE of the exam against the deadline. enforceNoteneintragungsfrist compares the
|
||||
* current time, which is a different question: a user can enter an exam today and give it a
|
||||
@@ -1228,6 +1284,9 @@ class Noten extends FHCAPI_Controller
|
||||
$note = $result->note;
|
||||
$punkte = $result->punkte;
|
||||
|
||||
// the day the assessment took place. The dialog sends it, older callers do not.
|
||||
$datum = property_exists($result, 'datum') ? $result->datum : null;
|
||||
|
||||
$this->assertLvAccess($lv_id, $sem_kurzbz);
|
||||
|
||||
// examination rules: no entry and no change after the grade entry deadline
|
||||
@@ -1245,6 +1304,15 @@ class Noten extends FHCAPI_Controller
|
||||
$fehler = $this->validateNotenvorschlag($lv_id, $student_uid, $sem_kurzbz, $note);
|
||||
if($fehler !== null) $this->terminateWithError($fehler, 'general');
|
||||
|
||||
$fehler = $this->validateBenotungsdatum($datum, $student_uid, $sem_kurzbz);
|
||||
if($fehler !== null) $this->terminateWithError($fehler, 'general');
|
||||
|
||||
// Der gewählte Tag ist das Datum von Antritt 1, nicht das benotungsdatum. Das benotungsdatum
|
||||
// bleibt der Zeitpunkt der Eingabe, weil die Freigabe es mit dem freigabedatum vergleicht:
|
||||
// ein Tag in der Vergangenheit liesse die geänderte Note als freigegeben erscheinen.
|
||||
$erstantrittDatum = $datum === null || $datum === '' ? date("Y-m-d") : substr((string) $datum, 0, 10);
|
||||
$lvgesamtnote = null;
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $student_uid, $sem_kurzbz);
|
||||
|
||||
// $this->addMeta('LvgesamtnoteModelresult', $result);
|
||||
@@ -1296,10 +1364,46 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
$this->logLib->logInfoDB(array('saveNotenvorschlag insert lv gesamtnote',$res, getAuthUID(), getAuthPersonId()));
|
||||
}
|
||||
|
||||
|
||||
// Ohne geschriebene LV-Note entsteht kein Antritt: eine Prüfung ohne Note ist ein Zustand,
|
||||
// den jeder andere Pfad ablehnt (c4keineLvNoteEingetragen).
|
||||
if($lvgesamtnote === null) {
|
||||
$this->terminateWithError($this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$student_uid]), 'general');
|
||||
}
|
||||
|
||||
// The course grade IS the first attempt. Write it as its own exam now, or the next exam
|
||||
// becomes attempt 2 and the legacy type of the whole chain moves one place.
|
||||
$this->erstantrittBeiUebernahme($lv_id, $student_uid, $sem_kurzbz, $note, $punkte, $erstantrittDatum);
|
||||
|
||||
// the client shows the new attempt at once, without a reload
|
||||
$lvgesamtnote->verlauf = $this->buildVerlaufSummary($student_uid, $lv_id, $sem_kurzbz);
|
||||
|
||||
$this->terminateWithSuccess(array($lvgesamtnote));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the first attempt when somebody takes the course grade over, the same way the
|
||||
* Studierendenverwaltung does it. CIS_GESAMTNOTE_ERSTANTRITT_BEI_UEBERNAHME switches it off.
|
||||
* A credited transcript grade forbids every exam.
|
||||
*/
|
||||
private function erstantrittBeiUebernahme($lv_id, $student_uid, $sem_kurzbz, $note, $punkte, $datum)
|
||||
{
|
||||
if(!$this->config->item('CIS_GESAMTNOTE_ERSTANTRITT_BEI_UEBERNAHME')) return;
|
||||
if($this->VerlaufLib->istAnrechnungsnote($this->getZeugnisnote($lv_id, $student_uid, $sem_kurzbz))) return;
|
||||
|
||||
// the person who enters the grade picks this date, therefore it also updates attempt 1
|
||||
$lehreinheit_id = $this->lehreinheitFuerStudent($lv_id, $student_uid, $sem_kurzbz);
|
||||
|
||||
$geschrieben = $this->VerlaufLib->upsertErstantritt(
|
||||
$student_uid, $lv_id, $sem_kurzbz, $note, $punkte, $datum,
|
||||
$this->benotenderMitarbeiter($lehreinheit_id), true, $lehreinheit_id
|
||||
);
|
||||
|
||||
if($geschrieben !== null) {
|
||||
$this->logLib->logInfoDB(array('erstantritt (uebernahme)', $student_uid, getAuthUID(), getAuthPersonId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST METHOD
|
||||
* expects 'sem_kurzbz', 'lv_id', 'noten'
|
||||
@@ -1327,6 +1431,9 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
foreach($noten as $note)
|
||||
{
|
||||
// je Zeile neu: sonst trägt die Variable die Zeile davor, und eine gescheiterte Zeile
|
||||
// meldet die Note der vorherigen Person zurück
|
||||
$lvgesamtnote = null;
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz);
|
||||
// $this->addMeta($note->uid.'$result', $result);
|
||||
@@ -1390,6 +1497,17 @@ class Noten extends FHCAPI_Controller
|
||||
$this->logLib->logInfoDB(array('saveNotenvorschlagBulk insert lv gesamtnote',$res, getAuthUID(), getAuthPersonId()));
|
||||
}
|
||||
|
||||
// Ohne geschriebene LV-Note entsteht kein Antritt, und die Zeile meldet den Fehler
|
||||
if($lvgesamtnote === null) {
|
||||
$retLvNoten[$note->uid] = $this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$note->uid]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// the same rule as the single dialog: the course grade is attempt 1
|
||||
$this->erstantrittBeiUebernahme($lv_id, $note->uid, $sem_kurzbz, trim($note->note), $note->punkte, date("Y-m-d"));
|
||||
|
||||
$lvgesamtnote->verlauf = $this->buildVerlaufSummary($note->uid, $lv_id, $sem_kurzbz);
|
||||
|
||||
$retLvNoten[$note->uid] = $lvgesamtnote;
|
||||
}
|
||||
|
||||
@@ -1534,51 +1652,19 @@ class Noten extends FHCAPI_Controller
|
||||
{
|
||||
if($this->VerlaufLib->istAnrechnungsnote($this->getZeugnisnote($lva_id, $student_uid, $stsem))) return;
|
||||
|
||||
$pruefungen = $this->VerlaufLib->getPruefungen($student_uid, $lva_id, $stsem);
|
||||
if(count($pruefungen) > 1) return;
|
||||
// The release makes the grade binding. It moves no date: an exam keeps the date it carries,
|
||||
// and attempt 1 keeps the date that the person picked while entering the grade. Only a
|
||||
// missing attempt 1 is created, and it is dated on the benotungsdatum.
|
||||
$lehreinheit_id = $this->lehreinheitFuerStudent($lva_id, $student_uid, $stsem);
|
||||
|
||||
$jetzt = date("Y-m-d H:i:s");
|
||||
|
||||
if(count($pruefungen) === 1) {
|
||||
$this->LePruefungModel->update(
|
||||
$pruefungen[0]->pruefung_id,
|
||||
array(
|
||||
'note' => $note,
|
||||
'punkte' => $punkte,
|
||||
'datum' => $datum,
|
||||
'updateamum' => $jetzt,
|
||||
'updatevon' => getAuthUID()
|
||||
)
|
||||
);
|
||||
|
||||
$this->logLib->logInfoDB(array('erstantritt aktualisiert (freigabe)', $student_uid, getAuthUID(), getAuthPersonId()));
|
||||
return;
|
||||
}
|
||||
|
||||
// the server finds the lehreinheit_id; it does not use the value from the client
|
||||
$resLe = $this->LehrveranstaltungModel->getLeByStudent($student_uid, $stsem, $lva_id);
|
||||
if(isError($resLe) || !hasData($resLe)) return;
|
||||
$le = current(getData($resLe));
|
||||
|
||||
$this->LePruefungModel->insert(
|
||||
array(
|
||||
'lehreinheit_id' => $le->lehreinheit_id,
|
||||
'student_uid' => $student_uid,
|
||||
'mitarbeiter_uid' => $this->benotenderMitarbeiter($le->lehreinheit_id),
|
||||
'note' => $note,
|
||||
'punkte' => $punkte,
|
||||
'pruefungstyp_kurzbz' => $this->VerlaufLib->legacyTypFuerAntritt(1),
|
||||
'datum' => $datum,
|
||||
'anmerkung' => "",
|
||||
'insertamum' => $jetzt,
|
||||
'insertvon' => getAuthUID(),
|
||||
'updateamum' => null,
|
||||
'updatevon' => null,
|
||||
'ext_id' => null
|
||||
)
|
||||
$geschrieben = $this->VerlaufLib->upsertErstantritt(
|
||||
$student_uid, $lva_id, $stsem, $note, $punkte, $datum,
|
||||
$this->benotenderMitarbeiter($lehreinheit_id), false, $lehreinheit_id
|
||||
);
|
||||
|
||||
$this->logLib->logInfoDB(array('erstantritt angelegt (freigabe)', $student_uid, getAuthUID(), getAuthPersonId()));
|
||||
if($geschrieben !== null) {
|
||||
$this->logLib->logInfoDB(array('erstantritt (freigabe)', $student_uid, getAuthUID(), getAuthPersonId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,7 @@ class PruefungsverlaufLib
|
||||
{
|
||||
$this->_ci =& get_instance();
|
||||
$this->_ci->load->model('education/LePruefung_model', 'LePruefungModel');
|
||||
$this->_ci->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
|
||||
$this->_ci->load->model('education/Note_model', 'NoteModel');
|
||||
$this->_ci->load->model('education/Pruefungstyp_model', 'PruefungstypModel');
|
||||
$this->_ci->load->config('noten');
|
||||
@@ -158,9 +159,108 @@ class PruefungsverlaufLib
|
||||
$verlauf->naechsteRolle = self::ROLLE_PRUEFUNG;
|
||||
}
|
||||
|
||||
// The next attempt is the kommissionelle one, but this tool may not create it.
|
||||
$verlauf->kommPruefGesperrt = $verlauf->canAdd
|
||||
&& $verlauf->naechsteRolle === self::ROLLE_KOMMISSIONELL
|
||||
&& !$this->darfKommPruefAnlegen();
|
||||
|
||||
if ($verlauf->kommPruefGesperrt) $verlauf->canAdd = false;
|
||||
|
||||
return $verlauf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trägt die Zeile eine Wiederholung, also einen Termin nach Antritt 1?
|
||||
*
|
||||
* Antritt 1 und die LV-Note sind dieselbe Leistung. Solange nur er existiert, darf der
|
||||
* Vorschlagsweg die Note schreiben, und upsertErstantritt hält genau diesen Termin nach. Ab der
|
||||
* ersten Wiederholung gehört die Note zum Antritt und folgt dessen Regeln.
|
||||
*
|
||||
* Die Antwort kommt aus dem Verlauf, nicht aus der Zahl der Zeilen: die Position sagt, der
|
||||
* wievielte Termin ein Datensatz ist.
|
||||
*
|
||||
* @param array $pruefungen die Termine der Zeile, ungeordnet
|
||||
* @return bool
|
||||
*/
|
||||
public function hatWiederholung($pruefungen)
|
||||
{
|
||||
foreach ($this->buildVerlauf($pruefungen)->pruefungen as $termin) {
|
||||
if ($termin->position > 1) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialises the first attempt as its own exam row. A course grade without any exam IS
|
||||
* attempt 1 (impliziterErstantritt). As long as that row is missing, the next exam becomes
|
||||
* attempt 2, and the legacy type of the whole chain moves one place. The caller therefore
|
||||
* writes the row as soon as the course grade exists, not only on the release.
|
||||
*
|
||||
* Idempotent. With more than one exam the chain has moved on and nothing is written. With
|
||||
* exactly one exam the grade follows the course grade.
|
||||
*
|
||||
* @param bool $mitDatum true also writes $datum into the existing attempt 1. Only the person
|
||||
* who enters the course grade picks that date; a release must not move
|
||||
* a date that somebody entered for the exam itself.
|
||||
* @param mixed $lehreinheit_id the caller resolved it already; null makes this method look it up
|
||||
* @return stdClass|null the exam row, or null if nothing was written
|
||||
*/
|
||||
public function upsertErstantritt($student_uid, $lv_id, $sem_kurzbz, $note, $punkte, $datum, $mitarbeiter_uid = null, $mitDatum = false, $lehreinheit_id = null)
|
||||
{
|
||||
$pruefungen = $this->getPruefungen($student_uid, $lv_id, $sem_kurzbz);
|
||||
if ($this->hatWiederholung($pruefungen)) return null;
|
||||
|
||||
$jetzt = date('Y-m-d H:i:s');
|
||||
$tag = substr((string) $datum, 0, 10);
|
||||
|
||||
if (count($pruefungen) === 1) {
|
||||
$daten = [
|
||||
'note' => $note,
|
||||
'punkte' => $punkte,
|
||||
'updateamum' => $jetzt,
|
||||
'updatevon' => getAuthUID()
|
||||
];
|
||||
if ($mitDatum) $daten['datum'] = $tag;
|
||||
|
||||
$this->_ci->LePruefungModel->update($pruefungen[0]->pruefung_id, $daten);
|
||||
|
||||
return $this->ladePruefung($pruefungen[0]->pruefung_id);
|
||||
}
|
||||
|
||||
// the server finds the Lehreinheit; it does not use a value from the client
|
||||
if ($lehreinheit_id === null) {
|
||||
$resLe = $this->_ci->LehrveranstaltungModel->getLeByStudent($student_uid, $sem_kurzbz, $lv_id);
|
||||
if (isError($resLe) || !hasData($resLe)) return null;
|
||||
$lehreinheit_id = current(getData($resLe))->lehreinheit_id;
|
||||
}
|
||||
|
||||
$id = $this->_ci->LePruefungModel->insert([
|
||||
'lehreinheit_id' => $lehreinheit_id,
|
||||
'student_uid' => $student_uid,
|
||||
'mitarbeiter_uid' => $mitarbeiter_uid,
|
||||
'note' => $note,
|
||||
'punkte' => $punkte,
|
||||
'pruefungstyp_kurzbz' => $this->legacyTypFuerAntritt(1),
|
||||
'datum' => $tag,
|
||||
'anmerkung' => '',
|
||||
'insertamum' => $jetzt,
|
||||
'insertvon' => getAuthUID(),
|
||||
'updateamum' => null,
|
||||
'updatevon' => null,
|
||||
'ext_id' => null
|
||||
]);
|
||||
|
||||
return $id ? $this->ladePruefung($id->retval) : null;
|
||||
}
|
||||
|
||||
/** @return stdClass|null */
|
||||
private function ladePruefung($pruefung_id)
|
||||
{
|
||||
$result = $this->_ci->LePruefungModel->load($pruefung_id);
|
||||
return (!isError($result) && hasData($result)) ? getData($result)[0] : null;
|
||||
}
|
||||
|
||||
/** 'angerechnet' or 'intern angerechnet'. The TRANSCRIPT grade decides. @return bool */
|
||||
public function istAnrechnungsnote($note)
|
||||
{
|
||||
@@ -187,6 +287,19 @@ class PruefungsverlaufLib
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells you if this tool may create the kommissionelle Prüfung. An installation that enters it
|
||||
* in another tool sets CIS_GESAMTNOTE_ALLOW_CREATE_KOMMPRUEF to false. The exam stays visible
|
||||
* in the tool either way.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function darfKommPruefAnlegen()
|
||||
{
|
||||
$erlaubt = $this->_ci->config->item('CIS_GESAMTNOTE_ALLOW_CREATE_KOMMPRUEF');
|
||||
return $erlaubt === null ? true : (bool) $erlaubt;
|
||||
}
|
||||
|
||||
/**
|
||||
* The NOTE decides, therefore the same type can occur more than one time. The type decides in
|
||||
* one case only: a type in PRUEFUNG_TYPEN_OHNE_ANTRITT never uses an attempt.
|
||||
@@ -261,43 +374,33 @@ class PruefungsverlaufLib
|
||||
return $this->legacyTypFuerAntritt(max(2, $verlauf->antrittCount + 1));
|
||||
}
|
||||
|
||||
/** @return array{entschuldigt: mixed, ohneAntritt: array, anrechnung: array, limitMap: array} */
|
||||
/**
|
||||
* The grades with a special meaning. The configuration names them, this method resolves the
|
||||
* names in lehre.tbl_note. A primary key means something else in every installation, therefore
|
||||
* no configuration holds one. A name that no grade carries is left out.
|
||||
*
|
||||
* @return array{entschuldigt: mixed, ohneAntritt: array, anrechnung: array, limitMap: array}
|
||||
*/
|
||||
public function getSpecialNotes()
|
||||
{
|
||||
if ($this->_specialNotes !== null) return $this->_specialNotes;
|
||||
|
||||
$cfgEntschuldigt = $this->_ci->config->item('NOTE_ENTSCHULDIGT');
|
||||
$cfgOhneAntritt = $this->_ci->config->item('NOTEN_OHNE_ANTRITT');
|
||||
$cfgLimitMap = $this->_ci->config->item('NOTEN_OCCURANCE_LIMIT_MAP');
|
||||
$bezeichnungen = $this->_ci->config->item('NOTEN_OHNE_ANTRITT_BEZEICHNUNGEN');
|
||||
if (!is_array($cfgOhneAntritt)) $cfgOhneAntritt = [];
|
||||
if (!is_array($cfgLimitMap)) $cfgLimitMap = [];
|
||||
if (!is_array($bezeichnungen)) $bezeichnungen = [];
|
||||
$entschuldigt = $this->getNoteByBezeichnung($this->_ci->config->item('NOTE_ENTSCHULDIGT_BEZEICHNUNG'));
|
||||
|
||||
$resEnt = $this->_ci->NoteModel->getEntschuldigtNote();
|
||||
$entschuldigt = (!isError($resEnt) && hasData($resEnt)) ? getData($resEnt)[0]->note : $cfgEntschuldigt;
|
||||
|
||||
// The name wins against the configured key. Do NOT merge the two sets: the same key has a
|
||||
// different meaning in each installation (17 is 'entschuldigt' here, but 'nicht zugelassen'
|
||||
// in the standard data).
|
||||
$ohneAntritt = [];
|
||||
foreach ($bezeichnungen as $bezeichnung) {
|
||||
$note = $this->getNoteByBezeichnung($bezeichnung);
|
||||
if ($note !== null && !in_array($note, $ohneAntritt)) $ohneAntritt[] = $note;
|
||||
}
|
||||
if (count($ohneAntritt) === 0) $ohneAntritt = $cfgOhneAntritt;
|
||||
$ohneAntritt = $this->resolveNoten('NOTEN_OHNE_ANTRITT_BEZEICHNUNGEN');
|
||||
if ($entschuldigt !== null && !in_array($entschuldigt, $ohneAntritt)) $ohneAntritt[] = $entschuldigt;
|
||||
|
||||
// move the configured limit for 'entschuldigt' to the key that was found
|
||||
// the limit names a grade, the rules work on its key
|
||||
$limitMap = [];
|
||||
foreach ($cfgLimitMap as $k => $v) {
|
||||
$limitMap[($k == $cfgEntschuldigt) ? $entschuldigt : $k] = $v;
|
||||
foreach ($this->configArray('NOTEN_OCCURANCE_LIMIT_MAP') as $bezeichnung => $limit) {
|
||||
$note = $this->getNoteByBezeichnung($bezeichnung);
|
||||
if ($note !== null) $limitMap[$note] = $limit;
|
||||
}
|
||||
|
||||
$this->_specialNotes = [
|
||||
'entschuldigt' => $entschuldigt,
|
||||
'ohneAntritt' => array_values($ohneAntritt),
|
||||
'anrechnung' => $this->resolveNoten('NOTEN_ANRECHNUNG_BEZEICHNUNGEN', 'NOTEN_ANRECHNUNG'),
|
||||
'anrechnung' => $this->resolveNoten('NOTEN_ANRECHNUNG_BEZEICHNUNGEN'),
|
||||
'limitMap' => $limitMap
|
||||
];
|
||||
return $this->_specialNotes;
|
||||
@@ -367,26 +470,25 @@ class PruefungsverlaufLib
|
||||
return $this->_typen;
|
||||
}
|
||||
|
||||
/** Finds the grades by their name. The configured keys are the fallback only. @return array */
|
||||
private function resolveNoten($bezeichnungKey, $pkKey)
|
||||
/** The grades of one configuration key, found by their name. @return array */
|
||||
private function resolveNoten($bezeichnungKey)
|
||||
{
|
||||
$bezeichnungen = $this->_ci->config->item($bezeichnungKey);
|
||||
if (!is_array($bezeichnungen)) $bezeichnungen = [];
|
||||
|
||||
$noten = [];
|
||||
foreach ($bezeichnungen as $bezeichnung) {
|
||||
foreach ($this->configArray($bezeichnungKey) as $bezeichnung) {
|
||||
$note = $this->getNoteByBezeichnung($bezeichnung);
|
||||
if ($note !== null && !in_array($note, $noten)) $noten[] = $note;
|
||||
}
|
||||
|
||||
if (count($noten) === 0) {
|
||||
$fallback = $this->_ci->config->item($pkKey);
|
||||
if (is_array($fallback)) $noten = $fallback;
|
||||
}
|
||||
|
||||
return array_values($noten);
|
||||
}
|
||||
|
||||
/** One configuration value that must be a list. @return array */
|
||||
private function configArray($key)
|
||||
{
|
||||
$wert = $this->_ci->config->item($key);
|
||||
return is_array($wert) ? $wert : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass|null
|
||||
*/
|
||||
@@ -400,10 +502,12 @@ class PruefungsverlaufLib
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
* @return mixed|null the key of the grade, or null if no grade carries the name
|
||||
*/
|
||||
private function getNoteByBezeichnung($bezeichnung)
|
||||
{
|
||||
if (!is_string($bezeichnung) || trim($bezeichnung) === '') return null;
|
||||
|
||||
$result = $this->_ci->NoteModel->loadWhere(['bezeichnung' => $bezeichnung]);
|
||||
return (!isError($result) && hasData($result)) ? getData($result)[0]->note : null;
|
||||
}
|
||||
|
||||
@@ -12,24 +12,19 @@ class Note_model extends DB_Model
|
||||
$this->pk = 'note';
|
||||
}
|
||||
|
||||
// Sorted like the grade list of the Stv
|
||||
// scale 1-5 by its value first, every other grade alphabetically after it.
|
||||
public function getAllActive() {
|
||||
$qry ="SELECT *
|
||||
FROM lehre.tbl_note
|
||||
WHERE aktiv = true";
|
||||
WHERE aktiv = true
|
||||
ORDER BY CASE WHEN note BETWEEN 1 AND 5 THEN 0 ELSE 1 END,
|
||||
CASE WHEN note BETWEEN 1 AND 5 THEN note END,
|
||||
bezeichnung";
|
||||
|
||||
return $this->execReadOnlyQuery($qry);
|
||||
}
|
||||
|
||||
// used to determine the primary key of note "entschuldigt" to avoid hardcoded magic numbers
|
||||
// that might differ in a different installation of fhcomplete
|
||||
public function getEntschuldigtNote() {
|
||||
$qry ="SELECT *
|
||||
FROM lehre.tbl_note
|
||||
WHERE bezeichnung = 'entschuldigt'";
|
||||
|
||||
return $this->execReadOnlyQuery($qry);
|
||||
}
|
||||
|
||||
// used to determine the primary key of note "noch nicht eingetragen" to avoid hardcoded magic numbers
|
||||
// that might differ in a different installation of fhcomplete
|
||||
public function getNochNichtEingetragenNote() {
|
||||
|
||||
@@ -63,10 +63,7 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* Prueufungszelle: feste Spuren fuer Note | Datum | Aktion.
|
||||
* Vorher war das eine zentrierte Flex-Zeile, dadurch verschoben laengere Notenbezeichnungen
|
||||
* ("Nicht Genuegend" vs "Genuegend") Datum und Button - die Zellen standen von Zeile zu Zeile
|
||||
* nicht mehr untereinander. Mit festen Spurbreiten fluchten sie immer.
|
||||
* Prueufungszelle: feste cols für Note | Datum | Aktion.
|
||||
*/
|
||||
.pruefung-cell {
|
||||
position: relative;
|
||||
@@ -75,20 +72,15 @@
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
height: 100%;
|
||||
padding-left: 32px; /* badge width (20) + its left offset (6) + gap (6) */
|
||||
padding-left: 44px;
|
||||
padding-right: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Spaltenmodus 'antritt': das Datum steht in der Zelle, nicht im Spaltenkopf */
|
||||
.pruefung-cell.mit-datum {
|
||||
grid-template-columns: minmax(0, 1fr) 4.75rem 6.5rem; /* Note | Datum | Aktion */
|
||||
}
|
||||
|
||||
/*
|
||||
* Zellen ohne Aktion (abschliessender Termin, wird in einem anderen Tool gepflegt): die Spur fuer
|
||||
* den Button entfaellt, der Platz gehoert der Notenbezeichnung.
|
||||
*/
|
||||
.pruefung-cell.ohne-aktion {
|
||||
grid-template-columns: minmax(0, 1fr); /* Note */
|
||||
}
|
||||
@@ -132,15 +124,16 @@
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
padding: 0 5px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.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; }
|
||||
/* Termin ohne verbrauchten Antritt (entschuldigt, nicht beurteilt) - traegt keine Nummer */
|
||||
.pruefung-badge.attempt-none::before { background: #767676; }
|
||||
@@ -42,11 +42,13 @@ export default {
|
||||
params: { password, noten, lv_id, sem_kurzbz }
|
||||
};
|
||||
},
|
||||
saveNotenvorschlag(lv_id, sem_kurzbz, student_uid, note, punkte = null) {
|
||||
// datum: das gewählte Benotungsdatum (YYYY-MM-DD). Ohne Wert nimmt der Server den aktuellen
|
||||
// Zeitpunkt. Die Freigabe macht daraus das Datum des ersten Antritts.
|
||||
saveNotenvorschlag(lv_id, sem_kurzbz, student_uid, note, punkte = null, datum = null) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/Noten/saveNotenvorschlag',
|
||||
params: { lv_id, sem_kurzbz, student_uid, note, punkte }
|
||||
params: { lv_id, sem_kurzbz, student_uid, note, punkte, datum }
|
||||
};
|
||||
},
|
||||
// kein Termintyp mehr: welchen Antritt die Prüfung darstellt, leitet der Server aus dem
|
||||
|
||||
@@ -57,20 +57,21 @@ export const Benotungstool = {
|
||||
debouncedFetchPunkteForPruefung: null,
|
||||
config: null, // cis config
|
||||
neuesPruefungsdatumModalVisible: false,
|
||||
freigabeModalVisible: false,
|
||||
loading: false,
|
||||
selectedUids: [], // shared selection state
|
||||
selectedLehreinheit: null,
|
||||
tabulatorCanBeBuilt: false,
|
||||
selectedPruefungNote: null,
|
||||
selectedPruefungDate: new Date(), // v-model for pruefung edit datepicker
|
||||
uebernahmeStudent: null,
|
||||
uebernahmeDate: new Date(),
|
||||
uebernahmeMaxDate: new Date(),
|
||||
selectedPruefungPunkte: null,
|
||||
pruefungNoteLocked: false, // grade read-only when a later pruefung exists (date stays editable)
|
||||
pruefungDateMin: null,
|
||||
pruefungDateMax: null,
|
||||
distinctPruefungsDates: null,
|
||||
// The column layout of the exams: 'antritt' (one column for each attempt number) or 'datum'
|
||||
// (one column for each exam date). null uses the default from the configuration. The browser
|
||||
// keeps the choice of the user.
|
||||
pruefungsspalten: localStorage.getItem('notenToolPruefungsspalten'),
|
||||
// The students of the current entry that have no course grade. The new exam creates the
|
||||
// grade, and the dialog shows a hint before that happens.
|
||||
@@ -470,7 +471,7 @@ export const Benotungstool = {
|
||||
|
||||
// check if student antrittCount is too high already
|
||||
if(!this.canAddPruefung(student)) {
|
||||
this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat bereits ' + student.hoechsterAntritt + ' Prüfungsantritte abgelegt. Die Zeile wurde übersprungen.')
|
||||
this.warnKeinAntritt(student, 'Die Zeile wurde übersprungen.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -491,6 +492,27 @@ export const Benotungstool = {
|
||||
validateNotenBulk(noten) {
|
||||
// in case we need to further validate noten, currently parser does all
|
||||
},
|
||||
/**
|
||||
* The grade of one import row. The value is the note itself, and with
|
||||
* CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL also the shorthand from tbl_note.anmerkung, which the
|
||||
* Excel grade list uses for the special grades.
|
||||
*/
|
||||
noteAusImportWert(wert) {
|
||||
const eingabe = String(wert ?? '').trim()
|
||||
if(eingabe === '') return null
|
||||
|
||||
const nachNote = this.notenOptions?.find(n => String(n.note).trim() === eingabe)
|
||||
if(nachNote) return nachNote
|
||||
|
||||
if(!this.config?.CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL) return null
|
||||
|
||||
const kuerzel = eingabe.toLowerCase()
|
||||
const treffer = (this.notenOptions ?? []).filter(
|
||||
n => String(n.anmerkung ?? '').trim().toLowerCase() === kuerzel
|
||||
)
|
||||
|
||||
return treffer.length === 1 ? treffer[0] : null
|
||||
},
|
||||
parseNote(rowParts, notenbulk, rowNum) {
|
||||
const id = this.identifyUid(rowParts[0])
|
||||
const idTrimmed = rowParts[0].trim()
|
||||
@@ -511,16 +533,16 @@ export const Benotungstool = {
|
||||
if(this.config?.CIS_GESAMTNOTE_PUNKTE) {
|
||||
punkte = Number.parseFloat(rowParts[1])
|
||||
} else {
|
||||
note = rowParts[1]
|
||||
|
||||
// find notenoption and check if its allowed to use in lehre
|
||||
const notenOption = this.notenOptions.find(n => n.note == note)
|
||||
const notenOption = this.noteAusImportWert(rowParts[1])
|
||||
if(!notenOption?.lehre) {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importNoGradeFoundForIdInRow', [rowParts[0], rowNum]))
|
||||
return
|
||||
}
|
||||
|
||||
note = notenOption.note
|
||||
}
|
||||
|
||||
|
||||
notenbulk.push({uid: student.uid, note, punkte})
|
||||
},
|
||||
parsePruefung(rowParts, pruefungbulk, rowNum) {
|
||||
@@ -559,14 +581,14 @@ export const Benotungstool = {
|
||||
if(this.config?.CIS_GESAMTNOTE_PUNKTE) {
|
||||
punkte = Number.parseFloat(rowParts[2])
|
||||
} else {
|
||||
note = rowParts[2]
|
||||
|
||||
// find notenoption and check if its allowed to use in lehre
|
||||
const notenOption = this.notenOptions.find(n => n.note == note)
|
||||
const notenOption = this.noteAusImportWert(rowParts[2])
|
||||
if(!notenOption?.lehre) {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('benotungstool/c4importNoGradeFoundForIdInRow', [rowParts[0], rowNum]))
|
||||
return
|
||||
}
|
||||
|
||||
note = notenOption.note
|
||||
}
|
||||
|
||||
pruefungbulk.push({uid: student.uid, datum: dateStr, note, punkte, lehreinheit_id: student.lehreinheit_id, dateObj})
|
||||
@@ -587,6 +609,7 @@ export const Benotungstool = {
|
||||
|
||||
s.note_vorschlag = lvn.note
|
||||
this.applyLvGesamtnote(s, lvn)
|
||||
if(lvn.verlauf) this.applyVerlauf(s, lvn.verlauf)
|
||||
})
|
||||
|
||||
if(errorList !== '') this.$fhcAlert.alertError(errorList)
|
||||
@@ -599,6 +622,8 @@ export const Benotungstool = {
|
||||
)
|
||||
}
|
||||
|
||||
// the import writes attempt 1 for each row, therefore the exam columns can change
|
||||
this.applyPruefungColumns()
|
||||
this.$refs.notenTable.tabulator.redraw(true)
|
||||
}).finally(()=>{
|
||||
this.loading = false
|
||||
@@ -691,13 +716,10 @@ export const Benotungstool = {
|
||||
delete student.verlauf.pruefungen
|
||||
|
||||
student.pruefungen = []
|
||||
delete student['kommPruef']
|
||||
|
||||
;(verlauf.pruefungen ?? []).forEach(p => {
|
||||
p.dateObj = this.parseISODate(p.datum)
|
||||
// the final exam has its own column, and that column is always the last one
|
||||
if(p.kommissionell) student['kommPruef'] = p
|
||||
else student.pruefungen.push(p)
|
||||
student.pruefungen.push(p)
|
||||
})
|
||||
|
||||
// Update the flat list too. It decides if the user can edit the grade proposal and the
|
||||
@@ -720,7 +742,6 @@ export const Benotungstool = {
|
||||
},
|
||||
/** The field name of the column that shows one exam. */
|
||||
pruefungField(pruefung, index) {
|
||||
if(pruefung.kommissionell) return 'kommPruef'
|
||||
return this.pruefungsspaltenModus === 'antritt' ? ('antritt_' + (index + 1)) : pruefung.datum
|
||||
},
|
||||
/** Puts the exams into the column fields. Remove the old fields first, or they stay behind. */
|
||||
@@ -758,24 +779,23 @@ export const Benotungstool = {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 'antritt' makes one column for each attempt number and puts the date into the cell. Use it
|
||||
* if each student has an own exam date. 'datum' makes one column for each exam date. Use it
|
||||
* if the students share the exam dates.
|
||||
* 'antritt' makes one column for each Prüfungstermin of the student and puts the date into
|
||||
* the cell. Use it if each student has an own exam date. 'datum' makes one column for each
|
||||
* exam date. Use it if the students share the exam dates.
|
||||
*/
|
||||
buildPruefungColumns() {
|
||||
if(this.pruefungsspaltenModus === 'antritt') {
|
||||
let count = 0
|
||||
this.studenten?.forEach(s => {
|
||||
// Add one more column while this row can get one more attempt. The kommissionelle
|
||||
// attempt has its own column, so it needs no attempt column.
|
||||
const kommissionellFaellig = s.verlauf?.naechsteRolle === 'kommissionell'
|
||||
const frei = this.canAddPruefung(s) && !kommissionellFaellig ? 1 : 0
|
||||
// Add one more column while this row can get one more Termin. The kommissionelle
|
||||
// Prüfung is the last Termin, therefore it uses such a column too.
|
||||
const frei = this.canAddPruefung(s) ? 1 : 0
|
||||
const needed = (s.pruefungen?.length ?? 0) + frei
|
||||
if(needed > count) count = needed
|
||||
})
|
||||
|
||||
return Array.from({length: count}, (_, i) =>
|
||||
this.pruefungColumnDef('antritt_' + (i + 1), this.$capitalize(this.$p.t('benotungstool/c4antrittNr', [i + 1])))
|
||||
this.pruefungColumnDef('antritt_' + (i + 1), this.$capitalize(this.$p.t('benotungstool/c4terminNr', [i + 1])))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -797,26 +817,10 @@ export const Benotungstool = {
|
||||
if(!force && key === this._pruefungColumnKey) return
|
||||
this._pruefungColumnKey = key
|
||||
|
||||
const cols = [...this.notenTableOptions.columns.slice(0, -1)]
|
||||
const kommCol = this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF
|
||||
? this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1]
|
||||
: null
|
||||
|
||||
const cols = [...this.notenTableOptions.columns]
|
||||
pruefungCols.forEach(c => cols.push(c))
|
||||
if(kommCol) cols.push(kommCol) // abschliessender Termin bleibt die letzte Spalte
|
||||
|
||||
table.setColumns(this.restoreColumnLayout(cols))
|
||||
this.applyKommPruefSpalte()
|
||||
},
|
||||
/** Blendet die kommissionelle Spalte ein oder aus. Nach jedem setColumns nötig. */
|
||||
applyKommPruefSpalte() {
|
||||
if(!this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) return
|
||||
|
||||
const col = this.$refs.notenTable?.tabulator?.getColumn("kommPruef")
|
||||
if(!col) return
|
||||
|
||||
if(this.kommPruefSpalteNoetig) col.show()
|
||||
else col.hide()
|
||||
},
|
||||
/**
|
||||
* Keeps the scroll position while a table operation renders the table again.
|
||||
@@ -1070,7 +1074,7 @@ export const Benotungstool = {
|
||||
editor: this.liveNumberEditor,
|
||||
editable: (cell) => {
|
||||
const rowData = cell.getRow().getData();
|
||||
if(this.pruefungen?.find(p => p.student_uid == rowData.uid)) return false
|
||||
if(this.hatWiederholung(rowData)) return false
|
||||
|
||||
return true
|
||||
},
|
||||
@@ -1099,8 +1103,8 @@ export const Benotungstool = {
|
||||
const noteOption = this.notenOptions.find(opt => opt.note == rowData.note)
|
||||
if(!noteOption) return true
|
||||
|
||||
// also if student has any pruefungsnote disable noten selection
|
||||
if(this.pruefungen?.find(p => p.student_uid == rowData.uid)) return false
|
||||
// from the first repeat on the grade belongs to the exam, not to the proposal
|
||||
if(this.hatWiederholung(rowData)) return false
|
||||
|
||||
return noteOption.lkt_ueberschreibbar
|
||||
},
|
||||
@@ -1109,16 +1113,16 @@ export const Benotungstool = {
|
||||
const value = cell.getValue()
|
||||
const match = this.notenOptions?.find(opt => opt.note == value)
|
||||
const val = match ? match.bezeichnung : value
|
||||
const p = this.pruefungen?.find(p => p.student_uid == rowData.uid)
|
||||
const gesperrt = this.hatWiederholung(rowData)
|
||||
let style = ''
|
||||
|
||||
if(val === undefined) return ''
|
||||
if(p || !match?.lkt_ueberschreibbar) style = 'color: gray;font-style: italic; background-color: #f0f0f0;pointer-events: none;opacity: 0.6;user-select: none;cursor: not-allowed;'
|
||||
if(gesperrt || !match?.lkt_ueberschreibbar) style = 'color: gray;font-style: italic; background-color: #f0f0f0;pointer-events: none;opacity: 0.6;user-select: none;cursor: not-allowed;'
|
||||
return '<div style="'+style+'">' + val + '</div>'
|
||||
}
|
||||
})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4notenvorschlagUebernehmen'))), field: 'übernehmen', width: 150, minWidth: 100, hozAlign: 'center', formatter: this.arrowFormatter,
|
||||
cellClick: this.saveNote,
|
||||
cellClick: this.openUebernahmeModal,
|
||||
variableHeight: true})
|
||||
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4lvnote'))), field: 'lv_note',
|
||||
minWidth: 160,
|
||||
@@ -1142,16 +1146,6 @@ export const Benotungstool = {
|
||||
},
|
||||
headerFilterFunc: this.notenFilterFunc
|
||||
})
|
||||
columns.push({title: Vue.computed(() => this.kommPruefSpaltentitel),
|
||||
field: 'kommPruef', widthGrow: 1,
|
||||
formatter: this.pruefungFormatter,
|
||||
sorter: this.pruefungSorter,
|
||||
topCalc: this.terminCalcFunc,
|
||||
topCalcFormatter: this.terminCalcFormatter,
|
||||
hozAlign:"center", minWidth: 200, visible: false,
|
||||
tooltip: false
|
||||
})
|
||||
|
||||
return columns
|
||||
},
|
||||
pruefungSorter(a, b, aRow, bRow, column, dir, params) {
|
||||
@@ -1319,29 +1313,87 @@ export const Benotungstool = {
|
||||
|
||||
return value
|
||||
},
|
||||
async saveNote(e, cell) { // Notenvorschlag freigeben
|
||||
const row = cell.getRow()
|
||||
const data = row.getData()
|
||||
/**
|
||||
* Notenvorschlag übernehmen. The dialog asks for the date of the assessment first. The
|
||||
* server writes the course grade AND attempt 1 with that date, so the chain is complete
|
||||
* from the start and no grade carries an implicit date.
|
||||
*/
|
||||
openUebernahmeModal(e, cell) {
|
||||
const data = cell.getRow().getData()
|
||||
|
||||
if(!data.note_vorschlag) return
|
||||
if(data.note_vorschlag == data.lv_note) return
|
||||
|
||||
// as soon as exams exist, the grade belongs to the exam history
|
||||
if(data.pruefungen.length) return
|
||||
// from the first repeat on the grade belongs to the exam history
|
||||
if(this.hatWiederholung(data)) return
|
||||
|
||||
this.uebernahmeStudent = data
|
||||
|
||||
// Antritt 1 der Zeile gewinnt, sonst die erste freie Datumsspalte, sonst heute
|
||||
this.uebernahmeDate = data.pruefungen[0]?.dateObj ?? this.ersteFreieDatumsspalte(data) ?? new Date()
|
||||
this.uebernahmeMaxDate = new Date()
|
||||
|
||||
this.$refs.modalContainerUebernahme.show()
|
||||
},
|
||||
/**
|
||||
* Das erste Prüfungsdatum der Gruppe, das diese Zeile noch nicht belegt. Der Datumsmodus
|
||||
* zeigt je Datum eine Spalte, der Vorschlag trifft also die Spalte, die in der Zeile noch
|
||||
* leer ist. Ein Datum in der Zukunft scheidet aus: es ist kein Benotungsdatum.
|
||||
*
|
||||
* @returns {Date|null}
|
||||
*/
|
||||
ersteFreieDatumsspalte(student) {
|
||||
const belegt = new Set((student.pruefungen ?? []).map(p => String(p.datum ?? '').slice(0, 10)))
|
||||
const heute = this.toISODate(new Date())
|
||||
|
||||
const frei = (this.distinctPruefungsDates ?? []).find(datum => {
|
||||
const tag = String(datum ?? '').slice(0, 10)
|
||||
return tag !== '' && tag <= heute && !belegt.has(tag)
|
||||
})
|
||||
|
||||
return frei ? this.parseISODate(frei) : null
|
||||
},
|
||||
/** Writes the course grade with the chosen date. */
|
||||
saveNotenvorschlagEingabe() {
|
||||
const student = this.uebernahmeStudent
|
||||
if(!student) return
|
||||
|
||||
this.loading = true
|
||||
this.$api.call(ApiNoten.saveNotenvorschlag(this.lv_id, this.sem_kurzbz, data.uid, data.note_vorschlag, data.punkte))
|
||||
.then((res) => {
|
||||
this.$api.call(ApiNoten.saveNotenvorschlag(
|
||||
this.lv_id, this.sem_kurzbz, student.uid, student.note_vorschlag, student.punkte,
|
||||
this.toISODate(this.uebernahmeDate)
|
||||
)).then((res) => {
|
||||
if (res.meta.status === 'success') {
|
||||
const s = this.studenten.find(s => s.uid === data.uid)
|
||||
const s = this.studenten.find(s => s.uid === student.uid)
|
||||
|
||||
// the same path as after an exam, so that the release state is the same
|
||||
this.applyLvGesamtnote(s, res.data[0])
|
||||
|
||||
row.update({ lv_note: s.lv_note, freigegeben: s.freigegeben })
|
||||
row.reformat() // trigger reformat of arrow
|
||||
// the answer carries attempt 1, which the server wrote with the chosen date
|
||||
if(res.data[0]?.verlauf) this.applyVerlauf(s, res.data[0].verlauf)
|
||||
|
||||
const row = this.$refs.notenTable?.tabulator?.rowManager?.getRowFromDataObject(s)?.getComponent()
|
||||
if(row) {
|
||||
row.update({ lv_note: s.lv_note, freigegeben: s.freigegeben })
|
||||
row.reformat() // trigger reformat of arrow
|
||||
}
|
||||
|
||||
this.preserveScroll(() => this.applyPruefungColumns())
|
||||
}
|
||||
}).finally(()=>this.loading = false)
|
||||
}).finally(() => {
|
||||
this.uebernahmeStudent = null
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
this.$refs.modalContainerUebernahme.hide()
|
||||
},
|
||||
/**
|
||||
* Attempt 1 and the course grade are the same assessment, therefore the proposal column
|
||||
* stays open while only attempt 1 exists. From the first repeat on the grade belongs to
|
||||
* the exam. The server applies the same rule (validateNotenvorschlag).
|
||||
*/
|
||||
hatWiederholung(student) {
|
||||
return (student?.pruefungen?.length ?? 0) > 1
|
||||
},
|
||||
teilnotenFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
@@ -1350,6 +1402,21 @@ export const Benotungstool = {
|
||||
|
||||
return '<div style="">'+val+'</div>'
|
||||
},
|
||||
/**
|
||||
* Meldet, warum diese Zeile keinen Antritt mehr bekommt. Die gesperrte kommissionelle
|
||||
* Prüfung nennt einen anderen Grund als die erreichte Grenze.
|
||||
*/
|
||||
warnKeinAntritt(student, nachsatz) {
|
||||
if(student.verlauf?.kommPruefGesperrt) {
|
||||
this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('benotungstool/kommPruefNichtErlaubt', [student.uid])))
|
||||
return
|
||||
}
|
||||
|
||||
this.$fhcAlert.alertWarning(
|
||||
'Student ' + student.uid + ' hat bereits ' + student.hoechsterAntritt +
|
||||
' Prüfungsantritte abgelegt. ' + nachsatz
|
||||
)
|
||||
},
|
||||
/** Tells you if this row can get one more attempt. The value comes from the server. */
|
||||
canAddPruefung(student) {
|
||||
return NotenRules.canAddPruefung(student, this.config)
|
||||
@@ -1358,10 +1425,25 @@ export const Benotungstool = {
|
||||
// an exam with a later date locks the grade; you can still correct the date
|
||||
if(!pruefung) return false
|
||||
|
||||
const alle = [...(student.pruefungen ?? [])]
|
||||
if(student.kommPruef) alle.push(student.kommPruef)
|
||||
return (student.pruefungen ?? []).some(p => p.pruefung_id != pruefung.pruefung_id && p.position > pruefung.position)
|
||||
},
|
||||
/**
|
||||
* The text of the badge tooltip. The column is a Prüfungstermin, the badge is an Antritt,
|
||||
* and the two numbers are different: an exam without a counted attempt (excused, not
|
||||
* assessed) keeps the Termin but uses no Antritt.
|
||||
*/
|
||||
antrittTooltip(student, pruefung) {
|
||||
const max = student.verlauf?.maxAntritte ?? NotenRules.maxAntrittCount(this.config)
|
||||
|
||||
return alle.some(p => p.pruefung_id != pruefung.pruefung_id && p.position > pruefung.position)
|
||||
if(pruefung.kommissionell) {
|
||||
return pruefung.antritt_nr
|
||||
? this.$capitalize(this.$p.t('benotungstool/c4badgeKommAntritt', [pruefung.antritt_nr, max]))
|
||||
: this.$capitalize(this.$p.t('benotungstool/c4badgeKommOhneAntritt'))
|
||||
}
|
||||
|
||||
return pruefung.antritt_nr
|
||||
? this.$capitalize(this.$p.t('benotungstool/c4badgeAntritt', [pruefung.antritt_nr, max]))
|
||||
: this.$capitalize(this.$p.t('benotungstool/c4badgeOhneAntritt'))
|
||||
},
|
||||
/** The cell of an exam column. indexPruefungen decides which attempt the cell shows. */
|
||||
pruefungFormatter(cell) {
|
||||
@@ -1408,11 +1490,11 @@ export const Benotungstool = {
|
||||
}
|
||||
|
||||
if(studentPruefung) {
|
||||
// The badge shows the attempt number. A kommissionelle exam shows K, because its number
|
||||
// follows from the position anyway. An exam that does not count (excused, not assessed)
|
||||
// has no number and gets a neutral style.
|
||||
// The badge shows the attempt number. A kommissionelle exam adds the K, so the row shows
|
||||
// both the number and the role ('3-K'). An exam that does not count (excused, not
|
||||
// assessed) has no number and gets a neutral style.
|
||||
const attemptLabel = studentPruefung.kommissionell
|
||||
? 'K'
|
||||
? (studentPruefung.antritt_nr ? studentPruefung.antritt_nr + '-K' : 'K')
|
||||
: (studentPruefung.antritt_nr ? String(studentPruefung.antritt_nr) : '–')
|
||||
const attemptClass = studentPruefung.kommissionell
|
||||
? 'attempt-k'
|
||||
@@ -1422,6 +1504,7 @@ export const Benotungstool = {
|
||||
rowDiv.setAttribute('data-attempt', attemptLabel)
|
||||
rowDiv.setAttribute('data-cy', 'pruefung-cell')
|
||||
rowDiv.setAttribute('data-note', studentPruefung.note)
|
||||
rowDiv.title = this.antrittTooltip(data, studentPruefung)
|
||||
|
||||
// The student administration owns the escalation (zusKommPruef), therefore that cell has
|
||||
// no action button and the space belongs to the grade name.
|
||||
@@ -1450,19 +1533,15 @@ export const Benotungstool = {
|
||||
return rowDiv
|
||||
}
|
||||
|
||||
// An empty cell gets an add button only if one more attempt is possible and if the column
|
||||
// is after all exams that exist.
|
||||
// An empty cell gets an add button only if one more Termin is possible and if the column
|
||||
// is after all exams that exist. The kommissionelle Prüfung needs no own column: the
|
||||
// server decides the role of the new exam, the cell then shows the K badge.
|
||||
if(!this.canAddPruefung(data)) return ''
|
||||
|
||||
// The last attempt is kommissionell, therefore only the kommPruef column may take it. All
|
||||
// other columns stay empty at that point.
|
||||
const kommissionellFaellig = data.verlauf?.naechsteRolle === 'kommissionell'
|
||||
if(field === 'kommPruef' ? !kommissionellFaellig : kommissionellFaellig) return ''
|
||||
|
||||
if(antrittModus && field !== 'kommPruef') {
|
||||
// only the next free attempt column shows an add button
|
||||
if(antrittModus) {
|
||||
// only the next free Termin column shows an add button
|
||||
if(field !== ('antritt_' + ((data.pruefungen?.length ?? 0) + 1))) return ''
|
||||
} else if(field !== 'kommPruef') {
|
||||
} else {
|
||||
// no new exam before an exam that exists
|
||||
if((data.pruefungen ?? []).some(p => p.datum >= field)) return ''
|
||||
}
|
||||
@@ -1481,6 +1560,13 @@ export const Benotungstool = {
|
||||
if(parts.length !== 3) return null
|
||||
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))
|
||||
},
|
||||
/** Date -> 'YYYY-MM-DD'. The API takes the day, never a timestamp. */
|
||||
toISODate(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0') // Months are 0-based
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
},
|
||||
formatDatumDMY(datum) {
|
||||
// 'YYYY-MM-DD[ ...]' -> 'DD.MM.YYYY'
|
||||
const parts = (datum ?? '').slice(0, 10).split('-')
|
||||
@@ -1495,7 +1581,7 @@ export const Benotungstool = {
|
||||
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.
|
||||
// A column name that is not a date ('antritt_2', 'kommPruef') means a new exam after all
|
||||
// A column name that is not a date ('antritt_2') means a new exam after all
|
||||
// existing ones. Use a date in the far future, so every existing exam is a lower bound.
|
||||
const raw = (pruefung?.datum ?? fallbackDate ?? '').slice(0, 10)
|
||||
const refDate = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? raw : '9999-12-31'
|
||||
@@ -1579,7 +1665,7 @@ export const Benotungstool = {
|
||||
|
||||
let style = 'display: flex; justify-content: center; align-items: center; height: 100%;'
|
||||
|
||||
if(!data.note_vorschlag || (data.note_vorschlag == data.lv_note) || data.pruefungen.length) {
|
||||
if(!data.note_vorschlag || (data.note_vorschlag == data.lv_note) || this.hatWiederholung(data)) {
|
||||
// arrow to ambiguous in meaning, use str8 forward worded button here instead
|
||||
// uncolored arrow
|
||||
// return '<div style="'+style+'">' +
|
||||
@@ -1644,13 +1730,8 @@ export const Benotungstool = {
|
||||
|
||||
p.dateObj = this.parseISODate(p.datum)
|
||||
|
||||
// the final exam has its own column, and that column is always the last one
|
||||
if(p.kommissionell) {
|
||||
student['kommPruef'] = p
|
||||
} else {
|
||||
student.pruefungen.push(p)
|
||||
if(!this.distinctPruefungsDates.includes(p.datum)) this.distinctPruefungsDates.push(p.datum)
|
||||
}
|
||||
student.pruefungen.push(p)
|
||||
if(!this.distinctPruefungsDates.includes(p.datum)) this.distinctPruefungsDates.push(p.datum)
|
||||
})
|
||||
|
||||
this.distinctPruefungsDates.sort()
|
||||
@@ -1927,10 +2008,7 @@ export const Benotungstool = {
|
||||
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}`;
|
||||
const dateStr = this.toISODate(this.selectedPruefungDate);
|
||||
|
||||
// when the grade is locked (later pruefung exists) keep the existing note untouched
|
||||
const note = this.pruefungNoteLocked && this.pruefung
|
||||
@@ -2029,6 +2107,7 @@ export const Benotungstool = {
|
||||
return loaded
|
||||
})
|
||||
}).finally(() => {
|
||||
this.password = '' // das Passwort verlässt den Speicher mit dem Dialog
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
@@ -2068,7 +2147,7 @@ export const Benotungstool = {
|
||||
|
||||
// check if student antrittCount is too high already
|
||||
if(!this.canAddPruefung(student)) {
|
||||
this.$fhcAlert.alertWarning('Student ' + student.uid + ' hat bereits ' + student.hoechsterAntritt + ' Prüfungsantritte abgelegt. Es wird keine Prüfung angelegt.')
|
||||
this.warnKeinAntritt(student, 'Es wird keine Prüfung angelegt.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2196,9 +2275,6 @@ export const Benotungstool = {
|
||||
this.selectedLehreinheit = null
|
||||
}
|
||||
},
|
||||
kommPruefSpalteNoetig() {
|
||||
this.applyKommPruefSpalte()
|
||||
},
|
||||
selectedPruefungNote(newVal, oldVal) {
|
||||
if (!newVal || !this.pruefungStudent) return
|
||||
|
||||
@@ -2210,9 +2286,8 @@ export const Benotungstool = {
|
||||
const limit = limitMap[note]
|
||||
if (limit == null) return
|
||||
|
||||
// all exams of the history; the row holds the final exam separately
|
||||
// all exams of the history, the kommissionelle Prüfung included
|
||||
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)
|
||||
@@ -2287,39 +2362,6 @@ export const Benotungstool = {
|
||||
.map(r => r.getData())
|
||||
.filter(s => s.selectable)
|
||||
},
|
||||
getKommPruefCount(){
|
||||
let counter = 0
|
||||
this.studenten?.forEach(s => {if(s['kommPruef']){counter++}})
|
||||
return counter
|
||||
},
|
||||
/**
|
||||
* Titel der kommissionellen Spalte. Im Datumsmodus tragen alle anderen Spalten ihr Datum im
|
||||
* Kopf, daher bekommt diese es auch - aber nur, wenn alle Termine darin auf denselben Tag
|
||||
* fallen. Bei verschiedenen Daten steht das Datum weiterhin in der Zelle.
|
||||
*/
|
||||
kommPruefSpaltentitel() {
|
||||
const titel = this.$capitalize(this.$p.t('benotungstool/c4kommPruef'))
|
||||
if(this.pruefungsspaltenModus !== 'datum') return titel
|
||||
|
||||
const daten = new Set(
|
||||
(this.studenten ?? [])
|
||||
.map(s => (s.kommPruef?.datum ?? '').slice(0, 10))
|
||||
.filter(d => d !== '')
|
||||
)
|
||||
if(daten.size !== 1) return titel
|
||||
|
||||
return `${titel} - ${this.formatDatumDMY([...daten][0])}`
|
||||
},
|
||||
/**
|
||||
* Die kommissionelle Spalte wird gebraucht, sobald ein Termin darin steht ODER bei einer
|
||||
* Zeile der letzte Antritt fällig ist - sonst fehlt der Platz zum Anlegen.
|
||||
*/
|
||||
kommPruefSpalteNoetig() {
|
||||
if(!this.config?.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) return false
|
||||
if(this.getKommPruefCount > 0) return true
|
||||
|
||||
return (this.studenten ?? []).some(s => s.verlauf?.naechsteRolle === 'kommissionell')
|
||||
},
|
||||
getSaveBtnClass() {
|
||||
return this.changedNoten?.length ? "btn btn-primary ml-2" : "btn btn-secondary ml-2"
|
||||
},
|
||||
@@ -2364,10 +2406,16 @@ export const Benotungstool = {
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenfreigabeHinweistextv4'))
|
||||
},
|
||||
getPruefungimportHinweistext() {
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv6'))
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv6')) + this.getNotenkuerzelHinweis
|
||||
},
|
||||
getNotenimportHinweistext() {
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv5'))
|
||||
return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv5')) + this.getNotenkuerzelHinweis
|
||||
},
|
||||
/** One more bullet for both import dialogs, as long as the shorthand is allowed. */
|
||||
getNotenkuerzelHinweis() {
|
||||
if(!this.config?.CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL) return ''
|
||||
|
||||
return '• ' + this.$capitalize(this.$p.t('benotungstool/c4importNotenkuerzel')) + '<br>'
|
||||
},
|
||||
freezableColumnOptions() {
|
||||
// the identity columns the user may pin to the left (matches freezableColumnFields)
|
||||
@@ -2522,7 +2570,37 @@ export const Benotungstool = {
|
||||
</template>
|
||||
</bs-modal>
|
||||
|
||||
<bs-modal data-cy="modal-freigabe" ref="modalContainerNotenSpeichern" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4">
|
||||
<bs-modal data-cy="modal-uebernahme" ref="modalContainerUebernahme" class="bootstrap-prompt" bodyClass="px-3 py-3">
|
||||
<template v-slot:title>{{$capitalize($p.t('benotungstool/c4notenvorschlagUebernehmen'))}}: {{uebernahmeStudent?.vorname}} {{uebernahmeStudent?.nachname}}</template>
|
||||
<template v-slot:default>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-nowrap">{{$capitalize($p.t('benotungstool/c4benotungsdatum'))}}:</span>
|
||||
<div class="flex-grow-1" data-cy="uebernahme-datum">
|
||||
<datepicker
|
||||
v-model="uebernahmeDate"
|
||||
:clearable="false"
|
||||
:enableTimePicker="false"
|
||||
format="dd.MM.yyyy"
|
||||
placeholder="TT.MM.JJJJ"
|
||||
:max-date="uebernahmeMaxDate"
|
||||
:text-input="true"
|
||||
:auto-apply="true"
|
||||
autocomplete="off">
|
||||
</datepicker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-2" data-cy="uebernahme-hinweis">
|
||||
{{$capitalize($p.t('benotungstool/c4benotungsdatumHinweis'))}}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button type="button" data-cy="uebernahme-submit" class="btn btn-primary" @click="saveNotenvorschlagEingabe">{{ $capitalize($p.t('global/speichern')) }}</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
|
||||
<bs-modal data-cy="modal-freigabe" ref="modalContainerNotenSpeichern" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4"
|
||||
@hideBsModal="freigabeModalVisible = false"
|
||||
@showBsModal="freigabeModalVisible = true">
|
||||
<template v-slot:title>{{ $p.t('benotungstool/noteneingabeSpeichern') }}</template>
|
||||
<template v-slot:default>
|
||||
<div class="row justify-content-center">
|
||||
@@ -2558,7 +2636,12 @@ export const Benotungstool = {
|
||||
<div class="col-12 text-center text-muted" data-cy="freigabe-summary-empty">{{ $p.t('benotungstool/c4freigabeSummaryEmpty') }}</div>
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-auto" data-cy="freigabe-passwort">
|
||||
<!--
|
||||
Das Passwortfeld entsteht erst mit dem geöffneten Dialog. Sonst steht es dauerhaft
|
||||
im Dokument, und der Passwortmanager bietet seine Zugangsdaten über jedem anderen
|
||||
Textfeld an, zum Beispiel über dem Datumsfeld der Notenübernahme.
|
||||
-->
|
||||
<div v-if="freigabeModalVisible" class="col-auto" data-cy="freigabe-passwort">
|
||||
<Password v-model="password" :feedback="false" showIcon="fa fa-eye" :toggleMask="true" :promptLabel="$p.t('benotungstool/passwort')"></Password>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2751,9 +2834,9 @@ export const Benotungstool = {
|
||||
<div class="btn-group ml-2" role="group">
|
||||
<button type="button"
|
||||
:class="pruefungsspaltenModus === 'antritt' ? 'btn btn-primary' : 'btn btn-outline-primary'"
|
||||
:title="$capitalize($p.t('benotungstool/c4spaltenAntrittHint'))"
|
||||
:title="$capitalize($p.t('benotungstool/c4spaltenTerminHint'))"
|
||||
@click="setPruefungsspalten('antritt')">
|
||||
{{$capitalize($p.t('benotungstool/c4spaltenAntritt'))}}
|
||||
{{$capitalize($p.t('benotungstool/c4spaltenTermin'))}}
|
||||
</button>
|
||||
<button type="button"
|
||||
:class="pruefungsspaltenModus === 'datum' ? 'btn btn-primary' : 'btn btn-outline-primary'"
|
||||
|
||||
@@ -36,7 +36,8 @@ export const antrittCountStudent = (student, config, notenOptions) => {
|
||||
/** Tells you if the student can get one more attempt. */
|
||||
export const canAddPruefung = (student, config) => {
|
||||
if (student.verlauf) return student.verlauf.canAdd;
|
||||
return !student.kommPruef && antrittCountStudent(student, config) < maxAntrittCount(config);
|
||||
const hatKommPruef = (student.pruefungen ?? []).some((p) => p.kommissionell);
|
||||
return !hatKommPruef && antrittCountStudent(student, config) < maxAntrittCount(config);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+209
-9
@@ -61461,6 +61461,106 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'kommPruefNichtErlaubt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die kommissionelle Prüfung für {0} wird nicht in diesem Werkzeug angelegt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The oral examination for {0} is not created in this tool.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4benotungsdatum',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Benotungsdatum',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Assessment date',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4benotungsdatumHinweis',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die Freigabe macht dieses Datum zum Datum des ersten Antritts.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The release turns this date into the date of the first attempt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'benotungsdatumUngueltig',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Das Benotungsdatum für {0} ist ungültig.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The assessment date for {0} is not valid.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'benotungsdatumInZukunft',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Das Benotungsdatum für {0} darf nicht in der Zukunft liegen.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The assessment date for {0} must not be in the future.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
@@ -62067,6 +62167,26 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4importNotenkuerzel',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Statt der Note können Sie das Kürzel aus der Notenliste eintragen. Beispiel: nb für die Note Nicht beurteilt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Instead of the grade you can enter the shorthand from the grade list. Example: nb for the grade Nicht beurteilt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
@@ -62170,18 +62290,18 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4antrittNr',
|
||||
'phrase' => 'c4terminNr',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '{0}. Antritt',
|
||||
'text' => '{0}. Prüfungstermin',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Attempt {0}',
|
||||
'text' => 'Exam session {0}',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
@@ -62190,18 +62310,18 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4spaltenAntritt',
|
||||
'phrase' => 'c4spaltenTermin',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Antritte',
|
||||
'text' => 'Termine',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Attempts',
|
||||
'text' => 'Exam sessions',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
@@ -62210,18 +62330,98 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4spaltenAntrittHint',
|
||||
'phrase' => 'c4spaltenTerminHint',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Eine Spalte je Antritt, das Prüfungsdatum steht in der Zelle.',
|
||||
'text' => 'Eine Spalte je Prüfungstermin. Das Prüfungsdatum steht in der Zelle.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'One column per attempt, the exam date is shown in the cell.',
|
||||
'text' => 'One column per exam session. The cell shows the exam date.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4badgeAntritt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '{0}. Antritt von {1}. Die Spalte zeigt den Prüfungstermin. Das Symbol zeigt den Antritt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Attempt {0} of {1}. The column shows the exam date. The badge shows the attempt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4badgeKommAntritt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Kommissionelle Prüfung. {0}. Antritt von {1}.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Oral examination. Attempt {0} of {1}.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4badgeOhneAntritt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Dieser Prüfungstermin zählt keinen Antritt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'This exam date uses no attempt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'c4badgeKommOhneAntritt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Kommissionelle Prüfung. Dieser Prüfungstermin zählt keinen Antritt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Oral examination. This exam date uses no attempt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user