diff --git a/application/config/stv.php b/application/config/stv.php index 42afc318c..d9519bd6e 100644 --- a/application/config/stv.php +++ b/application/config/stv.php @@ -113,3 +113,10 @@ $config['students_tab_order'] = [ 'finalexam', 'archive', ]; + +$config['stv_prestudent_tags'] = [ + 'tag_1' => ['readonly' => false], + 'tag_2' => ['readonly' => true], + 'tag_3' => ['readonly' => false], + 'tag_4' => ['readonly' => true] +]; diff --git a/application/controllers/api/frontend/v1/stv/Tags.php b/application/controllers/api/frontend/v1/stv/Tags.php new file mode 100644 index 000000000..397b2abbe --- /dev/null +++ b/application/controllers/api/frontend/v1/stv/Tags.php @@ -0,0 +1,80 @@ + self::BERECHTIGUNG_KURZBZ, + 'getTags' => self::BERECHTIGUNG_KURZBZ, + 'addTag' => self::BERECHTIGUNG_KURZBZ, + 'updateTag' => self::BERECHTIGUNG_KURZBZ, + 'doneTag' => self::BERECHTIGUNG_KURZBZ, + 'deleteTag' => self::BERECHTIGUNG_KURZBZ, +/* 'updateLehre' => self::BERECHTIGUNG_KURZBZ, + 'doneLehre' => self::BERECHTIGUNG_KURZBZ, + 'deleteLehre' => self::BERECHTIGUNG_KURZBZ,*/ + ]); + + $this->config->load('lvverwaltung'); + } + public function getTag($readonly_tags = null) + { + parent::getTag($this->config->item('lvverwaltung_tags')); + } + public function getTags($tags = null) + { + parent::getTags($this->config->item('lvverwaltung_tags')); + } + public function addTag($withZuordnung = true, $updatable_tags = null) + { + parent::addTag(true, $this->config->item('lvverwaltung_tags')); + } + public function updateTag($updatable_tags = null) + { + parent::updateTag($this->config->item('lvverwaltung_tags')); + } + public function deleteTag($withZuordnung = true, $updatable_tags = null) + { + parent::deleteTag(true, $this->config->item('lvverwaltung_tags')); + } + public function doneTag($updatable_tags = null) + { + parent::doneTag($this->config->item('lvverwaltung_tags')); + } + + /*$this->config->load('stv'); + } + + public function getTag($readonly_tags = null) + { + // console.log("in this endpoint. getTags"); + parent::getTag($this->config->item('stv_prestudent_tags')); + } + public function getTags($tags = null) + { + // $this->terminateWithError(" IN TAGS.PHP ", self::ERROR_TYPE_GENERAL); + parent::getTags($this->config->item('stv_prestudent_tags')); + } + public function addTag($withZuordnung = true, $updatable_tags = null) + { + parent::addTag(true, $this->config->item('stv_prestudent_tags')); + } + public function updateTag($updatable_tags = null) + { + parent::updateTag($this->config->item('stv_prestudent_tags')); + } + public function deleteTag($withZuordnung = true, $updatable_tags = null) + { + parent::deleteTag(true, $this->config->item('stv_prestudent_tags')); + } + public function doneTag($updatable_tags = null) + { + parent::doneTag($this->config->item('stv_prestudent_tags')); + }*/ +} diff --git a/application/views/Studentenverwaltung.php b/application/views/Studentenverwaltung.php index 01e611657..3c4938c0d 100644 --- a/application/views/Studentenverwaltung.php +++ b/application/views/Studentenverwaltung.php @@ -14,12 +14,14 @@ 'ui', 'notiz', ), + 'tags' => true, 'customCSSs' => [ #datepicker fuer component functions 'public/css/components/vue-datepicker.css', 'public/css/components/primevue.css', 'public/css/Studentenverwaltung.css', - 'public/css/components/function.css' + 'public/css/components/function.css', + //'public/css/Lvverwaltung.css' //css tags? ], 'customJSs' => [ 'vendor/vuejs/vuedatepicker_js/vue-datepicker.iife.js' diff --git a/public/js/api/factory/stv/tag.js b/public/js/api/factory/stv/tag.js new file mode 100644 index 000000000..29675f6d4 --- /dev/null +++ b/public/js/api/factory/stv/tag.js @@ -0,0 +1,54 @@ +export default { + + getTag(data) + { + return { + method: 'get', + url: 'api/frontend/v1/stv/Tags/getTag', + params: data + }; + }, + + getTags(data) + { + return { + method: 'get', + url: 'api/frontend/v1/stv/Tags/getTags' + }; + }, + + addTag(data) + { + return { + method: 'post', + url: 'api/frontend/v1/stv/Tags/addTag', + params: data + }; + }, + + updateTag(data) + { + return { + method: 'post', + url: 'api/frontend/v1/stv/Tags/updateTag', + params: data + }; + }, + doneTag(data) + { + return { + method: 'post', + url: 'api/frontend/v1/stv/Tags/doneTag', + params: data + }; + }, + + deleteTag(data) + { + return { + method: 'post', + url: 'api/frontend/v1/stv/Tags/deleteTag', + params: data + }; + }, +}; \ 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 67cada523..3d46dc32f 100644 --- a/public/js/components/Stv/Studentenverwaltung/List.js +++ b/public/js/components/Stv/Studentenverwaltung/List.js @@ -1,12 +1,17 @@ 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 { extendedHeaderFilter } from "../../../tabulator/filters/extendedHeaderFilter.js"; +import ApiTag from "../../../api/factory/stv/tag.js"; export default { name: "ListPrestudents", components: { CoreFilterCmpt, - ListNew + ListNew, + CoreTag }, inject: { 'lists': { @@ -45,6 +50,81 @@ export default { columns:[ {title:"UID", field:"uid", headerFilter: true}, {title:"TitelPre", field:"titelpre", headerFilter: "list", headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"}}, + { + title: 'Tags', + field: 'tags', + tooltip: false, + headerFilter: "input", + headerFilterFunc: tagHeaderFilter, + headerFilterFuncParams: {field: 'tags'}, + formatter: (cell) => { + let tags = cell.getValue(); + if (!tags) return; + + let container = document.createElement('div'); + container.className = "d-flex gap-1"; + + let parsedTags = JSON.parse(tags); + let maxVisibleTags = 2; + + const rowData = cell.getRow().getData(); + if (rowData._tagExpanded === undefined) { + rowData._tagExpanded = false; + } + + const renderTags = () => { + container.innerHTML = ''; + parsedTags = parsedTags.filter(item => item !== null); + + parsedTags.sort((a, b) => { + let adone = a.done ? 1 : 0; + let bbone = b.done ? 1 : 0; + + if (adone !== bbone) + { + return adone - bbone; + } + return b.id - a.id; + }); + const tagsToShow = rowData._tagExpanded ? parsedTags : parsedTags.slice(0, maxVisibleTags); + + tagsToShow.forEach(tag => { + if (!tag) return; + let tagElement = document.createElement('span'); + tagElement.innerText = tag.beschreibung; + tagElement.title = tag.notiz; + tagElement.className = "tag " + tag.style; + if (tag.done) tagElement.className += " tag_done"; + + tagElement.addEventListener('click', (event) => { + event.stopPropagation(); + event.preventDefault(); + this.$refs.tagComponent.editTag(tag.id); + }); + + container.appendChild(tagElement); + }); + + if (parsedTags.length > maxVisibleTags) { + let toggle = document.createElement('button'); + toggle.innerText = (rowData._tagExpanded ? '- ' : '+ ') + (parsedTags.length - maxVisibleTags); + toggle.className = "display_all"; + toggle.title = rowData._tagExpanded ? "Tags ausblenden" : "Tags einblenden"; + + toggle.addEventListener('click', () => { + rowData._tagExpanded = !rowData._tagExpanded; + renderTags(); + }); + + container.appendChild(toggle); + } + }; + + renderTags(); + return container; + }, + width: 150, + }, {title:"Nachname", field:"nachname", headerFilter: true}, {title:"Vorname", field:"vorname", headerFilter: true}, {title:"Wahlname", field:"wahlname", visible:false, headerFilter: true}, @@ -131,7 +211,7 @@ export default { selectable: true, selectableRangeMode: 'click', index: 'prestudent_id', - persistenceID: 'stv-list' + persistenceID: 'stv-list', }, tabulatorEvents: [ { @@ -140,7 +220,11 @@ export default { }, { event: 'dataProcessed', - handler: this.autoSelectRows + //handler: this.autoSelectRows TODO(Manu) combine + handler: (data) => { + this.reexpandRows() + this.$emit('update:selected', {}) + } }, { event: 'dataLoaded', @@ -153,6 +237,18 @@ export default { { 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() + } } ], focusObj: null, // TODO(chris): this should be in the filter component @@ -162,7 +258,11 @@ export default { count: 0, filteredcount: 0, selectedcount: 0, - currentEndpointRawUrl: '' + currentEndpointRawUrl: '', + //tags + expanded: [], + selectedColumnValues: [], + tagEndpoint: ApiTag } }, methods: { @@ -175,6 +275,11 @@ export default { rowSelectionChanged(data) { this.selectedcount = data.length; this.lastSelected = this.selected; + + //for tags + this.selectedRows = this.$refs.table.tabulator.getSelectedRows(); + this.selectedColumnValues = this.selectedRows.filter(row => row.getData().uid !== undefined && row.getData().uid).map(row => row.getData().uid); + this.$emit('update:selected', data); }, autoSelectRows(data) { @@ -294,7 +399,194 @@ export default { if (el != this.focusObj) this.changeFocus(this.focusObj, el); } - } + }, + //methods tags + addedTag(addedTag) + { + console.log("addedTag"); + const table = this.$refs.table.tabulator; + + this.selectedRows.forEach(row => + { + if (Array.isArray(addedTag.response)) + { + addedTag.response.forEach(tag => { + const targetRow = this.allRows.find(row => row.getData().uid === tag.uid); + if (targetRow) + { + const rowData = targetRow.getData(); + let tags = []; + try { + tags = JSON.parse(rowData.tags || '[]'); + } catch (e) {} + + const tagExists = tags.some((t) => t.id === tag.id); + if (!tagExists) + { + addedTag.id = tag.id; + tags.unshift({ ...addedTag }); + targetRow.update({ tags: JSON.stringify(tags) }); + targetRow.reformat(); + } + } + }); + } + }); + }, + deletedTag(deletedTag) { + console.log("deletedTag"); + const targetRow = this.allRows.find(row => { + const rowData = row.getData(); + + let tags = []; + try { + tags = JSON.parse(rowData.tags || '[]'); + } catch (e) {} + + return tags.some(tag => tag.id === deletedTag); + }); + + if (targetRow) { + const rowData = targetRow.getData(); + let tags = []; + + try { + tags = JSON.parse(rowData.tags || '[]'); + } catch (e) {} + + const filteredTags = tags.filter(t => t.id !== deletedTag); + const updatedTags = JSON.stringify(filteredTags); + + if (updatedTags !== rowData.tags) { + targetRow.update({ + tags: updatedTags + }); + + targetRow.reformat(); + } + } + }, + updatedTag(updatedTag) { + console.log("updatedTag"); + const targetRow = this.allRows.find(row => { + const rowData = row.getData(); + let tags = []; + + try { + tags = JSON.parse(rowData.tags || '[]'); + } catch (e) {} + + return tags.some(t => t?.id === updatedTag.id); + }); + + if (targetRow) + { + const rowData = targetRow.getData(); + let tags = []; + try { + tags = JSON.parse(rowData.tags || '[]'); + } catch (e) {} + + let changed = false; + + const tagIndex = tags.findIndex(tag => tag?.id === updatedTag.id); + if (tagIndex !== -1) { + tags[tagIndex] = { ...updatedTag }; + changed = true; + } + + if (changed) + { + targetRow.update({ + tags: JSON.stringify(tags), + }); + targetRow.reformat(); + } + } + }, + resetTree() { + console.log("reset tree"); + this.allRows.forEach(row => { + row._row.modules.dataTree.open = false; + }); + + let rootRows = this.$refs.table.tabulator.getRows(true); + var lastRow = rootRows[rootRows.length - 1]; + lastRow?.treeCollapse(true) + + this.currentTreeLevel = 0; + }, + expandTree() + { + console.log("expandTree"); + this.currentTreeLevel = (this.currentTreeLevel || 0) + 1; + + let lastMatchingRow = null; + + this.allRows.forEach(row => { + const level = row._row.modules.dataTree?.index ?? 0; + + if (level === this.currentTreeLevel - 1 ) + { + row._row.modules.dataTree.open = true; + + if (row._row.data._children?.length > 0) + { + lastMatchingRow = row; + } + } + }); + + if (lastMatchingRow) + { + lastMatchingRow.treeExpand(); + } + this.$refs.table.tabulator.redraw(); + }, + getAllRows(rows) + { + let result = []; + rows.forEach(row => + { + result.push(row); + let children = row.getTreeChildren(); + if(children && children.length > 0) + { + result = result.concat(this.getAllRows(children)); + } + }); + return result; + }, + async getExpandedRows() { + this.expanded = []; + + this.allRows.forEach(row => { + if (row.getTreeChildren().length > 0 && row.isTreeExpanded()) + { + this.expanded.push(row.getData().uniqueindex); + } + }); + }, + reexpandRows() { + this.allRows = this.getAllRows(this.$refs.table.tabulator.getRows()); + + const matchingRows = this.allRows.filter(row => + this.expanded.includes(row.getData().uniqueindex) + ); + + if (matchingRows.length === 0) + this.currentTreeLevel = 0; + + matchingRows.forEach((row, index) => { + row._row.modules.dataTree.open = true; + + if (index === matchingRows.length - 1) + { + row.treeExpand(); + } + }); + }, + }, computed: { countsToHTML: function() { @@ -313,6 +605,7 @@ export default { // TODO(chris): filter component column chooser has no accessibilty features template: `
+ test manu {{selectedColumnValues}}
+ + +