diff --git a/application/models/education/Note_model.php b/application/models/education/Note_model.php index 87a1501e0..ea1ccb663 100644 --- a/application/models/education/Note_model.php +++ b/application/models/education/Note_model.php @@ -15,7 +15,11 @@ class Note_model extends DB_Model 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); } diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 3d8fc1d79..25c231afe 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -54,6 +54,7 @@ export const AbgabetoolAssistenz = { qgate2FilterSelected: [], pa_noteFilterSelected: [], noteFilterSelected: [], + note_bezFilterSelected: [], count: 0, filteredcount: 0, selectedcount: 0, @@ -211,7 +212,7 @@ export const AbgabetoolAssistenz = { {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 150, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, sorter: this.notenSorter, visible: false, minWidth: 200, formatter: this.centeredTextFormatter}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: this.notenHeaderFilterEditor, headerFilterFunc: this.notenHeaderFilterFunc, headerFilterParams: {}, headerFilterFuncParams: { noteField: 'note' }, sorter: this.notenSorter, visible: false, minWidth: 200, formatter: this.centeredTextFormatter}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, visible: false, formatter: this.centeredTextFormatter, minWidth: 100}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, @@ -536,8 +537,6 @@ export const AbgabetoolAssistenz = { return aData.note - bData.note }, notenHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) { - if (!this.notenOptions) return; - const field = cell.getField(); const stateKey = field + 'FilterSelected'; let selected = [...(this[stateKey] || [])]; @@ -554,21 +553,16 @@ export const AbgabetoolAssistenz = { dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; - // mapping evaluated at render time, not at column definition time - const fieldOptionsMap = { - 'pa_note': this.notenOptions, - 'note': this.allowedNotenOptions, - }; - const options = fieldOptionsMap[cell.getField()] ?? this.notenOptions; - if (!options) return; - + // the table is built before getNoten resolves, so resolve the options on open + const getOptions = () => (field === 'note' ? this.allowedNotenOptions : this.notenOptions) ?? []; + const updateDisplay = () => { - display.value = options + display.value = getOptions() .filter(o => selected.includes(o.note)) .map(o => o.bezeichnung) .join(', '); }; - options.forEach(opt => { + const buildDropdown = () => dropdown.replaceChildren(...getOptions().map(opt => { const row = document.createElement('label'); row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;'; row.addEventListener('mousedown', e => e.preventDefault()); @@ -592,13 +586,15 @@ export const AbgabetoolAssistenz = { row.appendChild(cb); row.appendChild(labelText); - dropdown.appendChild(row); - }); + return row; + })); updateDisplay(); display.addEventListener('click', () => { if (dropdown.style.display === 'none') { + buildDropdown(); + updateDisplay(); const rect = display.getBoundingClientRect(); dropdown.style.top = rect.bottom + 'px'; dropdown.style.left = rect.left + 'px'; @@ -621,9 +617,11 @@ export const AbgabetoolAssistenz = { }, notenHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { - if (!filterVal || !filterVal.length) return true; - // rowVal is the raw integer note id or a note object - const noteId = typeof rowVal === 'object' ? rowVal?.note : rowVal; + if (!Array.isArray(filterVal) || !filterVal.length) return true; + // noteField: the column shows a text, the note id is in another field + const noteVal = filterParams?.noteField ? rowData[filterParams.noteField] : rowVal; + // noteVal is the raw integer note id or a note object + const noteId = typeof noteVal === 'object' ? noteVal?.note : noteVal; return filterVal.some(val => val == noteId); // loose equality: filter vals are numbers, noteId might be string }, handleFilterActiveChanged(active) { @@ -1496,6 +1494,8 @@ export const AbgabetoolAssistenz = { for (let hf of saved.headerFilters) { if (hf.field === 'qgate1Status') this.qgate1FilterSelected = hf.value || []; if (hf.field === 'qgate2Status') this.qgate2FilterSelected = hf.value || []; + // older saved states hold a text filter here + if (hf.field === 'note_bez') this.note_bezFilterSelected = Array.isArray(hf.value) ? hf.value : []; table.setHeaderFilterValue(hf.field, hf.value); } } diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index b13010cad..b4b05dbbd 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -40,6 +40,7 @@ export const AbgabetoolMitarbeiter = { selectedcount: 0, qgate1FilterSelected: [], qgate2FilterSelected: [], + note_bezFilterSelected: [], abgabetypenBetreuer: null, detailIsFullscreen: false, phrasenPromise: null, @@ -145,7 +146,7 @@ export const AbgabetoolMitarbeiter = { {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 100,visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: true}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: this.notenHeaderFilterEditor, headerFilterFunc: this.notenHeaderFilterFunc, headerFilterParams: {}, headerFilterFuncParams: { noteField: 'note' }, visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, visible: true, minWidth: 100}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, width: 500, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'betreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, @@ -270,6 +271,94 @@ export const AbgabetoolMitarbeiter = { const bData = bRow.getData() return aData.note - bData.note }, + notenHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) { + const field = cell.getField(); + const stateKey = field + 'FilterSelected'; + let selected = [...(this[stateKey] || [])]; + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'position: relative; width: 100%;'; + + const display = document.createElement('input'); + display.readOnly = true; + display.placeholder = ''; + display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;'; + + const dropdown = document.createElement('div'); + dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; + + + // the table is built before getNoten resolves, so resolve the options on open + const getOptions = () => this.notenOptions ?? []; + + const updateDisplay = () => { + display.value = getOptions() + .filter(o => selected.includes(o.note)) + .map(o => o.bezeichnung) + .join(', '); + }; + const buildDropdown = () => dropdown.replaceChildren(...getOptions().map(opt => { + const row = document.createElement('label'); + row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;'; + row.addEventListener('mousedown', e => e.preventDefault()); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.value = opt.note; + cb.checked = selected.includes(opt.note); + cb.style.cssText = 'margin: 0 6px;'; + cb.addEventListener('change', () => { + selected = cb.checked + ? [...selected, opt.note] + : selected.filter(v => v !== opt.note); + this[stateKey] = [...selected]; + updateDisplay(); + success([...selected]); + }); + + const labelText = document.createElement('span'); + labelText.textContent = opt.bezeichnung; + + row.appendChild(cb); + row.appendChild(labelText); + return row; + })); + + updateDisplay(); + + display.addEventListener('click', () => { + if (dropdown.style.display === 'none') { + buildDropdown(); + updateDisplay(); + const rect = display.getBoundingClientRect(); + dropdown.style.top = rect.bottom + 'px'; + dropdown.style.left = rect.left + 'px'; + dropdown.style.display = 'block'; + } else { + dropdown.style.display = 'none'; + } + }); + + display.addEventListener('blur', () => { + setTimeout(() => { dropdown.style.display = 'none'; }, 150); + }); + + document.body.appendChild(dropdown); + wrapper.appendChild(display); + cell.getElement().addEventListener('remove', () => dropdown.remove()); + onRendered(() => display.focus()); + + return wrapper; + }, + + notenHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { + if (!Array.isArray(filterVal) || !filterVal.length) return true; + // noteField: the column shows a text, the note id is in another field + const noteVal = filterParams?.noteField ? rowData[filterParams.noteField] : rowVal; + // noteVal is the raw integer note id or a note object + const noteId = typeof noteVal === 'object' ? noteVal?.note : noteVal; + return filterVal.some(val => val == noteId); // loose equality: filter vals are numbers, noteId might be string + }, async openBenotung(type, link) { if(type === 'new') { window.open(link, '_blank') @@ -782,6 +871,8 @@ export const AbgabetoolMitarbeiter = { if (saved?.headerFilters && !this.headerFiltersRestored) { this.headerFiltersRestored = true // instantly avoid retriggers for (let hf of saved.headerFilters) { + // older saved states hold a text filter here + if (hf.field === 'note_bez') this.note_bezFilterSelected = Array.isArray(hf.value) ? hf.value : []; table.setHeaderFilterValue(hf.field, hf.value); } }