Compare commits

..
Author SHA1 Message Date
adisposkofh 3c95907213 modified and put into use combined grade mod permission check function 2026-09-10 14:09:27 +02:00
adisposkofh f30c16aede combined grade modification permissions check functions into one 2026-09-10 12:40:12 +02:00
adisposkofh ab0a0aa5c3 Merge branch 'master' into bugfix/82291-grade-transfer-permissions 2026-09-09 16:17:35 +02:00
Andreas Österreicher 59b4b2bac9 Merge branch 'feature-71639/StudVw_Noten_Renaming_Infopopup' 2026-09-09 12:40:42 +02:00
Andreas Österreicher 5f546fca2a Merge branch 'feature-82630/Infocenter_Verlinkung_StudVW' 2026-09-09 12:37:33 +02:00
Andreas Österreicher f74b885253 Abfrage von Verträgen korrigiert für Fälle wo der Monatsletzte gleichzeitig der Start des neuen Studiensemesters ist (zB 31.08.2026) damit die Abrechnung korrekt funktioniert (BFI) 2026-09-09 08:55:24 +02:00
Andreas Österreicher 3ab402adef Merge branch 'bugfix-82884_grades-export-group-names' 2026-09-08 14:43:17 +02:00
Manuela Thamer 52f24645fb update link StudVW 2026-09-08 14:23:59 +02:00
Manuela Thamer 4ac108174b rename title delete button 2026-09-08 08:29:08 +02:00
adisposkofh 6378b80d87 fixed issue where exported gradelist excel file formatted group names as numbers 2026-09-07 15:24:38 +02:00
adisposkofh 2ed80b59b0 fixed issue with grade transfer permissions 2026-09-03 12:10:02 +02:00
Andreas Österreicher 658c8d6bd4 Schreibberechtigungsprüfung bei Anrechnungen angepasst damit diese korrekt angezeigt wird wenn oe_kurzb nicht gleich der Kurzbz des Studiegangs ist 2026-09-03 08:44:10 +02:00
Andreas Österreicher bf29faa5a7 Merge branch 'feature-82292/Cis4StudiumLVohneModulAnzeigen' 2026-09-02 15:20:28 +02:00
Andreas Österreicher 2ada167557 Merge branch 'bug-82614/StudVw_International_Incoming_Outgoing_GS_wird_nicht_geladen' 2026-09-02 14:56:27 +02:00
chfhtw cc9d1f983e bugfix: missing call to getStudiensemesterKurzbz 2026-09-01 10:39:02 +02:00
Johann Hoffmann 57c48f2c4a cis4 studium page dont filter away lehrveranstaltungen without a module and give them their own box in the ui; 2026-08-26 13:15:16 +02:00
25 changed files with 313 additions and 239 deletions
@@ -167,7 +167,9 @@ class Studium extends FHCAPI_Controller
if($aktuelles_studienplan){
$lehrveranstaltungen = $this->computeStudienplanLehrveranstaltungen($aktuelles_studienplan->studienplan_id, $aktuelles_semester);
foreach($lehrveranstaltungen as $lehrv){
foreach($lehrv->lehrveranstaltungen as $lv){
// a lehrveranstaltung without a modul has no children and gets its lektoren directly
$lvs = isset($lehrv->lehrveranstaltungen) ? $lehrv->lehrveranstaltungen : [$lehrv];
foreach($lvs as $lv){
$lvLektoren =$this->computeLektorenFromLehrveranstaltung($lv->lehrveranstaltung_id,$aktuelles_semester, $aktuelles_studiengang->studiengang_kz, $aktuelles_studiensemester->studiensemester_kurzbz);
$lv->lektoren = $lvLektoren;
}
@@ -286,6 +288,7 @@ SELECT tbl_lehrveranstaltung.*,
$lehrveranstaltungen = $this->LehrveranstaltungModel->execReadOnlyQuery($query,[$studienplan_id, $semester]);
$lehrveranstaltungen = $this->getDataOrTerminateWithError($lehrveranstaltungen);
usort($lehrveranstaltungen, function($a, $b){
if($a->lehrtyp_kurzbz == "modul"){
return -1;
@@ -302,12 +305,17 @@ SELECT tbl_lehrveranstaltung.*,
}
else{
$parent =array_filter($carry, function($item)use($lehrv){
return $item->studienplan_lehrveranstaltung_id == $lehrv->studienplan_lehrveranstaltung_id_parent;
return isset($item->lehrveranstaltungen)
&& $item->studienplan_lehrveranstaltung_id == $lehrv->studienplan_lehrveranstaltung_id_parent;
});
$parent = current($parent);
if($parent){
$parent->lehrveranstaltungen[] = $lehrv;
}
else{
// lehrveranstaltung without a modul: keep it on the top level
array_push($carry, $lehrv);
}
}
return $carry;
}, []);
@@ -198,7 +198,7 @@ class Grades extends FHCAPI_Controller
$now = date('c');
// NOTE(chris): Stg Permissions
if (!$this->hasPermissionUpdate($lehrveranstaltung_id, $student_uid))
if (!$this->hasPermissionGradeUID($lehrveranstaltung_id, $student_uid))
return $this->_outputAuthError([$this->router->method => ['admin', 'assistenz']]);
$this->load->model('education/Zeugnisnote_model', 'ZeugnisnoteModel');
@@ -260,7 +260,7 @@ class Grades extends FHCAPI_Controller
$lehrveranstaltung_id = $this->input->post('lehrveranstaltung_id');
// NOTE(chris): Stg Permissions
if (!$this->hasPermissionDelete($lehrveranstaltung_id, $student_uid))
if (!$this->hasPermissionGradeUID($lehrveranstaltung_id, $student_uid))
return $this->_outputAuthError([$this->router->method => ['admin', 'assistenz']]);
$this->load->model('education/Zeugnisnote_model', 'ZeugnisnoteModel');
@@ -298,7 +298,7 @@ class Grades extends FHCAPI_Controller
$authUID = getAuthUID();
// NOTE(chris): Stg Permissions
if (!$this->hasPermissionCopy($lehrveranstaltung_id, $student_uid))
if (!$this->hasPermissionGradeUID($lehrveranstaltung_id, $student_uid))
return $this->_outputAuthError([$this->router->method => 'student/noten']);
$this->load->model('education/Lvgesamtnote_model', 'LvgesamtnoteModel');
@@ -410,7 +410,7 @@ class Grades extends FHCAPI_Controller
$repeaterGrade = current($repeaterGrade);
// NOTE(chris): Stg Permissions
if (!$this->hasPermissionCopy($repeaterGrade->lehrveranstaltung_id, $repeaterGrade->student_uid))
if (!$this->hasPermissionGradeUID($repeaterGrade->lehrveranstaltung_id, $repeaterGrade->student_uid))
return $this->_outputAuthError([$this->router->method => 'student/noten']);
$data = [
@@ -565,136 +565,57 @@ class Grades extends FHCAPI_Controller
}
/**
* Helper function to check permissions for updateCertificate()
* Combined helper function to check permissions for
* certificate-modifying functions
*
* @param integer $lehrveranstaltung_id
* @param string $student_uid
*
* @return boolean
*/
protected function hasPermissionUpdate($lehrveranstaltung_id, $student_uid)
protected function hasPermissionGradeUID($lehrveranstaltung_id, $student_uid)
{
if ($lehrveranstaltung_id === null || $student_uid === null)
return true;
$this->load->model('crm/Student_model', 'StudentModel');
$result = $this->StudentModel->load([$student_uid]);
if (isError($result) || !hasData($result))
$studentResult = $this->StudentModel->load([$student_uid]);
if (isError($studentResult) || !hasData($studentResult))
return false;
$student = current(getData($result));
$student = current(getData($studentResult));
if ($this->permissionlib->isBerechtigt('admin', 'suid', $student->studiengang_kz))
return true;
if ($this->permissionlib->isBerechtigt('assistenz', 'suid', $student->studiengang_kz))
return true;
$this->load->model('organisation/Studienplan_model', 'StudienplanModel');
$result = $this->StudienplanModel->getAllOesForLv($lehrveranstaltung_id);
if (isError($result))
$oesResult = $this->StudienplanModel->getAllOesForLv($lehrveranstaltung_id);
if (isError($oesResult))
return false;
$oes = getData($result) ?: [];
$oes = hasData($oesResult) ? getData($oesResult) : [];
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$result = $this->LehrveranstaltungModel->getStg($lehrveranstaltung_id);
if (isError($result))
$lvResult = $this->LehrveranstaltungModel->load($lehrveranstaltung_id);
if (isError($lvResult))
return false;
if (hasData($result))
$oes[] = current(getData($result));
if (hasData($lvResult))
$oes[] = current(getData($lvResult));
if ($this->permissionlib->isBerechtigt('student/noten', 'suid', $student->studiengang_kz))
return true;
foreach ($oes as $oe) {
if ($this->permissionlib->isBerechtigt('admin', 'suid', $oe->oe_kurzbz))
if ($this->permissionlib->isBerechtigt('student/noten', 'suid', $oe->oe_kurzbz))
return true;
if ($this->permissionlib->isBerechtigt('assistenz', 'suid', $oe->oe_kurzbz))
if ($this->permissionlib->isBerechtigt('student/noten', 'suid', $oe->studiengang_kz))
return true;
}
return false;
}
/**
* Helper function to check permissions for deleteCertificate()
*
* @param integer $lehrveranstaltung_id
* @param string $student_uid
*
* @return boolean
*/
protected function hasPermissionDelete($lehrveranstaltung_id, $student_uid)
{
if ($lehrveranstaltung_id === null || $student_uid === null)
return true;
$this->load->model('crm/Student_model', 'StudentModel');
$result = $this->StudentModel->load([$student_uid]);
if (isError($result) || !hasData($result))
return false;
$student = current(getData($result));
if ($this->permissionlib->isBerechtigt('admin', 'suid', $student->studiengang_kz))
return true;
if ($this->permissionlib->isBerechtigt('assistenz', 'suid', $student->studiengang_kz))
return true;
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$result = $this->LehrveranstaltungModel->load($lehrveranstaltung_id);
if (isError($result) || !hasData($result))
return false;
$oe = current(getData($result));
if ($this->permissionlib->isBerechtigt('admin', 'suid', $oe->oe_kurzbz))
return true;
if ($this->permissionlib->isBerechtigt('assistenz', 'suid', $oe->oe_kurzbz))
return true;
return false;
}
/**
* Helper function to check permissions for
* copyTeacherProposalToCertificate() and copyRepeaterGradeToCertificate()
*
* @param integer $lehrveranstaltung_id
* @param string $student_uid
*
* @return boolean
*/
protected function hasPermissionCopy($lehrveranstaltung_id, $student_uid)
{
if ($lehrveranstaltung_id === null || $student_uid === null)
return true;
$this->load->model('crm/Student_model', 'StudentModel');
$result = $this->StudentModel->load([$student_uid]);
if (isError($result) || !hasData($result))
return false;
$student = current(getData($result));
if ($this->permissionlib->isBerechtigt('student/noten', 'suid', $student->studiengang_kz))
return true;
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$result = $this->LehrveranstaltungModel->load($lehrveranstaltung_id);
if (isError($result) || !hasData($result))
return false;
$oe = current(getData($result));
if ($this->permissionlib->isBerechtigt('student/noten', 'suid', $oe->oe_kurzbz))
return true;
return false;
}
}
@@ -108,6 +108,7 @@ class Students extends FHCAPI_Controller
'studiensemester_kurzbz' => $studiensemester_kurzbz
]);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$this->studentlistlib->addJoin(
"(
@@ -153,7 +154,8 @@ class Students extends FHCAPI_Controller
$this->addMeta('ci_params', [
'studiensemester_kurzbz' => $studiensemester_kurzbz
]);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$this->studentlistlib->addJoin(
"(
@@ -213,6 +215,7 @@ class Students extends FHCAPI_Controller
'studiensemester_kurzbz' => $studiensemester_kurzbz
]);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$this->studentlistlib->addJoin(
"(
@@ -16,7 +16,7 @@ $query = '
anrechnung.begruendung_id,
anrechnung.dms_id,
CASE
WHEN stg.typ || stg.kurzbz IN (' . $ORGANISATIONSEINHEITEN_SCHREIBBERECHTIGT . ') THEN TRUE
WHEN stg.oe_kurzbz IN (' . $ORGANISATIONSEINHEITEN_SCHREIBBERECHTIGT . ') THEN TRUE
ELSE FALSE
END "schreibberechtigt",
anrechnung.studiensemester_kurzbz,
@@ -57,7 +57,7 @@
<div class="row<?php if ($lockedbyother) echo ' alert-danger' ?>">
<div class="col-lg-8">
<h3 class="page-header">
Infocenter Details: <a target="_blank" title="Studentenverwaltung" href="<?php echo site_url('/Studentenverwaltung/' . $studiensemester . '/person/' . $stammdaten->person_id) ?>"><?php echo $stammdaten->vorname.' '.$stammdaten->nachname ?> <i class="fa fa-external-link" style="font-size:small"></i></a>
Infocenter Details: <a target="_blank" title="Studentenverwaltung" href="<?php echo site_url('/studvw/stdsem/' . $studiensemester . '/person/' . $stammdaten->person_id) ?>"><?php echo $stammdaten->vorname.' '.$stammdaten->nachname ?> <i class="fa fa-external-link" style="font-size:small"></i></a>
</h3>
</div>
<div class="col-lg-4">
+1 -1
View File
@@ -351,7 +351,7 @@ if (!$berechtigung->isBerechtigt('admin')
{
$worksheet->write($lines,3,$elem->vorname);
}
$worksheet->write($lines,4,$elem->semester.$elem->verband.$elem->gruppe);
$worksheet->writeString($lines,4,$elem->semester.$elem->verband.$elem->gruppe);
$worksheet->write($lines,5,trim($elem->matrikelnr),$format_highlight);
$worksheet->write($lines,6, $note, $format_highlightright);
+1 -1
View File
@@ -121,7 +121,7 @@ class vertrag extends basis_db
JOIN lehre.tbl_lehreinheit USING(lehreinheit_id)
WHERE
vertrag_id=tbl_vertrag.vertrag_id
AND studiensemester_kurzbz in (SELECT studiensemester_kurzbz FROM public.tbl_studiensemester WHERE start>=".$this->db_add_param($datum).")
AND studiensemester_kurzbz in (SELECT studiensemester_kurzbz FROM public.tbl_studiensemester WHERE start>".$this->db_add_param($datum).")
)
)";
}
+4 -4
View File
@@ -22,7 +22,6 @@ import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js";
import ApiRouteInfo from '../../api/factory/routeinfo.js';
import {capitalize} from "../../helpers/StringHelpers.js";
import { getTodayISO, toDateTime } from "../../helpers/DateHelpers.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const isMobile = window.matchMedia("(max-width: 767px)").matches;
@@ -89,7 +88,7 @@ const router = VueRouter.createRouter({
params: { // in this case always populate other params since they are not optional
ort_kurzbz: to.params.ort_kurzbz,
mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP,
focus_date: getTodayISO()
focus_date: new Date().toISOString().split("T")[0]
},
};
},
@@ -108,7 +107,8 @@ const router = VueRouter.createRouter({
: (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP);
// default to today date if not provided
const focus_date = toDateTime(route.params.focus_date) ? route.params.focus_date : getTodayISO();
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag
@@ -127,7 +127,7 @@ const router = VueRouter.createRouter({
name: "RoomInformation",
params: {
mode: to.params.mode || DEFAULT_MODE_RAUMINFO,
focus_date: to.params.focus_date || getTodayISO(),
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0],
ort_kurzbz: route.params.ort_kurzbz
}
});
@@ -2,7 +2,7 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import { getDateStyleClass } from "./getDateStyleClass.js";
import { compareISODateValues, formatDate, formatDateTime, getTodayISO } from "../../../helpers/DateHelpers.js";
import {compareISODateValues, formatDateTime, formatISODate, getViennaTodayISO} from "./dateUtils.js";
export const AbgabeMitarbeiterDetail = {
@@ -174,7 +174,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '', // todo kurzbz textfield value vorschlag für qualgates
'datum': getTodayISO(),
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -351,7 +351,9 @@ export const AbgabeMitarbeiterDetail = {
}
}
},
formatDate,
formatDate(dateParam) {
return formatISODate(dateParam)
},
openCreateNewAbgabeModal() {
if(this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter') {
// maybe alert that this is not allowed or smth
@@ -368,7 +370,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': getTodayISO(),
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -402,7 +404,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': getTodayISO(),
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -601,7 +603,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': getTodayISO(),
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -3,7 +3,7 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { formatDate, getTodayISO } from "../../../helpers/DateHelpers.js";
import { formatISODate, getViennaTodayISO } from "./dateUtils.js";
import { validateThesisTitle } from './titleValidation.js'
@@ -248,7 +248,9 @@ export const AbgabeStudentDetail = {
window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url)
},
formatDate,
formatDate(dateParam) {
return formatISODate(dateParam)
},
async upload(termin) {
if (! await this.validate(termin))
@@ -284,7 +286,7 @@ export const AbgabeStudentDetail = {
if(res.meta.status == "success") {
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3')))
termin.abgabedatum = getTodayISO();
termin.abgabedatum = getViennaTodayISO();
if(res?.data?.signatur !== undefined) {
termin.signatur = res.data.signatur
}
@@ -1,5 +1,5 @@
import AbgabeterminStatusLegende from "./StatusLegende.js";
import { formatDateTime } from "../../../helpers/DateHelpers.js";
import {formatDateTime} from "./dateUtils.js";
export const AbgabeStudentTimeline = {
name: "AbgabeStudentTimeline",
@@ -11,7 +11,7 @@ import AbgabeStudentTimeline from "./AbgabeStudentTimeline.js";
import { splitMailsHelper } from "../../../helpers/EmailHelpers.js"
import { getDateStyleClass} from "./getDateStyleClass.js";
import { dateFilter } from '../../../tabulator/filters/DatesManual.js';
import { compareISODateValues, formatDate, getNow, getTodayISO, toDateTime } from "../../../helpers/DateHelpers.js";
import { compareISODateValues, formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js";
export const AbgabetoolAssistenz = {
name: "AbgabetoolAssistenz",
@@ -114,7 +114,7 @@ export const AbgabetoolAssistenz = {
fixtermin: false,
},
serienTermin: Vue.reactive({
datum: getTodayISO(),
datum: getViennaTodayISO(),
bezeichnung: {
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
@@ -643,7 +643,7 @@ export const AbgabetoolAssistenz = {
upload_allowed: false,
fixtermin: false,
}
this.serienEdit.datum = getTodayISO()
this.serienEdit.datum = getViennaTodayISO()
this.serienEdit.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === 'zwischen')
this.serienEdit.kurzbz = ''
this.serienEdit.upload_allowed = false
@@ -1091,9 +1091,23 @@ export const AbgabetoolAssistenz = {
return false;
}
const rowDate = toDateTime(rowVal);
const von = toDateTime(filterVal[0]);
const bis = toDateTime(filterVal[1]);
const toLuxon = (val) => {
if (!val) return null;
let dt;
if (val instanceof Date) {
dt = luxon.DateTime.fromJSDate(val);
} else if (typeof val === "string") {
dt = toViennaDate(val);
} else { // fallback
dt = luxon.DateTime.fromMillis(Number(val));
}
return dt.isValid ? dt : null;
};
const rowDate = toLuxon(rowVal);
const von = toLuxon(filterVal[0]);
const bis = toLuxon(filterVal[1]);
// specific day
if (von && !bis) {
@@ -1114,8 +1128,22 @@ export const AbgabetoolAssistenz = {
const rowDate = rowVal.luxonDate;
const von = toDateTime(filterVal[0]);
const bis = toDateTime(filterVal[1]);
const toLuxon = (val) => {
if (!val) return null;
let dt;
if (val instanceof Date) {
dt = luxon.DateTime.fromJSDate(val);
} else if (typeof val === "string") {
dt = toViennaDate(val);
} else { // fallback
dt = luxon.DateTime.fromMillis(Number(val));
}
return dt.isValid ? dt : null;
};
const von = toLuxon(filterVal[0]);
const bis = toLuxon(filterVal[1]);
// specific day
if (von && !bis) {
@@ -1344,7 +1372,7 @@ export const AbgabetoolAssistenz = {
},
checkAbgabetermineProjektarbeit(projekt) {
const now = getNow()
const now = luxon.DateTime.now()
// calculate Abgabetermin time diff to now and assign last and next to projekt
projekt.abgabetermine.forEach(termin => {
@@ -1359,7 +1387,7 @@ export const AbgabetoolAssistenz = {
// while already looping through each termin, calculate datestyle beforehand
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
const date = toDateTime(termin.datum).endOf('day')
const date = toViennaDate(termin.datum).endOf('day')
termin.luxonDate = date
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
@@ -1626,7 +1654,9 @@ export const AbgabetoolAssistenz = {
getNotenFilterOptionLabel(option) {
return option.bezeichnung
},
formatDate,
formatDate(dateParam) {
return formatISODate(dateParam);
},
formAction(cell) {
const actionButtons = document.createElement('div');
actionButtons.className = "d-flex gap-3";
@@ -1821,7 +1851,9 @@ export const AbgabetoolAssistenz = {
return str
},
isPastDate(date) {
return getNow() > toDateTime(date).endOf("day");
const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
},
setDetailComponent(details){
const pa = this.projektarbeiten.find(projektarbeit => projektarbeit.projektarbeit_id == details.projektarbeit_id)
@@ -7,7 +7,7 @@ import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass } from "./getDateStyleClass.js";
import { dateFilter } from '../../../tabulator/filters/DatesManual.js';
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
import { formatDate, getNow, getTodayISO, toDateTime } from "../../../helpers/DateHelpers.js";
import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js";
export const AbgabetoolMitarbeiter = {
name: "AbgabetoolMitarbeiter",
@@ -54,7 +54,7 @@ export const AbgabetoolMitarbeiter = {
allowedNotenOptions: null,
notenOptionsNonFinal: null,
serienTermin: Vue.reactive({
datum: getTodayISO(),
datum: getViennaTodayISO(),
bezeichnung: {
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
@@ -668,8 +668,22 @@ export const AbgabetoolMitarbeiter = {
const rowDate = rowVal.luxonDate;
const von = toDateTime(filterVal[0]);
const bis = toDateTime(filterVal[1]);
const toLuxon = (val) => {
if (!val) return null;
let dt;
if (val instanceof Date) {
dt = luxon.DateTime.fromJSDate(val);
} else if (typeof val === "string") {
dt = toViennaDate(val);
} else { // fallback
dt = luxon.DateTime.fromMillis(Number(val));
}
return dt.isValid ? dt : null;
};
const von = toLuxon(filterVal[0]);
const bis = toLuxon(filterVal[1]);
// specific day
if (von && !bis) {
@@ -885,14 +899,14 @@ export const AbgabetoolMitarbeiter = {
}
},
checkAbgabetermineProjektarbeit(projekt) {
const now = getNow()
const now = luxon.DateTime.now()
// calculate Abgabetermin time diff to now and assign last and next to projekt
projekt.abgabetermine.forEach(termin => {
// while already looping through each termin, calculate datestyle beforehand
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
const date = toDateTime(termin.datum).endOf('day')
const date = toViennaDate(termin.datum).endOf('day')
termin.luxonDate = date
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
@@ -1003,7 +1017,9 @@ export const AbgabetoolMitarbeiter = {
getOptionLabelAbgabetyp(option){
return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz)
},
formatDate,
formatDate(dateParam) {
return formatISODate(dateParam);
},
undoSelection(cell) {
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
// but actually just revert it after the fact
@@ -1088,7 +1104,9 @@ export const AbgabetoolMitarbeiter = {
return str
},
isPastDate(date) {
return getNow() > toDateTime(date).endOf("day");
const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
},
setDetailComponent(details){
this.loading=true
@@ -4,7 +4,6 @@ import ApiAuthinfo from '../../../api/factory/authinfo.js';
import BsModal from "../../Bootstrap/Modal.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass} from "./getDateStyleClass.js";
import { getNow, toDateTime } from "../../../helpers/DateHelpers.js";
import { validateThesisTitle } from './titleValidation.js'
export const AbgabetoolStudent = {
@@ -172,7 +171,9 @@ export const AbgabetoolStudent = {
return qgate1positiv && qgate2positiv
},
isPastDate(date) {
return getNow() > toDateTime(date).endOf("day");
const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
},
setDetailComponent(details){
this.loading = true
@@ -0,0 +1,33 @@
const zone = 'Europe/Vienna';
export function getViennaTodayISO() {
return luxon.DateTime.now().setZone(zone).toISODate();
}
export function formatISODate(dateParam) {
if (!dateParam) return '';
const date = luxon.DateTime.fromISO(String(dateParam), { zone });
return date.isValid ? date.toFormat('dd.MM.yyyy') : '';
}
export function formatDateTime(dateParam) {
if (!dateParam) return '';
const date = luxon.DateTime.fromSQL(dateParam);
return date.isValid ? date.toFormat("dd.MM.yyyy HH:mm") : '';
}
export function toViennaDate(dateParam) {
if (!dateParam) return null;
return luxon.DateTime.fromISO(String(dateParam), { zone });
}
export function compareISODateValues(a, b) {
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
return String(a).localeCompare(String(b));
}
@@ -1,12 +1,12 @@
import { getNow, toDateTime } from "../../../helpers/DateHelpers.js";
const zone = 'Europe/Vienna';
export function getDateStyleClass(termin, notenOptions) {
const today = getNow();
const datum = toDateTime(termin.datum)?.endOf('day');
const abgabedatum = toDateTime(termin.abgabedatum);
termin.diffindays = datum ? datum.diff(today, 'days').days : NaN;
const isLate = abgabedatum && datum && abgabedatum > datum;
const today = luxon.DateTime.now().setZone(zone);
const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day');
const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null;
termin.diffindays = datum.diff(today, 'days').days;
const isLate = abgabedatum && abgabedatum > datum;
// GRADE STATUS
if (termin.note) {
+17 -4
View File
@@ -1,4 +1,4 @@
import { formatDate, formatTime } from "../../../helpers/DateHelpers.js"
import { numberPadding, formatDate } from "../../../helpers/DateHelpers.js"
export default {
props: {
@@ -25,14 +25,27 @@ export default {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/CisVue/Cms/content/${this.event.ort_content_id}`
},
start_time: function () {
return this.event.start ? formatTime(this.event.start) : 'N/A';
if (!this.event.start) return 'N/A';
if (!this.event.start instanceof Date) {
return this.event.start;
}
return numberPadding(this.event.start.getHours()) + ":" + numberPadding(this.event.start.getMinutes());
},
end_time: function () {
return this.event.end ? formatTime(this.event.end) : 'N/A';
if (!this.event.end) return 'N/A';
if (!this.event.end instanceof Date) {
return this.event.end;
}
return numberPadding(this.event.end.getHours()) + ":" + numberPadding(this.event.end.getMinutes());
}
},
methods: {
methodFormatDate: formatDate,
mehtodNumberPadding: function (number) {
return numberPadding(number);
},
methodFormatDate: function (d) {
return formatDate(d);
},
},
template:/*html*/`
<table class="table table-hover mb-4">
@@ -1,7 +1,6 @@
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import { getTodayISO } from "../../../helpers/DateHelpers.js";
export const DEFAULT_MODE_RAUMINFO_MOBILE = 'List';
export const DEFAULT_MODE_RAUMINFO_DESKTOP = 'Week';
@@ -18,7 +17,7 @@ export default {
inject: ["isMobile"],
computed: {
currentDay() {
return this.propsViewData?.focus_date || getTodayISO();
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
},
currentMode() {
const defaultMode = this.isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP;
+20 -13
View File
@@ -1,7 +1,6 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiOrt from '../../../api/factory/ort.js'
import { getNow, getTodayISO } from "../../../helpers/DateHelpers.js";
export const Raumsuche = {
name: "Raumsuche",
components: {
@@ -11,8 +10,6 @@ export const Raumsuche = {
},
inject: ["isMobile"],
data() {
const now = getNow();
return {
phrasenPromise: null,
phrasenResolved: false,
@@ -26,14 +23,14 @@ export const Raumsuche = {
},
anzahl: 1,
selectedType: null,
datum: getTodayISO(),
datum: new Date(),
von: Vue.ref({
hours: now.hour,
minutes: now.minute
hours: new Date().getHours(),
minutes: new Date().getMinutes()
}),
bis: Vue.ref({
hours: now.plus({ hours: 1 }).hour,
minutes: now.minute
hours: new Date().getHours() + 1,
minutes: new Date().getMinutes()
}),
datepickerTextInputOptions: {
enterSubmit: true,
@@ -139,7 +136,7 @@ export const Raumsuche = {
})
},
loadRooms() {
this.$api.call(ApiOrt.getRooms(this.datum, this.getTimeString(this.von), this.getTimeString(this.bis), this.selectedType?.raumtyp_kurzbz ?? '', this.anzahl))
this.$api.call(ApiOrt.getRooms(this.datum.toISOString(), this.getTimeString(this.von), this.getTimeString(this.bis), this.selectedType?.raumtyp_kurzbz ?? '', this.anzahl))
.then(res => {
if(res?.data?.retval) this.setupData(res.data.retval)
})
@@ -153,6 +150,17 @@ export const Raumsuche = {
setRoute(val) {
// TODO: router push
},
dateFormat(date) {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`
},
timeFormat(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
},
async setupMounted() {
this.tableBuiltPromise = new Promise(this.tableResolve)
@@ -191,8 +199,7 @@ export const Raumsuche = {
:dark="isDarkMode"
:clearable="false"
:enable-time-picker="false"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:format="dateFormat"
:text-input="datepickerTextInputOptions"
:min-date="new Date()"
date-picker
@@ -206,7 +213,7 @@ export const Raumsuche = {
v-model="von"
:dark="isDarkMode"
:clearable="false"
format="HH:mm"
:format="timeFormat"
:text-input="timepickerTextInputOptions"
:is-24="true"
time-picker
@@ -220,7 +227,7 @@ export const Raumsuche = {
v-model="bis"
:dark="isDarkMode"
:clearable="false"
format="HH:mm"
:format="timeFormat"
:text-input="timepickerTextInputOptions"
:is-24="true"
time-picker
@@ -206,6 +206,8 @@ export default {
});
this.lehrveranstaltungen.forEach((lehrveranstaltung)=>{
// a lehrveranstaltung without a modul has no children
if(!Array.isArray(lehrveranstaltung.lehrveranstaltungen)) return;
lehrveranstaltung.lehrveranstaltungen.sort((lv1,lv2)=>{
if (lv1.bezeichnung.toLowerCase() > lv2.bezeichnung.toLowerCase()) {
return 1;
@@ -393,6 +395,16 @@ export default {
</ul>
</div>
</div>
<div class="card" v-else-if="!Array.isArray(lehrveranstaltung.lehrveranstaltungen)">
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="d-flex list-group-item">
<a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lehrveranstaltung)">{{isGermanLanguage ? lehrveranstaltung.bezeichnung : lehrveranstaltung.bezeichnung_english}}</a>
<p>{{lehrveranstaltung.lehrform_kurzbz}}</p>
</li>
</ul>
</div>
</div>
</template>
</div>
@@ -1,5 +1,3 @@
import { formatDate, formatDateTime } from "../../helpers/DateHelpers.js";
export default {
props: [
"config",
@@ -32,7 +30,14 @@ export default {
},
},
methods: {
formatDateTime,
getDate: formatDate
formatDateTime: function(dateTime) {
const dt = new Date(dateTime);
return dt.getDate() + '.' + (dt.getMonth()+1) + '.' + dt.getFullYear() + ' | '
+ dt.getHours() + ':' + dt.getMinutes();
},
getDate: function(dateTime){
const dt = new Date(dateTime);
return dt.getDate() + '.' + (dt.getMonth()+1) + '.' + dt.getFullYear();
}
}
}
+9 -3
View File
@@ -1,6 +1,6 @@
import AbstractWidget from './Abstract.js';
import BsModal from '../Bootstrap/Modal.js';
import { formatDate, formatTime } from '../../helpers/DateHelpers.js';
import { numberPadding } from '../../helpers/DateHelpers.js';
import ApiCms from '../../api/factory/cms.js';
const MAX_LOADED_NEWS = 30;
@@ -84,8 +84,14 @@ export default {
});
})
},
formatDate,
formatTime,
formatDate: function (dateTime) {
const dt = new Date(dateTime);
return numberPadding(dt.getDate()) + '.' + numberPadding((dt.getMonth() + 1)) + '.' + dt.getFullYear();
},
formatTime: function (dateTime) {
const dt = new Date(dateTime);
return numberPadding(dt.getHours()) + ':' + numberPadding(dt.getMinutes());
},
isString(value){
return Object.prototype.toString.call(value) === '[object String]';
},
@@ -255,7 +255,7 @@ export default {
deleteButton.className = 'btn btn-outline-secondary';
const icon = document.createElement('i');
icon.className = 'fa fa-trash';
icon.title = this.$p.t('ui/loeschen');
icon.title = this.$p.t('lehre/info_deleteMarkEntry');
deleteButton.append(icon);
deleteButton.addEventListener('click', evt => {
evt.stopPropagation();
+52 -60
View File
@@ -1,69 +1,61 @@
// Everything parses and formats with luxon in the server timezone. new Date("2026-09-02")
// reads UTC midnight and shows the day before west of Greenwich.
// HELPER FILE -- that contains multiple functions which create, handle and manipulate js dates
function getZone() {
return globalThis.FHC_JS_DATA_STORAGE_OBJECT?.timezone || 'Europe/Vienna';
// custom Error class for DateHelpers.js misuse
class DateHelperError extends Error {
constructor(message) {
super(message);
this.name = "DateHelperError";
}
}
/**
* converts a date value into a luxon DateTime in the server timezone
* adds padding to a number
*
* @param {string|number|Date|Object} value - ISO string, SQL string, milliseconds, Date or luxon DateTime.
* @returns {Object|null} luxon DateTime, or null if the value is empty or unparsable.
* @param {number|string} number - the number on which padding should be added.
* @returns {string} number with padding.
*/
export function toDateTime(value) {
if (!value) return null;
const zone = getZone();
let date;
if (value instanceof luxon.DateTime) date = value.setZone(zone);
else if (value instanceof Date) date = luxon.DateTime.fromJSDate(value, { zone });
else if (typeof value === 'number') date = luxon.DateTime.fromMillis(value, { zone });
// fromISO rejects the space that SQL puts between date and time
else if (typeof value === 'string') date = value.includes(' ')
? luxon.DateTime.fromSQL(value, { zone })
: luxon.DateTime.fromISO(value, { zone });
else return null;
return date.isValid ? date : null;
}
// current time in the server timezone
export function getNow() {
return luxon.DateTime.now().setZone(getZone());
}
// today in the server timezone, as yyyy-MM-dd
export function getTodayISO() {
return getNow().toISODate();
}
// the three formatters return '' for an empty or unparsable value
export function formatDate(value) {
return toDateTime(value)?.toFormat('dd.MM.yyyy') ?? '';
}
export function formatTime(value) {
return toDateTime(value)?.toFormat('HH:mm') ?? '';
}
export function formatDateTime(value) {
return toDateTime(value)?.toFormat('dd.MM.yyyy HH:mm') ?? '';
}
// tabulator sorter for ISO dates, sorts empty values last
export function compareISODateValues(a, b) {
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
return String(a).localeCompare(String(b));
}
// pads hours and minutes to two digits
export function numberPadding(number) {
return String(number).padStart(2, '0');
if(typeof number !== "string" && typeof number !== "number")
{
throw new TypeError("function numberPadding in file DateHelpers.js is only usable with strings or numbers");
}
if(number.toString().length > 2)
{
throw new DateHelperError("The number on which the padding should be added should not be longer than to 2 characters, please refere to the function numberPadding in the helper file DateHelpers.js");
}
return number.toString().length == 1 ? '0' + number.toString() : number.toString();
}
/**
* formats date to dd.mm.yyyy
*
* @param {string|Date} d - the date that should be formatted.
* @returns {string} formatted date string.
*/
export function formatDate(d) {
// parameter is of type Date
if(d instanceof Date)
{
if (isNaN(date.valueOf())) {
return 'N/A';
}
// if the date is an invalid string then creating a date from the string will fail and N/A is returned
return `${numberPadding(d.getDate())}.${numberPadding(d.getMonth() + 1)}.${d.getFullYear()}`;
}
// parameter is of type string
else if (typeof d === "string")
{
let date = new Date(d);
// if the date is an invalid string then creating a date from the string will fail and N/A is returned
if (isNaN(date.valueOf())) {
return 'N/A';
}
return `${numberPadding(date.getDate())}.${numberPadding(date.getMonth() + 1)}.${date.getFullYear()}`;
}
// parameter is not of type string or Date and an exception is thrown
else
{
throw new TypeError("The parameter provided for this function is not a string or a Date object, please refere to the function formatDate in the DateHelpers.js file");
}
}
+20
View File
@@ -62411,6 +62411,26 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
array(
'app' => 'core',
'category' => 'lehre',
'phrase' => 'info_deleteMarkEntry',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Noteneintrag entfernen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Remove grade entry',
'description' => '',
'insertvon' => 'system'
)
)
),
);