diff --git a/application/controllers/api/frontend/v1/LvMenu.php b/application/controllers/api/frontend/v1/LvMenu.php index 6bad00e1e..27cbba1c2 100644 --- a/application/controllers/api/frontend/v1/LvMenu.php +++ b/application/controllers/api/frontend/v1/LvMenu.php @@ -62,6 +62,10 @@ 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 = $this->input->post('lvMenuOptionList', true); diff --git a/public/css/components/MyLv.css b/public/css/components/MyLv.css index 1305cfc36..432591959 100644 --- a/public/css/components/MyLv.css +++ b/public/css/components/MyLv.css @@ -10,4 +10,9 @@ /* 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/components/Cis/Mylv/MyLv.js b/public/js/components/Cis/Mylv/MyLv.js index b87bcd749..8256d87eb 100644 --- a/public/js/components/Cis/Mylv/MyLv.js +++ b/public/js/components/Cis/Mylv/MyLv.js @@ -17,7 +17,7 @@ export default { studiensemester: null, lvs: {}, currentSemester: null, - mode: 'table' // TODO: load from local storage + mode: localStorage.getItem('myLvaDefaultMode') ?? 'cards' }; }, provide() { @@ -46,20 +46,7 @@ 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; - - // pretty slow to load all at once if one lva has a weird moodle / lvinfo situation - // that multiplies the loadtime for everything by a factor of 3-5 - // this.$api.call(ApiAddons.getMultipleLvMenu(this.lvs[this.currentSemester].lvs, this.currentSemester)).then(res => { - // if(res.data) { - // Object.entries(res.data).forEach((entry) => { - // // entry[0] -> key -> lvid - // // entry[1] -> value -> menu - // const lv = this.lvs[this.currentSemester].lvs.find(lv => lv.lehrveranstaltung_id == entry[0]) - // lv.menu = entry[1] - // }) - // } - // }) this.lvs[this.currentSemester].lvs.forEach(lv=>{ this.$api.call(ApiAddons.getLvMenu(lv.lehrveranstaltung_id, this.currentSemester)).then(res => { @@ -108,6 +95,7 @@ export default { }, methods: { clickMode(evt, mode) { + localStorage.setItem('myLvaDefaultMode', mode) this.mode = mode }, prevSem() { diff --git a/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js b/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js index c1afd65d0..c00eeced1 100644 --- a/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js +++ b/public/js/components/Cis/Mylv/Semester/Studiengang/Lv.js @@ -28,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, } }, @@ -104,11 +104,6 @@ export default { }); } }, - watch:{ - studien_semester(newValue){ - this.fetchMenu(this.lehrveranstaltung_id, newValue); - } - }, created() { if(this.type == 'student') { this.$api @@ -119,9 +114,6 @@ export default { }); } }, - mounted() { - this.fetchMenu(this.lehrveranstaltung_id, this.studien_semester); - }, template: /*html*/`
diff --git a/public/js/components/Cis/Mylv/Table.js b/public/js/components/Cis/Mylv/Table.js index 6f0232c64..18f169e8f 100644 --- a/public/js/components/Cis/Mylv/Table.js +++ b/public/js/components/Cis/Mylv/Table.js @@ -13,40 +13,34 @@ export default { return { phrasenPromise: null, phrasenResolved: false, - tabulatorUuid: Vue.ref(0), + tabulatorUuid: null, tableBuiltResolve: null, tableBuiltPromise: null, mylvTableOptions: { height: Vue.ref(400), index: 'lehrveranstaltung_id', - layout: 'fitData', + layout: 'fitDataStretch', placeholder: this.$p.t('global/noDataAvailable'), columns: [ - {title: Vue.computed(() => this.$p.t('lehre/studiengang')), field: 'sg_bezeichnung', widthGrow: 1}, - {title: Vue.computed(() => this.$p.t('global/bezeichnung')), field: 'bezeichnung', widthGrow: 2}, - {title: Vue.computed(() => this.$p.t('lehre/orgform')), field: 'orgform_kurzbz', widthGrow: 1}, - {title: Vue.computed(() => this.$p.t('lehre/kurzbz')), field: 'studiengang_kuerzel', widthGrow: 1}, - {title: Vue.computed(() => this.$p.t('lehre/semesterstunden')), field: 'semesterstunden', - bottomCalc: this.semesterstundenCalc, widthGrow: 1, visible: true}, - {title: Vue.computed(() => this.$p.t('global/actions')), headerSort: false, + {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: [ - { - event: "tableBuilt", - handler: async () => { - this.tableBuiltResolve() - } - } + ] } }, computed: { ready() { return this.lvs !== null; }, - }, methods: { semesterstundenCalc(values, data) { @@ -61,10 +55,6 @@ export default { // to individual button tooltips return '' }, - 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) { @@ -84,14 +74,10 @@ export default { container.className = "d-flex gap-2"; const data = cell.getData() - console.log(data) if(data.menu && data.menu.length) { - - const calculatedMinWidth = data.menu.length * 120; - container.style.minWidth = `${calculatedMinWidth}px`; - const abbreviate = (str, limit = 12) => - str.length > limit + 3 ? `${str.slice(0, limit)}...` : str; + 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; @@ -102,30 +88,7 @@ export default { group.className = 'btn-group'; // main action button - 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; - - 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 = `${abbreviate(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); - } - }); + const button= this.createActionButton(lvLink) // toggle button const toggle = document.createElement('button'); @@ -143,10 +106,7 @@ export default { 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 })); - - for(let i = 0; i < 10; i++) { - items.push({text: 'puffer', href: 'www.google.com'}) - } + items.forEach(({ text, href }) => { const li = document.createElement('li'); @@ -165,33 +125,7 @@ export default { container.appendChild(group); } else { - // action button only - 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; - - 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 = `${abbreviate(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); - } - }); - - container.appendChild(button); + container.appendChild(this.createActionButton(lvLink)); } }) @@ -200,15 +134,150 @@ export default { 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() { - // console.log('mounted pre table promise') this.tableBuiltPromise = new Promise(this.tableResolve) await this.tableBuiltPromise - - console.log('mounted post table promise') + this.setupData() const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : '' @@ -216,9 +285,13 @@ export default { if(!tableDataSet) return const rect = tableDataSet.getBoundingClientRect(); - const h = window.visualViewport.height - rect.top - 100 + 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) } } @@ -232,21 +305,20 @@ export default { }, watch: { lvs: { - handler(newVal, oldVal) { - console.log('watcher') + 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) { - console.log('watcher inside if ref table clause') - 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 - 100 - if(this.$refs.mylvTable) { - this.$refs.mylvTable.$refs.table.style.setProperty('height', h+'px') - } + this.$refs.mylvTable.$refs.table.style.setProperty('height', h+'px') } }, deep: true @@ -261,8 +333,13 @@ export default { ref="mylvTable" :tabulator-options="mylvTableOptions" :tabulator-events="mylvTableEventHandlers" + @tableBuilt="handleTableBuilt" tableOnly :sideMenu="false" /> -
` +
+
+ +
+ ` }; \ No newline at end of file