diff --git a/application/controllers/api/frontend/v1/LvMenu.php b/application/controllers/api/frontend/v1/LvMenu.php index 45936d9f5..27cbba1c2 100644 --- a/application/controllers/api/frontend/v1/LvMenu.php +++ b/application/controllers/api/frontend/v1/LvMenu.php @@ -36,7 +36,8 @@ class LvMenu extends FHCAPI_Controller public function __construct() { parent::__construct([ - 'getLvMenu' => self::PERM_LOGGED + 'getLvMenu' => self::PERM_LOGGED, + 'getMultipleLvMenu' => self::PERM_LOGGED ]); $this->load->model("ressource/Mitarbeiter_model"); @@ -61,24 +62,23 @@ class LvMenu extends FHCAPI_Controller /** * alternative function to get multiple lvMenus with a single http request + * not yet working as intended as the menu_lv.inc.php scripts called by the + * lvMenuBuild event have logic coupled to require_once import which results in + * a wrong logic after the first invocation -> faulty results for lvinfo, moodle + * and several others */ - public function getMultipleLvMenu($lvMenuOptionList){ + public function getMultipleLvMenu(){ + $lvMenuOptionList = $this->input->post('lvMenuOptionList', true); $result =[]; foreach($lvMenuOptionList as $lvMenuOptions){ - $lvMenu = $this->getLvMenu($lvMenuOptions['lvid'],$lvMenuOptions['studiensemester_kurzbz']); - if(isError($lvMenu)){ - // TODO: some lvMenu threw an error, handle error here - } + $lvMenu = $this->getLvMenuInternal($lvMenuOptions['lvid'],$lvMenuOptions['studiensemester_kurzbz']); + $result[$lvMenuOptions['lvid']]=$lvMenu; } $this->terminateWithSuccess($result); } - - /** - * - */ - public function getLvMenu($lvid, $studiensemester_kurzbz) - { + + private function getLvMenuInternal($lvid, $studiensemester_kurzbz) { // return early if parameters are missing if(!isset($lvid) || !isset($studiensemester_kurzbz)) @@ -89,14 +89,14 @@ class LvMenu extends FHCAPI_Controller // get the user if (!$user=getAuthUID()) - $this->terminateWithError($this->p->t('global', 'nichtAngemeldet')); + $this->terminateWithError($this->p->t('global', 'nichtAngemeldet')); // check if is_lector $is_lector = false; $mares = $this->Mitarbeiter_model->isMitarbeiter($user); if(hasData($mares)) { - $is_lector = getData($mares); + $is_lector = getData($mares); } // definition of user_is_allowed_to_upload @@ -105,7 +105,7 @@ class LvMenu extends FHCAPI_Controller // load lehrveranstaltung $lvres = $this->Lehrveranstaltung_model->load($lvid); - if(!hasData($lvres)) + if(!hasData($lvres)) { $this->terminateWithError('LV ' . $lvid . ' not found.'); } @@ -124,7 +124,7 @@ class LvMenu extends FHCAPI_Controller $stgres = $this->Studiengang_model->load(strval($studiengang_kz)); if(!hasData($stgres)) { - $this->terminateWithError('Stg ' . $lv->studiengang_kz . ' not found.'); + $this->terminateWithError('Stg ' . $lv->studiengang_kz . ' not found.'); } $stg = (getData($stgres))[0]; $kurzbz = strtoupper($stg->typ . $stg->kurzbz); @@ -139,7 +139,7 @@ class LvMenu extends FHCAPI_Controller $angemeldet = false; $lesres = $this->Lehreinheit_model->getLehreinheitenForStudentAndStudienSemester( - $lvid, $user, $angezeigtes_stsem + $lvid, $user, $angezeigtes_stsem ); if(hasData($lesres) && count(getData($lesres)) > 0) @@ -148,7 +148,7 @@ class LvMenu extends FHCAPI_Controller // lehrfach $lehrfach_id=''; - + if(defined('CIS_LEHRVERANSTALTUNG_LEHRFACH_ANZEIGEN') && CIS_LEHRVERANSTALTUNG_LEHRFACH_ANZEIGEN) { // Wenn der eingeloggte User zu einer der Lehreinheiten zugeteilt ist @@ -211,8 +211,8 @@ class LvMenu extends FHCAPI_Controller foreach($fbs as $row) { $lehrfach_oe_kurzbz_arr[] = $row->oe_kurzbz; - if($this->PermissionLib->isBerechtigt('lehre', null, $row->oe_kurzbz) - || $this->PermissionLib->isBerechtigt('assistenz', null, $stg->oe_kurzbz)) + if($this->PermissionLib->isBerechtigt('lehre', null, $row->oe_kurzbz) + || $this->PermissionLib->isBerechtigt('assistenz', null, $stg->oe_kurzbz)) { $user_is_allowed_to_upload=true; } @@ -224,21 +224,21 @@ class LvMenu extends FHCAPI_Controller $menu = array(); $this->fhc_menu_lvinfo($menu, $lvid, $studiengang_kz, $lektor_der_lv, $is_lector, $lehrfach_oe_kurzbz_arr); - + $this->fhc_menu_feedback($menu, $angemeldet, $lvid); - + $this->fhc_menu_gesamtnote($menu, $angemeldet, $lvid, $lv, $is_lector, $angezeigtes_stsem); - + $this->fhc_menu_emailStudierende($menu, $user, $angemeldet, $lvid, $angezeigtes_stsem); - + $this->fhc_menu_abmeldung($menu, $user, $is_lector, $lvid, $angezeigtes_stsem); - + $this->fhc_menu_lehretools($menu, $lvid, $angezeigtes_stsem, $sprache); - + $this->fhc_menu_anrechnungStudent($menu, $lvid, $angezeigtes_stsem); - + $this->fhc_menu_anrechnungLector($menu, $angezeigtes_stsem); - + // Addons Menu Logic // ########################################################################################## @@ -272,18 +272,18 @@ class LvMenu extends FHCAPI_Controller 'permissionLib' => &$this->PermissionLib, 'phrasesLib' => &$this->PhrasesLib ]; - - Events::trigger('lvMenuBuild', - // passing $menu per reference - function & () use (&$menu) { - return $menu; - }, - $params + + Events::trigger('lvMenuBuild', + // passing $menu per reference + function & () use (&$menu) { + return $menu; + }, + $params ); // Menu sortieren // ########################################################################################## - + foreach ($menu as $key => $row){ // removes menu points that are not needed in the c4 lvUebersicht @@ -291,7 +291,7 @@ class LvMenu extends FHCAPI_Controller unset($menu[$key]); continue; } - + // fills pos array to sort the menu $pos[$key] = $row['position']; @@ -299,11 +299,18 @@ class LvMenu extends FHCAPI_Controller array_multisort($pos, SORT_ASC, SORT_NUMERIC, $menu); - // HTTP response - // ########################################################################################## + + return $menu; + } + + /** + * + */ + public function getLvMenu($lvid, $studiensemester_kurzbz) + { + $menu = $this->getLvMenuInternal($lvid, $studiensemester_kurzbz); $this->terminateWithSuccess($menu); - } private function fhc_menu_lvinfo(&$menu, $lvid, $studiengang_kz, $lektor_der_lv, $is_lector, $lehrfach_oe_kurzbz_arr){ diff --git a/application/controllers/components/Cis/Mylv.php b/application/controllers/components/Cis/Mylv.php index 1fdb7e2a1..b6d10931c 100644 --- a/application/controllers/components/Cis/Mylv.php +++ b/application/controllers/components/Cis/Mylv.php @@ -13,12 +13,13 @@ class Mylv extends Auth_Controller */ public function __construct() { + parent::__construct([ - 'Student' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions? - 'Studiensemester' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions? - 'Lvs' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions? - 'Info' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions? - 'Pruefungen' => ['student/anrechnung_beantragen:r','user:r'] // TODO(chris): permissions? + 'Student' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions? + 'Studiensemester' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions? + 'Lvs' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions? + 'Info' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions? + 'Pruefungen' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'] // TODO(chris): permissions? ]); } @@ -44,13 +45,27 @@ class Mylv extends Auth_Controller public function Studiensemester() { $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); + $this->load->model('crm/Student_model', 'StudentModel'); + $this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel'); - $result = $this->StudiensemesterModel->getWhereStudentHasLvs(getAuthUID()); + $isMitarbeiter = getData($this->MitarbeiterModel->isMitarbeiter(getAuthUID())) ?? false; + if($isMitarbeiter) { + $result = $this->StudiensemesterModel->getWhereMitarbeiterHasLvs(getAuthUID()); - if (isError($result)) - return $this->outputJsonError(getError($result)); + if (isError($result)) + return $this->outputJsonError(getError($result)); - $this->outputJsonSuccess(getData($result)); + $this->outputJsonSuccess(getData($result)); + } else if(getData($this->StudentModel->isStudent(getAuthUID())) ?? false) { // $isStudent + $result = $this->StudiensemesterModel->getWhereStudentHasLvs(getAuthUID()); + + if (isError($result)) + return $this->outputJsonError(getError($result)); + + $this->outputJsonSuccess(getData($result)); + } else { + $this->outputJsonError('neither student or mitarbeiter'); + } } /** @@ -58,13 +73,27 @@ class Mylv extends Auth_Controller public function Lvs($studiensemester_kurzbz) { $this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); + $this->load->model('crm/Student_model', 'StudentModel'); + $this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel'); - $result = $this->LehrveranstaltungModel->getLvsByStudentWithGrades(getAuthUID(), $studiensemester_kurzbz, getUserLanguage()); + $isMitarbeiter = getData($this->MitarbeiterModel->isMitarbeiter(getAuthUID())) ?? false; + if($isMitarbeiter) { + $result = $this->LehrveranstaltungModel->getLvsByMitarbeiterInSemester(getAuthUID(), $studiensemester_kurzbz); - if (isError($result)) - return $this->outputJsonError(getError($result)); + if (isError($result)) + return $this->outputJsonError(getError($result)); - $this->outputJsonSuccess(getData($result)); + $this->outputJsonSuccess(getData($result)); + } else if(getData($this->StudentModel->isStudent(getAuthUID())) ?? false) { // $isStudent + $result = $this->LehrveranstaltungModel->getLvsByStudentWithGrades(getAuthUID(), $studiensemester_kurzbz, getUserLanguage()); + + if (isError($result)) + return $this->outputJsonError(getError($result)); + + $this->outputJsonSuccess(getData($result)); + } else { + $this->outputJsonError('neither student or mitarbeiter'); + } } /** diff --git a/application/models/education/Lehrveranstaltung_model.php b/application/models/education/Lehrveranstaltung_model.php index f6b54098b..463c7bcfb 100644 --- a/application/models/education/Lehrveranstaltung_model.php +++ b/application/models/education/Lehrveranstaltung_model.php @@ -1366,4 +1366,45 @@ class Lehrveranstaltung_model extends DB_Model return $this->execReadOnlyQuery($qry, array($sem_kurzbz, $uid)); } + + // used for cis4 mylv mitarbeiter + public function getLvsByMitarbeiterInSemester($mitarbeiter_uid, $sem_kurzbz) { + $qry = "SELECT * FROM ( + SELECT DISTINCT ON (lehre.tbl_lehrveranstaltung.lehrveranstaltung_id) + public.tbl_studiengang.studiengang_kz, + lehre.tbl_lehrveranstaltung.semester, + public.tbl_studiengang.bezeichnung as sg_bezeichnung, + public.tbl_studiengang.english as sg_bezeichnung_eng, + UPPER(tbl_studiengang.typ::varchar(1) || tbl_studiengang.kurzbz) as studiengang_kuerzel, + lehre.tbl_lehrveranstaltung.lehrveranstaltung_id, + lehre.tbl_lehrveranstaltung.bezeichnung, + lehre.tbl_lehrveranstaltung.bezeichnung_english as bezeichnung_eng, + lehre.tbl_lehrveranstaltung.farbe, + lehre.tbl_lehrveranstaltung.lvinfo, + lehre.tbl_lehrveranstaltung.benotung, + lehre.tbl_lehrveranstaltung.orgform_kurzbz, + lehre.tbl_lehrveranstaltung.sprache, + lehre.tbl_lehrveranstaltung.ects, + lehre.tbl_lehrveranstaltung.incoming + FROM + lehre.tbl_lehreinheit JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id) + JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id) + JOIN public.tbl_studiengang USING(studiengang_kz) + JOIN lehre.tbl_lehrveranstaltung as lehrfach ON(tbl_lehreinheit.lehrfach_id=lehrfach.lehrveranstaltung_id) + WHERE + tbl_lehreinheit.studiensemester_kurzbz = ? + AND mitarbeiter_uid = ?) as distincted_by_lva_id + JOIN ( + SELECT lehrveranstaltung_id, TRUNC(SUM(lehre.tbl_lehreinheitmitarbeiter.semesterstunden)) as semesterstunden + FROM lehre.tbl_lehreinheit + JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id) + JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id) + WHERE tbl_lehreinheit.studiensemester_kurzbz = ? + AND mitarbeiter_uid = ? + GROUP BY lehrveranstaltung_id + ) semesterstundenAggregatedSubquery USING(lehrveranstaltung_id) + ORDER BY studiengang_kuerzel, semester, bezeichnung"; + + return $this->execReadOnlyQuery($qry, [$sem_kurzbz, $mitarbeiter_uid, $sem_kurzbz, $mitarbeiter_uid]); + } } diff --git a/application/models/organisation/Studiensemester_model.php b/application/models/organisation/Studiensemester_model.php index 5fa6ffb14..bed138b8a 100644 --- a/application/models/organisation/Studiensemester_model.php +++ b/application/models/organisation/Studiensemester_model.php @@ -242,6 +242,30 @@ class Studiensemester_model extends DB_Model return $this->loadWhere(['uid' => $student_uid, 'v.lehre' => true]); } + public function getWhereMitarbeiterHasLvs($uid) { + // first order by year with last 2 letter from right, + // then order by WS/SS inside the years + // query it asc so the ordering magic in cis4 turns it around again + $qry = "WITH unique_semesters AS ( + SELECT DISTINCT ON (studiensemester_kurzbz) + studiensemester_kurzbz, + start, + ende, + bezeichnung, + studienjahr_kurzbz + FROM lehre.tbl_lehreinheit + JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id) + JOIN public.tbl_studiensemester USING(studiensemester_kurzbz) + WHERE mitarbeiter_uid = ? + ) + SELECT * FROM unique_semesters + ORDER BY + RIGHT(studiensemester_kurzbz, 2) ASC, + LEFT(studiensemester_kurzbz, 2) ASC;"; + + return $this->execReadOnlyQuery($qry, [$uid]); + } + public function getAktAndFutureSemester() { $query = 'SELECT studiensemester_kurzbz diff --git a/public/css/components/MyLv.css b/public/css/components/MyLv.css index 267e2f45e..432591959 100644 --- a/public/css/components/MyLv.css +++ b/public/css/components/MyLv.css @@ -6,3 +6,13 @@ color: var(--fhc-myLv-disabled) !important; cursor: default; } + +/* adjustment to have bs5 dropdownmenus rendered properly over a tabulator table */ +.mylv-semester-table .tabulator-cell { + overflow: unset; +} + +.mylv-semester-table .tabulator-cell .action-col { + /*min-height: 2.5rem;*/ + align-items: flex-start; /* so wrapped rows don't stretch vertically */ +} \ No newline at end of file diff --git a/public/js/api/factory/addons.js b/public/js/api/factory/addons.js index 23fd19e39..90030d21f 100644 --- a/public/js/api/factory/addons.js +++ b/public/js/api/factory/addons.js @@ -21,5 +21,20 @@ export default { method: 'get', url: `/api/frontend/v1/LvMenu/getLvMenu/${lvid}/${studiensemester_kurzbz}` }; + }, + getMultipleLvMenu(lvas, studiensemester_kurzbz) { + // format params for backend bulk function + const lvMenuOptionList = lvas.map(lva => { + return { + lvid: lva.lehrveranstaltung_id, + studiensemester_kurzbz + } + }) + + return { + method: 'post', + url: `/api/frontend/v1/LvMenu/getMultipleLvMenu`, + params: { lvMenuOptionList } + }; } }; \ No newline at end of file diff --git a/public/js/apps/Cis/Cis.js b/public/js/apps/Cis/Cis.js index 1ab87090d..58c8f4356 100644 --- a/public/js/apps/Cis/Cis.js +++ b/public/js/apps/Cis/Cis.js @@ -5,7 +5,7 @@ import contrast from '../../directives/contrast.js'; import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js"; import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.js"; -import MylvStudent from "../../components/Cis/Mylv/Student.js"; +import Mylv from "../../components/Cis/Mylv/MyLv.js"; import Profil from "../../components/Cis/Profil/Profil.js"; import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js"; import CmsNews from "../../components/Cis/Cms/News.js"; @@ -24,6 +24,7 @@ import Benotungstool from "../../components/Cis/Benotungstool/Benotungstool.js"; import ApiRouteInfo from '../../api/factory/routeinfo.js'; import {capitalize} from "../../helpers/StringHelpers.js"; +import ApiAuthinfo from "../../api/factory/authinfo.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; @@ -165,8 +166,8 @@ const router = VueRouter.createRouter({ { path: `/Cis/MyLv/:studiensemester?`, name: 'MyLv', - component: MylvStudent, - props: true + component: Mylv, + props: true, }, { path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`, @@ -266,6 +267,9 @@ const app = Vue.createApp({ name: 'CisApp', data: () => ({ appSideMenuEntries: {}, + uid: '', + isStudent: null, + isMitarbeiter: null }), components: {}, computed: { @@ -279,6 +283,9 @@ const app = Vue.createApp({ return { // provide injectable & watchable language property language: Vue.computed(() => this.$p.user_language), isMobile: this.isMobile, + uid: Vue.computed(() => this.uid), + isStudent: Vue.computed(() => this.isStudent), + isMitarbeiter: Vue.computed(() => this.isMitarbeiter) } }, methods: { @@ -316,6 +323,15 @@ const app = Vue.createApp({ } } }, + async created(){ + await this.$api + .call(ApiAuthinfo.getAuthInfo()) + .then(res => { + this.uid = res.data.uid; + this.isMitarbeiter = res.data.isMitarbeiter; + this.isStudent = res.data.isStudent; + }); + }, mounted() { document.addEventListener('click', this.handleClick); diff --git a/public/js/components/Cis/Mylv/LvMenu.js b/public/js/components/Cis/Mylv/LvMenu.js index ef820ebb0..101f0be0f 100644 --- a/public/js/components/Cis/Mylv/LvMenu.js +++ b/public/js/components/Cis/Mylv/LvMenu.js @@ -1,5 +1,6 @@ export default { + name: 'LvMenu', props:{ menu:{ type:Array, diff --git a/public/js/components/Cis/Mylv/LvUebersicht.js b/public/js/components/Cis/Mylv/LvUebersicht.js index df79275d0..c85519943 100644 --- a/public/js/components/Cis/Mylv/LvUebersicht.js +++ b/public/js/components/Cis/Mylv/LvUebersicht.js @@ -4,7 +4,7 @@ import LvMenu from "./LvMenu.js"; import ApiAddons from '../../../api/factory/addons.js'; export default { - + name: 'LvUebersicht', props:{ event:{ type:Object, diff --git a/public/js/components/Cis/Mylv/Student.js b/public/js/components/Cis/Mylv/MyLv.js similarity index 71% rename from public/js/components/Cis/Mylv/Student.js rename to public/js/components/Cis/Mylv/MyLv.js index 4cc45b85f..8256d87eb 100644 --- a/public/js/components/Cis/Mylv/Student.js +++ b/public/js/components/Cis/Mylv/MyLv.js @@ -1,25 +1,37 @@ -import MylvSemester from "./Semester.js"; -import Phrasen from "../../../mixins/Phrasen.js"; +import MylvSemesterCards from "./Semester.js"; +import MylvTable from "./Table.js"; +import ApiAddons from "../../../api/factory/addons.js" // TODO(chris): phrase: global/studiensemester_auswaehlen // TODO(chris): phrase: next & prev +aria-label export default { + name: 'MyLv', components: { - MylvSemester + MylvSemesterCards, + MylvTable }, - mixins: [ - Phrasen - ], data: () => { return { firstLoad: true, studiensemester: null, lvs: {}, - currentSemester: null + currentSemester: null, + mode: localStorage.getItem('myLvaDefaultMode') ?? 'cards' }; }, + provide() { + return { + type: Vue.computed(() => this.type), + } + }, + inject: ['isStudent', 'isMitarbeiter'], computed: { + type() { + if(this.isStudent) return 'student' + if(this.isMitarbeiter) return 'employee' + return null + }, ready() { return this.studiensemester !== null && (!this.firstLoad || this.current.lvs !== null); }, @@ -34,7 +46,22 @@ export default { axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Lvs/' + this.currentSemester).then(res => { this.lvs[this.currentSemester].lvs = res.data.retval || []; this.firstLoad = false; - }); + + this.lvs[this.currentSemester].lvs.forEach(lv=>{ + + this.$api.call(ApiAddons.getLvMenu(lv.lehrveranstaltung_id, this.currentSemester)).then(res => { + if(res.data) { + + const lvProp = this.lvs[this.currentSemester].lvs.find(lv2 => lv2.lehrveranstaltung_id == lv.lehrveranstaltung_id) + lvProp.menu = res.data + + } + }) + + }) + + + }) } return this.lvs[this.currentSemester]; }, @@ -67,6 +94,10 @@ export default { } }, methods: { + clickMode(evt, mode) { + localStorage.setItem('myLvaDefaultMode', mode) + this.mode = mode + }, prevSem() { this.$refs.studiensemester.selectedIndex--; this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true })); @@ -99,7 +130,7 @@ export default {

{{$p.t('lehre/myLV')}}


-
+
@@ -117,13 +148,34 @@ export default {
+
+
+ + +
+
- + +
-
+
` }; \ No newline at end of file diff --git a/public/js/components/Cis/Mylv/Semester.js b/public/js/components/Cis/Mylv/Semester.js index 90dab5a32..f774be506 100644 --- a/public/js/components/Cis/Mylv/Semester.js +++ b/public/js/components/Cis/Mylv/Semester.js @@ -1,6 +1,7 @@ import MylvSemesterStudiengang from "./Semester/Studiengang.js"; export default { + name: 'Semester', components: { MylvSemesterStudiengang }, @@ -43,4 +44,4 @@ export default {
` -}; \ No newline at end of file +}; diff --git a/public/js/components/Cis/Mylv/Semester/Studiengang.js b/public/js/components/Cis/Mylv/Semester/Studiengang.js index 80a969673..7ff1a0d51 100644 --- a/public/js/components/Cis/Mylv/Semester/Studiengang.js +++ b/public/js/components/Cis/Mylv/Semester/Studiengang.js @@ -3,6 +3,7 @@ import MylvSemesterStudiengangAverageGrade from "./Studiengang/AverageGrade.js"; import Phrasen from "../../../../mixins/Phrasen.js"; export default { + name: 'Studiengang', components: { MylvSemesterStudiengangLv, MylvSemesterStudiengangAverageGrade @@ -34,8 +35,8 @@ export default { return lv.benotung ? lv.znote || lv.lvnote || null : null; }, }, - template: `
- + template: ` +

{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}} {{semester}}.{{$p.t('lehre/semester')}} @@ -48,4 +49,4 @@ export default {

` -}; \ No newline at end of file +}; diff --git a/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js b/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js index a8cb2bc97..842aa21d6 100644 --- a/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js +++ b/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js @@ -1,7 +1,4 @@ import LvPruefungen from "./Lv/Pruefungen.js"; -import LvInfo from "./Lv/Info.js"; -import Phrasen from "../../../../../mixins/Phrasen.js"; -import LvUebersicht from "../../LvUebersicht.js"; import ApiLehre from '../../../../../api/factory/lehre.js'; import ApiAddons from '../../../../../api/factory/addons.js'; @@ -9,15 +6,11 @@ import ApiAddons from '../../../../../api/factory/addons.js'; // TODO(chris): L10n export default { - components:{ - LvUebersicht, - }, - mixins: [ - Phrasen - ], - inject: ['studien_semester'], + name: 'Lv', + inject: ['studien_semester', 'type'], props: { - lehrveranstaltung_id: Number, + lehrveranstaltung_id: [Number, String], + semesterstunden: [Number, String], bezeichnung: String, bezeichnung_eng: String, module: String, @@ -35,13 +28,13 @@ export default { ects: String, incoming: Number, positiv: Boolean, - note_index: String + note_index: String, + menu: [Array, String] }, data: () => { return { pruefungenData: null, info: null, - menu: null, preselectedMenuItem: null, } }, @@ -65,12 +58,6 @@ export default { emptyMenu(){ return !this.menu || !Array.isArray(this.menu) || Array.isArray(this.menu) && this.menu.length == 0; }, - bodyStyle() {return {}; - /*const bodyStyle = {}; - if (this.farbe) - bodyStyle['background-color'] = '#' + this.farbe; - return bodyStyle;*/ - }, grade() { const languageIndex = this.$p.user_language.value === 'English' ? 1 : 0 // no more showing of grade LV, if grade Zeugnis is not existing yet @@ -84,7 +71,6 @@ export default { }, }, methods: { - fetchMenu(lehrveranstaltung_id = this.lehrveranstaltung_id, studien_semester = this.studien_semester) { return this.$api .call(ApiAddons.getLvMenu(lehrveranstaltung_id, studien_semester)) @@ -96,28 +82,18 @@ export default { this.menu = []; }); }, - + c4_target(menuItem) { + if (menuItem.c4_moodle_links?.length > 0) return null; + return menuItem.c4_target ?? null; + }, c4_link(menuItem) { if (!menuItem) return null; if (Array.isArray(menuItem.c4_moodle_links) && menuItem.c4_moodle_links.length) { return '#'; - } - else { + } else { return menuItem.c4_link ?? null; } }, - openLvOption(menuItem){ - if (menuItem.id == "core_menu_mailanstudierende"){ - window.location.href = menuItem.c4_link; - } else if (menuItem.id == "core_menu_digitale_anwesenheitslisten") { - window.location.href = menuItem.c4_link; - } else{ - this.preselectedMenuItem = menuItem; - Vue.nextTick(() => { - this.$refs.lvUebersicht.show(); - }); - } - }, openPruefungen() { // early return if the pruefungenData is empty or not set if (!this.LvHasPruefungenInformation) return; @@ -126,75 +102,65 @@ export default { pruefungenData: this.pruefungenData, bezeichnung: this.bezeichnung }); - }, - openInfos() { - if (!this.info) { - this.info = true; - // TODO(chris): load all this params on ajax? - LvInfo.popup({ - lehrveranstaltung_id: this.lehrveranstaltung_id, - bezeichnung: this.bezeichnung, - bezeichnung_eng: this.bezeichnung_eng, - studiengang_kuerzel: this.studiengang_kuerzel, - semester: this.semester, - studien_semester: this.studien_semester, - orgform_kurzbz: this.orgform_kurzbz, - sprache: this.sprache, - ects: this.ects, - incoming: this.incoming - }).then(() => this.info = false).catch(() => this.info = false); - } - } - }, - watch:{ - studien_semester(newValue){ - this.fetchMenu(this.lehrveranstaltung_id, newValue); } }, created() { - this.$api - .call(ApiLehre.getStudentPruefungen(this.lehrveranstaltung_id)) - .then(res => res.data) - .then(pruefungen => { - this.pruefungenData = pruefungen; - }); + if(this.type == 'student') { + this.$api + .call(ApiLehre.getStudentPruefungen(this.lehrveranstaltung_id)) + .then(res => res.data) + .then(pruefungen => { + this.pruefungenData = pruefungen; + }); + } }, - mounted() { - this.fetchMenu(this.lehrveranstaltung_id, this.studien_semester); - }, - template: /*html*/`
- - + template: /*html*/` +
{{ $p.t('lehre/organisationseinheit') }}:
{{$p.user_language.value === 'English' ? bezeichnung_eng : bezeichnung}}
-
+
@@ -204,7 +170,7 @@ export default {
- ` }; \ No newline at end of file diff --git a/public/js/components/Cis/Mylv/Table.js b/public/js/components/Cis/Mylv/Table.js new file mode 100644 index 000000000..18f169e8f --- /dev/null +++ b/public/js/components/Cis/Mylv/Table.js @@ -0,0 +1,345 @@ +import {CoreFilterCmpt} from "../../../components/filter/Filter.js"; + +export default { + name: 'MylvTable', + components: { + CoreFilterCmpt + }, + props: { + semester: [String], + lvs: Array, + }, + data() { + return { + phrasenPromise: null, + phrasenResolved: false, + tabulatorUuid: null, + tableBuiltResolve: null, + tableBuiltPromise: null, + mylvTableOptions: { + height: Vue.ref(400), + index: 'lehrveranstaltung_id', + layout: 'fitDataStretch', + placeholder: this.$p.t('global/noDataAvailable'), + columns: [ + {title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/studiengang'))), field: 'sg_bezeichnung', widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('global/bezeichnung'))), field: 'bezeichnung', widthGrow: 2}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/orgform'))), field: 'orgform_kurzbz', widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/kurzbz'))), field: 'studiengang_kuerzel', widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/semesterstunden'))), field: 'semesterstunden', + bottomCalc: this.semesterstundenCalc, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('global/actions'))), headerSort: false, + field: 'menu', formatter: this.actionFormatter, widthGrow: 1, tooltip: this.spoofingFunc} + ], + persistence: false, + persistenceID: "mylv_2026_04_17" + }, + mylvTableEventHandlers: [ + + ] + } + }, + computed: { + ready() { return this.lvs !== null; }, + }, + methods: { + semesterstundenCalc(values, data) { + let sum = 0 + values.forEach(val => { + sum += Number(val) + }) + return sum + }, + spoofingFunc() { + // intentionally send empty tooltip info so tabulator tooltip doesnt get rendered but hover event propagates + // to individual button tooltips + return '' + }, + c4_link(menuItem) { + if (!menuItem) return null; + if (Array.isArray(menuItem.c4_moodle_links) && menuItem.c4_moodle_links.length) { + return '#'; + } else { + return menuItem.c4_link ?? null; + } + }, + handleUuidDefined(uuid) { + this.tabulatorUuid = uuid + }, + tableResolve(resolve) { + this.tableBuiltResolve = resolve + }, + actionFormatter(cell) { + let container = document.createElement('div'); + container.className = "d-flex gap-2"; + + const data = cell.getData() + if(data.menu && data.menu.length) { + + container.className = "d-flex flex-wrap gap-2" + + data.menu.forEach((lvLink) => { + // render dropdown if we have a link and some some linklist + const hasDropdown = (lvLink.c4_moodle_links?.length || lvLink.c4_linkList?.length) && lvLink.c4_link; + + if (hasDropdown) { + // button group + const group = document.createElement('div'); + group.className = 'btn-group'; + + // main action button + const button= this.createActionButton(lvLink) + + // toggle button + const toggle = document.createElement('button'); + toggle.className = 'btn btn-sm dropdown-toggle dropdown-toggle-split border-0'; + toggle.type = 'button'; + toggle.dataset.bsToggle = 'dropdown'; // uses absolute position which gets clipped by tabulator + toggle.ariaExpanded = 'false'; + toggle.innerHTML = 'Toggle Dropdown'; + + // dropdown menu + const dropMenu = document.createElement('ul'); + dropMenu.className = 'dropdown-menu dropdown-menu p-0'; + + // moodle links have priority to be dropdown items but both can be! + const items = lvLink.c4_moodle_links?.length + ? lvLink.c4_moodle_links.map(item => ({ text: item.lehrform, href: item.url })) + : lvLink.c4_linkList.map(([text, link]) => ({ text, href: link })); + + + items.forEach(({ text, href }) => { + const li = document.createElement('li'); + const a = document.createElement('a'); + a.className = 'dropdown-item border-bottom'; + a.href = href; + a.target = '#'; + a.textContent = text; + li.appendChild(a); + dropMenu.appendChild(li); + }); + + group.appendChild(button); + group.appendChild(toggle); + group.appendChild(dropMenu); + container.appendChild(group); + + } else { + container.appendChild(this.createActionButton(lvLink)); + } + + }) + + } + + return container; + }, + createActionButton(lvLink){ + const button = document.createElement('a'); + button.className = 'fhc-body text-decoration-none text-truncate'; + if (!lvLink.c4_link) button.classList.add('unavailable'); + button.id = `${lvLink.name}_${lvLink.lehrveranstaltung_id}`; + + const icon = lvLink.c4_icon2 ?? 'fa-solid fa-pen-to-square'; + const label = lvLink.phrase ? this.$p.t(lvLink.phrase) : lvLink.name; + button.title = label; + button.innerHTML = `${label}`; + + button.addEventListener('click', (event) => { + event.preventDefault(); + const url = this.c4_link(lvLink); + if (url) { + const target = lvLink.c4_target || '_blank'; + if (target === '_blank') { + window.open(url, '_blank', 'noopener,noreferrer'); + } else { + window.location.href = url; + } + } else { + console.warn("Link is unavailable for:", lvLink.name); + } + }); + return button + }, + loadState() { + return JSON.parse(localStorage.getItem(this.mylvTableOptions.persistenceID) || "null"); + }, + saveState(table) { + // avoid storing state after first restore part happened + if(!this.stateRestored) return + const rawLayout = table.getColumnLayout(); + const state = { + columns: rawLayout.map(col => ({ + field: col.field, + visible: col.visible, + width: col.width, + })), + sort: table.getSorters().map(s => ({ + field: s.field, + dir: s.dir, + })), + filters: table.getFilters(), + headerFilters: table.getHeaderFilters() + }; + + localStorage.setItem(this.mylvTableOptions.persistenceID, JSON.stringify(state)); + }, + handleTableBuilt() { + const table = this.$refs.mylvTable.tabulator + + this.tableBuiltResolve() + + table.on("columnMoved", () => { + this.saveState(table); + }); + + table.on("columnResized", () => { + this.saveState(table); + }); + + table.on("columnVisibilityChanged", () => { + this.saveState(table); + }); + + table.on("filterChanged", () => { + this.saveState(table); + }); + + table.on("headerFilterChanged", () => { + this.saveState(table); + }); + + table.on("dataSorted", () => { + this.saveState(table); + }); + + table.on("columnSorted", () => { + this.saveState(table); + }); + + table.on("sortersChanged", () => { + this.saveState(table); + }); + + const saved = this.loadState(); + + table.on("renderComplete", () => { + if(!this.stateRestored) { + + if (saved?.columns && !this.colLayoutRestored) { + const layout = saved.columns.map(col => ({ + field: col.field, + width: col.width, + visible: col.visible, + // add more if needed, but keep it simple + })); + + table.setColumnLayout(layout); + + this.colLayoutRestored = true; + } + + if (saved?.filters && !this.filtersRestored) { + this.filtersRestored = true // instantly avoid retriggers + table.setFilter(saved.filters); + } + if (saved?.headerFilters && !this.headerFiltersRestored) { + this.headerFiltersRestored = true // instantly avoid retriggers + for (let hf of saved.headerFilters) { + table.setHeaderFilterValue(hf.field, hf.value); + } + } + + if (saved?.sort?.length && !this.sortRestored) { + this.sortRestored = true; + + setTimeout(() => { + const sortList = saved.sort.map(s => { + const col = table.columnManager.findColumn(s.field); + if (!col) { + return null; + } + return { column: col, dir: s.dir }; + }).filter(Boolean); + + table.setSort(sortList); + }, 100); + } + this.stateRestored = true + + } + + }); + }, + async setupData() { + this.$refs.mylvTable.tabulator.setData(this.lvs); + }, + async setupMounted() { + this.tableBuiltPromise = new Promise(this.tableResolve) + await this.tableBuiltPromise + + this.setupData() + + const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : '' + const tableDataSet = document.getElementById('filterTableDataset' + tableID); + if(!tableDataSet) return + const rect = tableDataSet.getBoundingClientRect(); + + const h = window.visualViewport.height - rect.top - 50 + if(this.$refs.mylvTable) { + this.$refs.mylvTable.$refs.table.style.setProperty('height', h+'px') + + // necessary so the wrapping action row resolves to the full rowHeight required + // without the redraw here actions past the initial rowHeight would be clipped off + this.$refs.mylvTable.tabulator.redraw(true) + } + + } + }, + created() { + this.phrasenPromise = this.$p.loadCategory(['global', 'lehre', 'lvinfo']) + this.phrasenPromise.then(()=> {this.phrasenResolved = true}) + }, + mounted() { + this.setupMounted() + }, + watch: { + lvs: { + async handler(newVal) { + await this.tableBuiltPromise; + if(!this.$refs.mylvTable?.tabulator) return + + this.$refs.mylvTable.tabulator.setData(newVal); + + const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : '' + const tableDataSet = document.getElementById('filterTableDataset' + tableID); + if(!tableDataSet) return + const rect = tableDataSet.getBoundingClientRect(); + + const h = window.visualViewport.height - rect.top - 50 + if(this.$refs.mylvTable) { + this.$refs.mylvTable.$refs.table.style.setProperty('height', h+'px') + } + }, + deep: true + } + }, + template: ` +
+ +
+
+ +
+ ` +}; \ No newline at end of file diff --git a/public/js/components/Cis/Raumsuche/Raumsuche.js b/public/js/components/Cis/Raumsuche/Raumsuche.js index 4790272a6..dea747780 100644 --- a/public/js/components/Cis/Raumsuche/Raumsuche.js +++ b/public/js/components/Cis/Raumsuche/Raumsuche.js @@ -68,8 +68,8 @@ export const Raumsuche = { handler: async () => { this.tableBuiltResolve() } - } - ]}; + }] + }; }, computed: { isDarkMode(){ diff --git a/public/js/components/Cis/Studium/Studium.js b/public/js/components/Cis/Studium/Studium.js index c70646608..815793f55 100644 --- a/public/js/components/Cis/Studium/Studium.js +++ b/public/js/components/Cis/Studium/Studium.js @@ -3,6 +3,7 @@ import LvUebersicht from "../Mylv/LvUebersicht.js"; import ApiCisStudium from '../../../api/factory/cis/studium.js'; export default { + name: 'Studium', data(){ return { studienSemester :[],