diff --git a/application/controllers/api/v1/system/Tag.php b/application/controllers/api/v1/system/Tag.php index 7ce375217..df3003b44 100644 --- a/application/controllers/api/v1/system/Tag.php +++ b/application/controllers/api/v1/system/Tag.php @@ -37,6 +37,7 @@ class Tag extends API_Controller if (isset($tag)) { + $this->TagModel->addJoin('public.tbl_notiz_typ', 'typ_kurzbz', 'typ_kurzbz'); $result = $this->TagModel->load($tag); $this->response($result, REST_Controller::HTTP_OK); diff --git a/application/core/Tag_Controller.php b/application/core/Tag_Controller.php index 2963c0fdf..9d2f4cf6a 100644 --- a/application/core/Tag_Controller.php +++ b/application/core/Tag_Controller.php @@ -73,6 +73,7 @@ class Tag_Controller extends FHCAPI_Controller tbl_notiz.notiz_id, tbl_notiz_typ.style, tbl_notiz_typ.automatisiert, + tbl_notiz_typ.prioritaet, tbl_notiz.erledigt as done, tbl_notiz.insertamum, tbl_notiz.updateamum, @@ -107,7 +108,8 @@ class Tag_Controller extends FHCAPI_Controller style, beschreibung, tag, - automatisiert + automatisiert, + prioritaet " ); $this->NotiztypModel->addOrder('prioritaet'); @@ -317,6 +319,7 @@ class Tag_Controller extends FHCAPI_Controller tbl_notiz.notiz_id, tbl_notiz_typ.style, tbl_notiz_typ.automatisiert, + tbl_notiz_typ.prioritaet, tbl_notiz.erledigt as done, tbl_notiz.insertamum, tbl_notiz.updateamum, @@ -336,6 +339,8 @@ class Tag_Controller extends FHCAPI_Controller $this->NotizModel->addJoin('public.tbl_notizzuordnung notizzuordnung', 'tbl_notiz.notiz_id = notizzuordnung.notiz_id'); + $this->NotizModel->addOrder('done'); + $this->NotizModel->addOrder('tbl_notiz_typ.prioritaet'); $notiz = $this->NotizModel->loadWhere(array('prestudent_id' => $prestudent_id)); diff --git a/application/libraries/stv/StudentListLib.php b/application/libraries/stv/StudentListLib.php index bafa1ca46..293d74932 100644 --- a/application/libraries/stv/StudentListLib.php +++ b/application/libraries/stv/StudentListLib.php @@ -346,7 +346,7 @@ class StudentListLib $subQueryTag = "( SELECT tag.prestudent_id, - COALESCE(json_agg(tag ORDER BY tag.done), '[]'::json) AS tags + COALESCE(json_agg(tag ORDER BY tag.done, tag.prioritaet), '[]'::json) AS tags FROM ( SELECT DISTINCT ON (n.notiz_id) n.notiz_id AS id, @@ -355,6 +355,8 @@ class StudentListLib n.text AS notiz, nt.style, n.erledigt AS done, + nt.prioritaet AS prioritaet, + nt.automatisiert AS automatisiert, nz.prestudent_id, n.start, n.ende diff --git a/application/models/education/Lehreinheit_model.php b/application/models/education/Lehreinheit_model.php index 2f955c295..808c4e443 100644 --- a/application/models/education/Lehreinheit_model.php +++ b/application/models/education/Lehreinheit_model.php @@ -719,7 +719,7 @@ EOSQL; return "tag_data_agg AS ( SELECT lehreinheit_id, - COALESCE(json_agg(tag ORDER BY done), '[]'::json) AS tags + COALESCE(json_agg(tag ORDER BY done, prioritaet), '[]'::json) AS tags FROM ( SELECT DISTINCT ON (public.tbl_notiz.notiz_id) tbl_notiz.notiz_id AS id, @@ -728,7 +728,9 @@ EOSQL; text AS notiz, style, erledigt AS done, - lehreinheit_id + lehreinheit_id, + prioritaet, + automatisiert FROM public.tbl_notizzuordnung JOIN public.tbl_notiz ON tbl_notizzuordnung.notiz_id = tbl_notiz.notiz_id JOIN public.tbl_notiz_typ ON tbl_notiz.typ = tbl_notiz_typ.typ_kurzbz diff --git a/application/views/jssnippets/tabulatorcolumns/lvverwaltung.js b/application/views/jssnippets/tabulatorcolumns/lvverwaltung.js index 60d156a8a..9b7c44212 100644 --- a/application/views/jssnippets/tabulatorcolumns/lvverwaltung.js +++ b/application/views/jssnippets/tabulatorcolumns/lvverwaltung.js @@ -5,8 +5,7 @@ return [ field: "lv_kurzbz", headerFilterFuncParams: { field: 'lv_kurzbz' }, headerFilter: true, - formatter(cell, - formatterParams) { + formatter(cell, formatterParams) { const rowData = cell.getRow().getData(); const iconKey = (rowData.lehrtyp_kurzbz || '').toLowerCase(); const lvkurzbz = (cell.getValue()).toUpperCase(); @@ -22,8 +21,7 @@ return [ return parentspan; }, - cellClick(e, - cell) { + cellClick(e, cell) { cell.getRow().treeToggle(); } }, diff --git a/application/views/jssnippets/tabulatorcolumns/stv.js b/application/views/jssnippets/tabulatorcolumns/stv.js index 54a370f5d..8270defe1 100644 --- a/application/views/jssnippets/tabulatorcolumns/stv.js +++ b/application/views/jssnippets/tabulatorcolumns/stv.js @@ -268,9 +268,9 @@ return [ }, headerFilter: "tickCross", headerFilterParams: { - "tristate": true, + tristate: true, elementAttributes: { - "value": "true" + value: "true" } }, headerFilterEmptyCheck(value) { @@ -326,9 +326,9 @@ return [ }, headerFilter: "tickCross", headerFilterParams: { - "tristate": true, + tristate: true, elementAttributes: { - "value": "true" + value: "true" } }, headerFilterEmptyCheck(value) { diff --git a/public/js/components/DetailHeader/DetailHeader.js b/public/js/components/DetailHeader/DetailHeader.js index 84e462df3..2a5987a9b 100644 --- a/public/js/components/DetailHeader/DetailHeader.js +++ b/public/js/components/DetailHeader/DetailHeader.js @@ -298,7 +298,15 @@ export default { return this.$api .call(ApiTag.getAllTagsPrestudent({prestudent_id})) .then(result => { - this.tagData = result.data; + this.tagData = result.data.sort((a, b) => { + let adone = a.done ? 1 : 0; + let bdone = b.done ? 1 : 0; + + if (adone !== bdone) { + return adone - bdone; + } + return a.prioritaet - b.prioritaet; + }); }) .catch(this.$fhcAlert.handleSystemError); }, diff --git a/public/js/components/LVVerwaltung/Setup/Table.js b/public/js/components/LVVerwaltung/Setup/Table.js index 28c7ceb87..0262b957b 100644 --- a/public/js/components/LVVerwaltung/Setup/Table.js +++ b/public/js/components/LVVerwaltung/Setup/Table.js @@ -2,8 +2,16 @@ import {CoreFilterCmpt} from "../../filter/Filter.js"; import BsModal from "../../Bootstrap/Modal.js"; import DetailsForm from "../Details/Form.js"; import CoreTag from '../../Tag/Tag.js'; -import { tagHeaderFilter } from '../../../../js/tabulator/filters/extendedHeaderFilter.js'; -import { extendedHeaderFilter } from "../../../../js/tabulator/filters/extendedHeaderFilter.js"; +import { + buildTagHeaderFilterExpression, + buildTagOptionsFromRows, + customTagFilter, + setTagHeaderFilterValue, + tagHeaderFilter, + extendedHeaderFilter, + syncTagHeaderFilterOptions, + syncSelectedTagOptionsWithHeaderFilters +} from "../../../../js/tabulator/filters/extendedHeaderFilter.js"; import { tagFormatter } from "../../../../js/tabulator/formatter/tags.js"; import { addTagInTable, deleteTagInTable, updateTagInTable } from "../../../../js/helpers/TagHelper.js"; @@ -69,7 +77,20 @@ export default { this.lv_info_default.studiensemester_kurzbz = newVal this.lv_info = false; } - } + }, + "tagFilterState.selectedOptions": { + handler() { + const selectedOptions = this.tagFilterState.selectedOptions; + const combinedFilterStatement = + buildTagHeaderFilterExpression(selectedOptions); + + setTagHeaderFilterValue( + combinedFilterStatement, + this.$refs.table.tabulator + ); + }, + deep: true, + }, }, data() { return { @@ -100,8 +121,37 @@ export default { handler: (data) => { this.getExpandedRows() } - } + }, + { + event: "dataLoaded", + handler: (data) => { + syncTagHeaderFilterOptions( + Array.isArray(data) ? data : [], + this.tagFilterState.initialOptions, + this.tagFilterState.selectedOptions, + ); + }, + }, + { + event: "dataFiltered", + handler: (filters, rows) => { + syncSelectedTagOptionsWithHeaderFilters( + filters, + this.tagFilterState.selectedOptions, + this.tagsEnabled, + ); + }, + }, + { + event: "columnWidth", + handler: (column) => { + if (column.getField() !== "tags") return; + column.getCells().forEach((cell) => { + cell.getElement().firstElementChild?.fitTags?.(); + }); + }, + }, ], formData: {}, lv_info: false, @@ -115,7 +165,20 @@ export default { raumtypalternativ: this.lehreinheitRaumtypAlternativeDefault, lehrfach_id: '' - } + }, + tagFilterState: { + initialOptions: [], + selectedOptions: [], + }, + tagFilterLabels: { + tag: "Tag", + clear: "Clear", + connectors: { + AND: "AND", + OR: "OR", + NOT: "NOT", + }, + }, } }, computed: { @@ -146,6 +209,14 @@ export default { layout: 'fitDataStretch', height: '100%', persistenceID: 'lehrveranstaltungen_2026_07_15_v1', + persistence: { + sort: true, + columns: ["width", "visible"], + filter: false, + headerFilter: false, + group: false, + page: false, + }, selectableRowsRangeMode: 'click', selectableRows: true, rowContextMenu: (component, e) => { @@ -204,11 +275,25 @@ export default { created() { Tabulator.extendModule("format", "formatters", { tagHeaderFilter, - tagFormatter: cell => tagFormatter(cell, this.$refs.tagComponent), + tagFormatter: (cell, params, onRendered) => tagFormatter(cell, this.$refs.tagComponent, onRendered), ja_nein: cell => cell.getValue() ? this.toUpperCase(this.$p.t('ui', 'ja')) : this.toUpperCase(this.$p.t('ui', 'nein')) }); + LvvColumns.foreach(col => { + if (col.headerFilter == 'input' && col.headerFilterFunc == 'tagHeaderFilter') { + col.headerFilter = customTagFilter; + col.headerFilterParams = { + listOnEmpty: true, + autocomplete: true, + sort: "asc", + initialOptions: this.tagFilterState.initialOptions, + selectedOptions: this.tagFilterState.selectedOptions, + labels: this.tagFilterLabels, + }; + col.headerSort = false; + } + }); }, mounted() { if (this.shouldAutoLoad()) @@ -319,16 +404,31 @@ export default { }, addedTag(addedTag) { - addTagInTable(addedTag, this.allRows, 'lehreinheit_id') + addTagInTable(addedTag, this.allRows, 'lehreinheit_id'); + syncTagHeaderFilterOptions( + this.$refs.table?.tabulator?.getData() || [], + this.tagFilterState.initialOptions, + this.tagFilterState.selectedOptions, + ); }, deletedTag(deletedTag) { deleteTagInTable(deletedTag, this.allRows); + syncTagHeaderFilterOptions( + this.$refs.table?.tabulator?.getData() || [], + this.tagFilterState.initialOptions, + this.tagFilterState.selectedOptions, + ); }, updatedTag(updatedTag) { updateTagInTable(updatedTag, this.allRows); + syncTagHeaderFilterOptions( + this.$refs.table?.tabulator?.getData() || [], + this.tagFilterState.initialOptions, + this.tagFilterState.selectedOptions, + ); }, async copyLehreinheit(row, art) { @@ -502,4 +602,4 @@ export default { ` -}; \ No newline at end of file +}; diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js index 899ec4ffa..095e34d3f 100644 --- a/public/js/components/Stv/Studentenverwaltung/List.js +++ b/public/js/components/Stv/Studentenverwaltung/List.js @@ -1,7 +1,14 @@ import {CoreFilterCmpt} from "../../filter/Filter.js"; import ListNew from './List/New.js'; import CoreTag from '../../Tag/Tag.js'; -import { tagHeaderFilter } from "../../../tabulator/filters/extendedHeaderFilter.js"; +import { + buildTagHeaderFilterExpression, + customTagFilter, + setTagHeaderFilterValue, + syncSelectedTagOptionsWithHeaderFilters, + syncTagHeaderFilterOptions, + tagHeaderFilter, +} from "../../../tabulator/filters/extendedHeaderFilter.js"; import { addTagInTable, deleteTagInTable, updateTagInTable } from "../../../../js/helpers/TagHelper.js"; import { tagFormatter } from "../../../../js/tabulator/formatter/tags.js"; @@ -18,568 +25,756 @@ import ModuleFilterFilters from '../../../tabulator/filter/filters.js'; Tabulator.extendModule("filter", "filters", ModuleFilterFilters); export default { - name: "ListPrestudents", - components: { - CoreFilterCmpt, - ListNew, - CoreTag, - ListFilter - }, - directives: { - draggable - }, - inject: { - lists: { - from: 'lists', - required: true - }, - $reloadList: { - from: '$reloadList', - required: true - }, - currentSemester: { - from: 'currentSemester', - required: true - }, - tagsEnabled: { - from: 'configStvTagsEnabled', - default: false - }, - }, - props: { - selected: Array, - studiengangKz: Number, - studiensemesterKurzbz: String - }, - emits: [ - 'update:selected', - 'filterActive' - ], - data() { - return { - tabulatorOptions: { - columns: StvColumns, - locale: true, - rowFormatter(row) { - if (row.getData().bnaktiv === false) { - row.getElement().classList.add('text-black','text-opacity-50','fst-italic'); - } - row.getElement().draggable = true - }, + name: "ListPrestudents", + components: { + CoreFilterCmpt, + ListNew, + CoreTag, + ListFilter + }, + directives: { + draggable + }, + inject: { + lists: { + from: 'lists', + required: true + }, + $reloadList: { + from: '$reloadList', + required: true + }, + currentSemester: { + from: 'currentSemester', + required: true + }, + tagsEnabled: { + from: 'configStvTagsEnabled', + default: false + }, + }, + props: { + selected: Array, + studiengangKz: Number, + studiensemesterKurzbz: String + }, + emits: [ + 'update:selected', + 'filterActive' + ], + data() { + return { + tagFilterState: { + initialOptions: [], + selectedOptions: [], + }, + tagFilterLabels: { + tag: "Tag", + clear: "Clear", + connectors: { + AND: "AND", + OR: "OR", + NOT: "NOT", + }, + }, + selectedColumnValues: [], + tabulatorOptions: { + columns: StvColumns, + locale: true, + rowFormatter(row) { + if (row.getData().bnaktiv === false) { + row.getElement().classList.add('text-black','text-opacity-50','fst-italic'); + } + row.getElement().draggable = true + }, - ajaxRequestFunc: (url, config, params) => { - if( url === '' ) - { - return Promise.resolve({ data: []}); - } - /** - * NOTE(chris): Because of a bug in Tabulator - * we need to get the params from elsewhere. - * @see https://github.com/olifolkerd/tabulator/issues/4318 - */ - const apiconfig = { - ...this.tabulatorOptions.ajaxConfig, - url: this.tabulatorOptions.ajaxURL, - params: this.tabulatorOptions.ajaxParams - }; - return this.$api.call(apiconfig); - }, - ajaxResponse: (url, params, response) => { - return response?.data; - }, + ajaxRequestFunc: (url, config, params) => { + if ( url === '' ) { + return Promise.resolve({ data: []}); + } + /** + * NOTE(chris): Because of a bug in Tabulator + * we need to get the params from elsewhere. + * @see https://github.com/olifolkerd/tabulator/issues/4318 + */ + const apiconfig = { + ...this.tabulatorOptions.ajaxConfig, + url: this.tabulatorOptions.ajaxURL, + params: this.tabulatorOptions.ajaxParams + }; + return this.$api.call(apiconfig); + }, + ajaxResponse: (url, params, response) => { + return response?.data; + }, - layout: 'fitDataStretch', - layoutColumnsOnNewData: false, - height: '100%', - selectableRows: true, - selectableRowsRangeMode: 'click', - index: 'prestudent_id', - persistenceID: 'stv-list-20260223_01' - }, - tabulatorEvents: [ - { - event: 'rowSelectionChanged', - handler: this.rowSelectionChanged - }, - { - event: 'dataLoading', - handler: this.handleDataLoading - }, - { - event: 'renderComplete', - handler: this.handleRenderComplete - }, - { - event: 'dataProcessed', - handler: (data) => { - this.getAllRows() - this.autoSelectRows(data) - } - }, - { - event: 'dataLoaded', - handler: data => { - if (Array.isArray(data)) { - this.count = data.length; - this.allPrestudents = data.map(item => item.prestudent_id); - } else { - this.count = 0; - this.allPrestudents = []; - } - } - }, - { - event: 'dataFiltered', - handler: (filters, rows) => this.filteredcount = rows.length - }, - { - event: 'rowClick', - handler: this.handleRowClick // TODO(chris): this should be in the filter component - }, - { - event: 'dataTreeRowExpanded', - handler: (data) => { - this.getExpandedRows() - } - }, - { - event: 'dataTreeRowCollapsed', - handler: (data) => { - this.getExpandedRows() - } - }, - { - event: 'rowMouseDown', - handler: this.handleMouseDown - } - ], - focusObj: null, // TODO(chris): this should be in the filter component - lastSelected: null, - filter: [], - count: 0, - filteredcount: 0, - selectedcount: 0, - //tags - expanded: [], - selectedColumnValues: [], - tagEndpoint: ApiTag, - currentEndpoint: null, - headerFilterActive: false, - dragSource: [], - oldScrollUrl: '', - oldScrollLeft: 0, - oldScrollTop: 0, - allPrestudents: [] - } - }, - computed: { - countsToHTML: function() { - return this.$p.t('global/ausgewaehlt') - + ': ' + (this.selectedcount || 0) + '' - + ' | ' - + this.$p.t('global/gefiltert') - + ': ' - + '' + (this.filteredcount || 0) + '' - + ' | ' - + this.$p.t('global/gesamt') - + ': ' + (this.count || 0) + ''; - }, - selectedDragObject() { - let items = this.dragSource?.length ? this.dragSource : this.selected; + layout: 'fitDataStretch', + layoutColumnsOnNewData: false, + height: '100%', + selectableRows: true, + selectableRowsRangeMode: 'click', + index: 'prestudent_id', + persistence: { + sort: true, + columns: ["width", "visible"], + filter: false, + headerFilter: false, + group: false, + page: false, + }, + persistenceID: 'stv-list-20260223_01' + }, + tabulatorEvents: [ + { + event: 'rowSelectionChanged', + handler: this.rowSelectionChanged + }, + { + event: 'dataLoading', + handler: this.handleDataLoading + }, + { + event: 'renderComplete', + handler: this.handleRenderComplete + }, + { + event: 'dataProcessed', + handler: (data) => { + this.getAllRows() + this.autoSelectRows(data) + } + }, + { + event: 'dataLoaded', + handler: data => { + if (Array.isArray(data)) { + this.count = data.length; + this.allPrestudents = data.map(item => item.prestudent_id); + } else { + this.count = 0; + this.allPrestudents = []; + } + syncTagHeaderFilterOptions( + Array.isArray(data) ? data : [], + this.tagFilterState.initialOptions, + this.tagFilterState.selectedOptions, + ); + } + }, + { + event: 'dataFiltered', + handler: (filters, rows) => { + this.filteredcount = rows.length; + syncSelectedTagOptionsWithHeaderFilters( + filters, + this.tagFilterState.selectedOptions, + this.tagsEnabled, + ); + }, + }, + { + event: 'rowClick', + handler: this.handleRowClick // TODO(chris): this should be in the filter component + }, + { + event: 'dataTreeRowExpanded', + handler: (data) => { + this.getExpandedRows() + } + }, + { + event: 'dataTreeRowCollapsed', + handler: (data) => { + this.getExpandedRows() + } + }, + { + event: 'rowMouseDown', + handler: this.handleMouseDown + }, + { + event: "columnWidth", + handler: (column) => { + if (column.getField() !== "tags") return; - return items.map(item => { - let type, id; - if (item.uid) { - type = 'student'; - id = item.uid; - } else if (item.prestudent_id) { - type = 'prestudent'; - id = item.prestudent_id; - } else if (item.person_id) { - type = 'person'; - id = item.person_id; - } - return { - ...item, - type, - id - }; - }); - }, - downloadConfig() { - return { - csv: { - formatter: 'csv', - file: this.fileString, - options: { - delimiter: ';', - bom: true, - }, - rowRange: this.selectedcount > 0 ? "selected" : "all", - }, - }; - }, - fileString() { - let today = new Date().toLocaleDateString('en-GB') - .replace(/\//g, '_'); - return "StudentList_" + today + ".csv"; - }, - selectedPrestudents() { - if (this.selected && this.selected.length > 0) { - return this.selected.map(item => item.prestudent_id); - } else { - // fallback whole list of prestudents - return this.allPrestudents || []; - } - }, + column.getCells().forEach((cell) => { + cell.getElement().firstElementChild?.fitTags?.(); + }); + }, + }, + ], + focusObj: null, // TODO(chris): this should be in the filter component + lastSelected: null, + filter: [], + count: 0, + filteredcount: 0, + selectedcount: 0, + //tags + expanded: [], + tagEndpoint: ApiTag, + currentEndpoint: null, + headerFilterActive: false, + dragSource: [], + oldScrollUrl: '', + oldScrollLeft: 0, + oldScrollTop: 0, + allPrestudents: [] + } + }, + computed: { + countsToHTML: function() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcount || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcount || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.count || 0) + ''; + }, + selectedDragObject() { + let items = this.dragSource?.length ? this.dragSource : this.selected; - linkXLS(){ - return FHC_JS_DATA_STORAGE_OBJECT.app_root - + 'content/statistik/studentenexportextended.xls.php?' - + '&studiensemester_kurzbz=' + this.currentSemester - + '&data=' + this.selectedPrestudents.join(";"); - }, - }, - created() { - if(this.tagsEnabled) { - const coltags = { - title: 'Tags', - field: 'tags', - tooltip: false, - headerFilter: "input", - headerFilterFunc: tagHeaderFilter, - headerFilterFuncParams: {field: 'tags'}, - formatter: (cell) => tagFormatter(cell, this.$refs.tagComponent), - width: 150, - }; - this.tabulatorOptions.columns.splice(2, 0, coltags); - } - }, - methods: { - reload() { - this.$refs.table.reloadTable(); - }, - actionNewPrestudent() { - this.$refs.new.open(); - }, - rowSelectionChanged(data, rows, selected, deselected) { - this.selectedcount = data.length; + return items.map(item => { + let type, id; + if (item.uid) { + type = 'student'; + id = item.uid; + } else if (item.prestudent_id) { + type = 'prestudent'; + id = item.prestudent_id; + } else if (item.person_id) { + type = 'person'; + id = item.person_id; + } + return { + ...item, + type, + id + }; + }); + }, + downloadConfig() { + return { + csv: { + formatter: 'csv', + file: this.fileString, + options: { + delimiter: ';', + bom: true, + }, + rowRange: this.selectedcount > 0 ? "selected" : "all", + }, + }; + }, + fileString() { + let today = new Date().toLocaleDateString('en-GB') + .replace(/\//g, '_'); + return "StudentList_" + today + ".csv"; + }, + selectedPrestudents() { + if (this.selected && this.selected.length > 0) { + return this.selected.map(item => item.prestudent_id); + } else { + // fallback whole list of prestudents + return this.allPrestudents || []; + } + }, - //in case of empty selection (eg. in future or past semester of selected student without sem) - if(selected.length == 0 ) { - this.lastSelected = this.selected; - } + linkXLS(){ + return FHC_JS_DATA_STORAGE_OBJECT.app_root + + 'content/statistik/studentenexportextended.xls.php?' + + '&studiensemester_kurzbz=' + this.currentSemester + + '&data=' + this.selectedPrestudents.join(";"); + }, + }, + watch: { + "$p.user_language.value"(n, o) { + if (n !== o && o !== undefined && this.$refs.table.tableBuilt) { + this.translateTabulator(); + } + }, + "tagFilterState.selectedOptions": { + handler() { + const selectedOptions = this.tagFilterState.selectedOptions; + const combinedFilterStatement = + buildTagHeaderFilterExpression(selectedOptions); - if(selected.length > 0 || deselected.length > 0){ + setTagHeaderFilterValue( + combinedFilterStatement, + this.$refs.table.tabulator, + ); + }, + deep: true, + }, + }, + methods: { + translateTabulator() { + this.$p + .loadCategory([ + "global", + "person", + "lehre", + "ui", + "profilUpdate", + "admission", + "stv", + ]) + .then(() => { + this.updateTagFilterLabels(); - //for tags - this.selectedRows = this.$refs.table.tabulator.getSelectedRows(); - this.selectedColumnValues = this.selectedRows.filter( - row => row.getData().prestudent_id !== undefined - && row.getData().prestudent_id - ).map( - row => row.getData().prestudent_id - ); + const translations = { + uid: capitalize(this.$p.t("person/uid")), + titelpre: capitalize(this.$p.t("person/titelpre")), + nachname: capitalize(this.$p.t("person/nachname")), + vorname: capitalize(this.$p.t("person/vorname")), + wahlname: capitalize(this.$p.t("person/wahlname")), + vornamen: capitalize(this.$p.t("person/vornamen")), + titelpost: capitalize(this.$p.t("person/titelpost")), + ersatzkennzeichen: capitalize( + this.$p.t("person/ersatzkennzeichen"), + ), + gebdatum: capitalize(this.$p.t("person/geburtsdatum")), + geschlecht: capitalize(this.$p.t("person/geschlecht")), + semester_berechnet: capitalize(this.$p.t("lehre/sem")), + verband: capitalize(this.$p.t("lehre/verb")), + gruppe: capitalize(this.$p.t("lehre/grp")), + studiengang: capitalize(this.$p.t("lehre/studiengang")), + studiengang_kz: capitalize(this.$p.t("lehre/studiengang_kz")), + matrikelnr: capitalize(this.$p.t("person/personenkennzeichen")), + person_id: capitalize(this.$p.t("person/person_id")), + status: capitalize(this.$p.t("global/status")), + status_datum: capitalize(this.$p.t("profilUpdate/statusDate")), + status_bestaetigung: capitalize( + this.$p.t("global/status_bestaetigung"), + ), + mail_privat: capitalize(this.$p.t("person/email_private")), + mail_intern: capitalize(this.$p.t("person/email_intern")), + anmerkungen: capitalize(this.$p.t("stv/notes_person")), + anmerkung: capitalize(this.$p.t("stv/notes_prestudent")), + orgform_kurzbz: capitalize(this.$p.t("lehre/orgform")), + aufmerksamdurch_kurzbz: capitalize( + this.$p.t("person/aufmerksamDurch"), + ), + punkte: capitalize(this.$p.t("admission/gesamtpunkte")), + aufnahmegruppe_kurzbz: capitalize( + this.$p.t("stv/aufnahmegruppe_kurzbz"), + ), + dual: capitalize(this.$p.t("lehre/dual_short")), + matr_nr: capitalize(this.$p.t("person/matrikelnummer")), + studienplan_bezeichnung: capitalize(this.$p.t("lehre/studienplan")), + prestudent_id: capitalize(this.$p.t("ui/prestudent_id")), + priorisierung_relativ: capitalize(this.$p.t("lehre/prioritaet")), + mentor: capitalize(this.$p.t("stv/mentor")), + bnaktiv: capitalize(this.$p.t("person/aktiv")), + }; - this.$emit('update:selected', data); - } - }, - autoSelectRows(data) { - if (Array.isArray(this.lastSelected) && this.lastSelected.length){ - // NOTE(chris): reselect rows on refresh - let selected = this.lastSelected.map(el => this.$refs.table.tabulator.getRow(el.prestudent_id)) - // TODO(chris): unselect current item if it's no longer in the table? - // or maybe reselect only the last one? - selected = selected.filter(el => el); + /** NOTE(chris): + * use this approach because updateDefinition + * on the Tabulator columns is way slower and + * freezes up the GUI. + */ + // Overwrite definition for column show/hide + this.$refs.table.tabulator.getColumns().forEach((col) => { + const trans = translations[col.getField()]; + if (!trans) return; + col.getDefinition().title = trans; + }); + // Overwrite node in dom + this.$refs.table.tabulator.element + .querySelectorAll(".tabulator-col[tabulator-field]") + .forEach((el) => { + const field = el.getAttribute("tabulator-field"); + if (!translations[field]) return; - if (selected.length) - this.$refs.table.tabulator.selectRow(selected); - } else if(data && this.lastSelected === undefined) { - // NOTE(chris): select row if it's the only one (preferably only on startup) - if (data.length == 1) { - this.$refs.table.tabulator.selectRow(this.$refs.table.tabulator.getRows()); - } - } - }, - updateFilter(filter) { - this.filter = filter; - this.$emit('filterActive', filter); - this.updateUrl(); - }, - updateUrl(endpoint, first) { - this.lastSelected = first ? undefined : this.selected; + const title = el.querySelector(".tabulator-col-title"); + if (!title) return; -/* console.log('function param endpoint: ' + JSON.stringify(endpoint)); - console.log('current endpoint: ' + JSON.stringify(this.currentEndpoint));*/ + title.innerText = translations[field]; + }); + }); + }, + reload() { + this.$refs.table.reloadTable(); + }, + actionNewPrestudent() { + this.$refs.new.open(); + }, + rowSelectionChanged(data, rows, selected, deselected) { + this.selectedcount = data.length; - if( endpoint === undefined && this.currentEndpoint === null) - { - endpoint = { url: '' }; - } - else if( endpoint === undefined ) - { - endpoint = JSON.parse(JSON.stringify(this.currentEndpoint)); - } - else - { - this.currentEndpoint = JSON.parse(JSON.stringify(endpoint)); - } + //in case of empty selection (eg. in future or past semester of selected student without sem) + if (selected.length == 0 ) { + this.lastSelected = this.selected; + } - endpoint.url = endpoint.url.replace( - 'CURRENT_SEMESTER', - encodeURIComponent(this.currentSemester) - ); + if (selected.length > 0 || deselected.length > 0) { + this.lastSelected = this.selected; - const params = (endpoint?.params !== undefined) ? endpoint.params : {}; - let method = (endpoint?.method !== undefined) ? endpoint.method : 'get'; - if (this.filter.length && !endpoint.url.match(/\/search\//)) - { - params.filter = this.filter; - method = 'post'; - } + //for tags + this.selectedRows = this.$refs.table.tabulator.getSelectedRows(); + this.selectedColumnValues = this.selectedRows.filter( + row => row.getData().prestudent_id !== undefined + && row.getData().prestudent_id + ).map( + row => row.getData().prestudent_id + ); - this.tabulatorOptions.ajaxURL = endpoint.url; - this.tabulatorOptions.ajaxParams = { ...params }; - this.tabulatorOptions.ajaxConfig = {method}; + this.$emit('update:selected', data); + } + }, + autoSelectRows(data) { + if (Array.isArray(this.lastSelected) && this.lastSelected.length){ + // NOTE(chris): reselect rows on refresh + let selected = this.lastSelected.map(el => this.$refs.table.tabulator.getRow(el.prestudent_id)) + // TODO(chris): unselect current item if it's no longer in the table? + // or maybe reselect only the last one? + selected = selected.filter(el => el); - if (!this.$refs.table.tableBuilt) { - if (this.$refs.table.tabulator) { - this.$refs.table.tabulator.on("tableBuilt", () => { - this.$refs.table.tabulator.setData(endpoint.url, params, method); - }); - } - } else - this.$refs.table.tabulator.setData(endpoint.url, params, method); - }, - dragCleanup(evt) { - this.dragSource = []; - if (evt.dataTransfer.dropEffect == 'none') - return; // aborted or wrong target - - this.$reloadList(); - }, - onKeydown(e) { // TODO(chris): this should be in the filter component + if (selected.length) + this.$refs.table.tabulator.selectRow(selected); + } else if(data && this.lastSelected === undefined) { + // NOTE(chris): select row if it's the only one (preferably only on startup) + if (data.length == 1) { + this.$refs.table.tabulator.selectRow(this.$refs.table.tabulator.getRows()); + } + } + }, + updateFilter(filter) { + this.filter = filter; + this.$emit('filterActive', filter); + this.updateUrl(); + }, + updateTagFilterLabels() { + this.tagFilterLabels.clear = this.$p.t("ui/filterdelete"); + this.tagFilterLabels.tag = "Tag"; + this.tagFilterLabels.connectors.AND = this.$p + .t("ui/andCondition") + .toUpperCase(); + this.tagFilterLabels.connectors.OR = this.$p + .t("ui/orCondition") + .toUpperCase(); + this.tagFilterLabels.connectors.NOT = this.$p + .t("ui/notCondition") + .toUpperCase(); + }, + updateUrl(endpoint, first) { + this.lastSelected = first ? undefined : this.selected; - if ((e.ctrlKey || e.metaKey) && e.code === "KeyA") { - e.preventDefault(); + if (endpoint === undefined && this.currentEndpoint === null) { + endpoint = { url: '' }; + } else if (endpoint === undefined) { + endpoint = JSON.parse(JSON.stringify(this.currentEndpoint)); + } else { + this.currentEndpoint = JSON.parse(JSON.stringify(endpoint)); + } - this.$refs.table.tabulator.deselectRow(); - this.$refs.table.tabulator.selectRow(); - } + endpoint.url = endpoint.url.replace( + 'CURRENT_SEMESTER', + encodeURIComponent(this.currentSemester) + ); - if (!this.focusObj) - return; + const params = (endpoint?.params !== undefined) ? endpoint.params : {}; + let method = (endpoint?.method !== undefined) ? endpoint.method : 'get'; + if (this.filter.length && !endpoint.url.match(/\/search\//)) { + params.filter = this.filter; + method = 'post'; + } - // Ignore typing inside editable elements - if ( - e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement || - e.target.isContentEditable - ) - return; + this.tabulatorOptions.ajaxURL = endpoint.url; + this.tabulatorOptions.ajaxParams = { ...params }; + this.tabulatorOptions.ajaxConfig = { method }; - var next; - switch (e.code) { - case 'Enter': - case 'Space': - e.preventDefault(); - var e2 = new Event('click', e); - e2.altKey = e.altKey; - e2.ctrlKey = e.ctrlKey; - e2.shiftKey = e.shiftKey; - this.focusObj.dispatchEvent(e2); - //row.component.toggleSelect(); - break; - case 'ArrowUp': - e.preventDefault(); - next = this.focusObj.previousElementSibling; - if (next) - this.changeFocus(this.focusObj, next); - break; - case 'ArrowDown': - e.preventDefault(); - next = this.focusObj.nextElementSibling; - if (next) - this.changeFocus(this.focusObj, next); - break; - } - }, - changeFocus(a, b) { // TODO(chris): this should be in the filter component - if (b) { - b.tabIndex = 0; - this.focusObj = b; - b.focus(); - } else { - this.focusObj = null; - } - a.tabIndex = -1; - return this.focusObj; - }, - onFocus(e) { // TODO(chris): this should be in the filter component - if (!this.focusObj) { - var container, target; - if (e.target.classList.contains('tabulator-container')) { - container = e.target; - target = container.querySelector('.tabulator-row'); - } else if (e.target.classList.contains('tabulator-row')) { - container = e.target.closest('.tabulator-container'); - target = e.target; - } - if (container && target) { - this.changeFocus(container, target); - } - } - }, - handleRowClick(e, row) { // TODO(chris): this should be in the filter component - if (this.focusObj) { - let el = row.getElement(); - if (el != this.focusObj) - this.changeFocus(this.focusObj, el); - } - }, - clearSelection(){ - this.lastSelected = []; - this.$emit('update:selected',[]); - }, - //methods tags - addedTag(addedTag) - { - addTagInTable(addedTag, this.allRows, 'prestudent_id') - }, - deletedTag(deletedTag) - { - deleteTagInTable(deletedTag, this.allRows); - }, - updatedTag(updatedTag) - { - updateTagInTable(updatedTag, this.allRows) - }, - getAllRows() { - this.allRows = this.$refs.table.tabulator.getRows(); - }, - resetFilter(){ - this.$refs.listfilter.resetFilter(); - this.$refs.table.clearFilters(); - }, - handleHeaderFilter(filterActive){ - this.headerFilterActive = filterActive; - }, - handleMouseDown(e, row) - { - let data = row.getData(); - let id = data.uid ?? data.prestudent_id ?? data.person_id; + if (!this.$refs.table.tableBuilt) { + if (this.$refs.table.tabulator) { + this.$refs.table.tabulator.on("tableBuilt", () => { + this.$refs.table.tabulator.setData(endpoint.url, params, method); + }); + } + } else + this.$refs.table.tabulator.setData(endpoint.url, params, method); + }, + dragCleanup(evt) { + this.dragSource = []; + if (evt.dataTransfer.dropEffect == 'none') + return; // aborted or wrong target + + this.$reloadList(); + }, + onKeydown(e) { // TODO(chris): this should be in the filter component - const isAlreadySelected = this.selected?.some(row => (row.uid ?? row.prestudent_id ?? row.person_id) === id); + if ((e.ctrlKey || e.metaKey) && e.code === "KeyA") { + e.preventDefault(); - this.dragSource = (isAlreadySelected && this.selected?.length) ? this.selected : [data]; - }, - handleDataLoading() { - this.oldScrollLeft = this.$refs.table.tabulator.rowManager.scrollLeft; - this.oldScrollTop = this.$refs.table.tabulator.rowManager.scrollTop; - }, - handleRenderComplete() { - const table = this.$refs.table.tabulator.element.querySelector('.tabulator-tableholder'); - if(table) { - const curAjaxUrl = this.$refs.table.tabulator.getAjaxUrl(); - if(this.oldScrollUrl === curAjaxUrl) { - table.scrollLeft = this.oldScrollLeft; - table.scrollTop = this.oldScrollTop; - } else { - this.oldScrollLeft = table.scrollLeft; - this.oldScrollTop = table.scrollTop; - } - this.oldScrollUrl = this.$refs.table.tabulator.getAjaxUrl(); - } - }, - }, - // TODO(chris): focusin, focusout, keydown and tabindex should be in the filter component - // TODO(chris): filter component column chooser has no accessibilty features - template: ` -
- {{$p.t('filter','filterActive')}} -
+ const isAlreadySelected = this.selected?.some( + row => (row.uid ?? row.prestudent_id ?? row.person_id) === id + ); - -+ {{$p.t('filter','filterActive')}} +
+ + +