add semester filter for tags

- in detailheader: use currentSem for manual triggering and refactor formatter
- in list: preload list of ids with start and end for tabulator formatter to enable filtering
This commit is contained in:
ma0068
2026-05-07 17:36:34 +02:00
parent ef1347c7d5
commit 3dcf72d679
7 changed files with 293 additions and 31 deletions
+35 -4
View File
@@ -15,11 +15,12 @@ class Tag_Controller extends FHCAPI_Controller
'getTag' => self::BERECHTIGUNG_KURZBZ,
'getTags' => self::BERECHTIGUNG_KURZBZ,
'addTag' => self::BERECHTIGUNG_KURZBZ,
'updateTag' => self::BERECHTIGUNG_KURZBZ,
'doneTag' => self::BERECHTIGUNG_KURZBZ,
'deleteTag' => self::BERECHTIGUNG_KURZBZ,
'getAllTags' => self::BERECHTIGUNG_KURZBZ,
'getSemDates' => self::BERECHTIGUNG_KURZBZ,
'getAllStartAndEndAutomatedTags' => self::BERECHTIGUNG_KURZBZ,
'rebuildTagsForTypeId' => self::BERECHTIGUNG_KURZBZ,
];
@@ -341,16 +342,46 @@ class Tag_Controller extends FHCAPI_Controller
{
$id = $this->input->post('id');
$typeId = $this->input->post('typeId');
$semester = $this->input->post('sem');
$result = $this->taglib->rebuildTagsForTypeId($typeId, $id, $semester);
$result = $this->taglib->rebuildTagsForTypeId($typeId, $id);
//TODO (refactor; um semester, studiengang_kz)
//$result = $this->taglib->rebuildTagsForTypeId($params);
if (isError($result))
return error ('Error occurred during updateAutomatedTags');
$this->terminateWithSuccess($result);
}
public function getSemDates()
{
$studiensemester_kurzbz = $this->input->get('semester');
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$result = $this->StudiensemesterModel->loadWhere(array('studiensemester_kurzbz' => $studiensemester_kurzbz));
if (isError($result))
return error('Error occurred during retrieving studiensemester');
$data = getData($result);
$this->terminateWithSuccess(current($data));
}
public function getAllStartAndEndAutomatedTags()
{
$this->NotizModel->addSelect('notiz_id as id');
$this->NotizModel->addSelect('start');
$this->NotizModel->addSelect('ende');
$this->NotizModel->addSelect('typ');
$result = $this->NotizModel->loadWhere(array(
'titel' => 'TAG',
'verfasser_uid' => 'sftest'
));
if (isError($result))
return error('Error occurred during retrieving intervalls automated tags');
$data = getData($result);
$this->terminateWithSuccess($data);
}
private function _setAuthUID()
{
$this->_uid = getAuthUID();
+2 -5
View File
@@ -241,21 +241,18 @@ class TagLib
/*
* main function for rebuild Tags for typeId
* */
public function rebuildTagsForTypeId($typeId, $id) //TODO aktSem of frontend
public function rebuildTagsForTypeId($typeId, $id, $studiensemester_kurzbz)
{
$automatedTagsRes = $this->_ci->NotiztypModel->loadWhere(array('automatisiert' => true, 'taglib IS NOT NULL' => null));
$automatedTags = hasData($automatedTagsRes) ? getData($automatedTagsRes) : [];
//TODO change to chosen Studiensemester in frontend
$result = $this->_ci->StudiensemesterModel->getAktOrNextSemester();
$result = $this->_ci->StudiensemesterModel->load($studiensemester_kurzbz);
if (isError($result))
return error('Error occurred during retrieving studiensemester');
if (empty($result->retval) || !isset($result->retval[0])) {
return error('No studiensemester found');
}
$studiensemester_kurzbz = $result->retval[0]->studiensemester_kurzbz ?? null;
//for checkDelete
$startSem = $result->retval[0]->start ?? null;
$endeSem = $result->retval[0]->ende ?? null;
$return = [];
+15
View File
@@ -61,6 +61,21 @@ export default {
};
},
getSemDates(studiensemester_kurzbz){
return {
method: 'get',
url: 'api/frontend/v1/stv/Tags/getSemDates',
params: studiensemester_kurzbz
};
},
getAllStartAndEndAutomatedTags(){
return {
method: 'get',
url: 'api/frontend/v1/stv/Tags/getAllStartAndEndAutomatedTags',
};
},
rebuildTagsforTypeId(data){
return {
method: 'post',
@@ -49,6 +49,10 @@ export default {
from: 'configStvTagsEnabled',
default: false
},
currentSemester: {
from: 'currentSemester',
required: true
},
},
computed: {
appRoot() {
@@ -85,6 +89,7 @@ export default {
}
if(this.tagsEnabled) {
this.getSemesterDates(this.currentSemester);
this.loadTagsAndRender(this.headerData[0].prestudent_id);
}
@@ -113,6 +118,14 @@ export default {
}
},
deep: true,
},
currentSemester: {
handler(newVal) {
if (newVal) {
this.getSemesterDates(newVal);
}
},
deep: true,
}
},
data(){
@@ -123,7 +136,8 @@ export default {
isFetchingIssues: false,
tagEndpoint: ApiTag,
tagData: null,
rebuildData: null
rebuildData: null,
semDates: {}
};
},
methods: {
@@ -219,7 +233,9 @@ export default {
prestudent_id,
this.tagData,
this.$refs.tagComponent,
'prestudent_id'
'prestudent_id',
this.semDates.start,
this.semDates.ende
);
this.$refs.tagWrapper.innerHTML = '';
@@ -248,7 +264,8 @@ export default {
rebuildPrestudentTags(){
const params = {
id : this.headerData[0].prestudent_id,
typeId: 'prestudent_id'
typeId: 'prestudent_id',
sem: this.currentSemester
};
return this.$api
@@ -259,6 +276,17 @@ export default {
this.reload();
})
.catch(this.$fhcAlert.handleSystemError);
},
getSemesterDates(semester){
const params = {
studiensemester_kurzbz: semester
};
return this.$api
.call(ApiTag.getSemDates({semester}))
.then(result => {
this.semDates = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
}
},
template: `
@@ -339,8 +367,10 @@ export default {
v-if="tagsEnabled"
@click="rebuildPrestudentTags"
class="btn btn-outline btn-light mb-1"
title="Automatische Tags neu laden">
:title="'Automatische Tags fuer ' + currentSemester + ' neu laden'"
>
<i class="fa-solid fa-refresh"></i></button>
<span>{{currentSemester}}</span>
</div>
<h6 v-if="headerData[0].unruly" class="badge" :class="'bg-unruly rounded-0'"><strong>unruly</strong></h6>
</div>
@@ -237,7 +237,9 @@ export default {
dragSource: [],
oldScrollUrl: '',
oldScrollLeft: 0,
oldScrollTop: 0
oldScrollTop: 0,
semDates: {}, //TODO(Manu) check injections
intervalMap: {}
}
},
computed: {
@@ -293,7 +295,12 @@ export default {
},
},
created: function() {
if(this.tagsEnabled) {
this.getSemesterDates(this.currentSemester);
this.loadIntervals(); //preload
const coltags = {
title: 'Tags',
field: 'tags',
@@ -301,7 +308,36 @@ export default {
headerFilter: "input",
headerFilterFunc: tagHeaderFilter,
headerFilterFuncParams: {field: 'tags'},
formatter: (cell) => tagFormatter(cell, this.$refs.tagComponent),
//formatter: (cell) => tagFormatter(cell, this.$refs.tagComponent), //prev. Version without filter
formatter: (cell) => {
const raw = cell.getValue();
const tags =
Array.isArray(raw)
? raw
: (typeof raw === 'string'
? JSON.parse(raw)
: []);
const id = tags?.[0]?.id;
const interval = id
? this.intervalMap[id]
: null;
const enrichedTags = {
tags: tags,
start: interval?.start || null,
ende: interval?.end || null,
};
return tagFormatter(
enrichedTags,
this.$refs.tagComponent,
this.semDates.start,
this.semDates.ende
);
},
width: 150,
};
this.tabulatorOptions.columns.splice(2, 0, coltags);
@@ -312,9 +348,40 @@ export default {
if (n !== o && o !== undefined && this.$refs.table.tableBuilt) {
this.translateTabulator();
}
},
currentSemester: {
handler(newVal) {
if (newVal) {
this.getSemesterDates(newVal);
}
},
deep: true,
}
},
methods: {
loadIntervals() {
return this.$api
.call(ApiTag.getAllStartAndEndAutomatedTags())
.then(result => {
const data = result.data || [];
this.intervalMap = {};
data.forEach(item => {
this.intervalMap[item.id] = item;
});
})
.catch(this.$fhcAlert.handleSystemError);
},
getSemesterDates(semester){ //TODO(check for injections)
const params = {
studiensemester_kurzbz: semester
};
return this.$api
.call(ApiTag.getSemDates({semester}))
.then(result => {
this.semDates = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
translateTabulator() {
this.$p
.loadCategory(['global', 'person', 'lehre', 'ui', 'profilUpdate', 'admission', 'stv'])
@@ -398,7 +465,6 @@ export default {
//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.$emit('update:selected', data);
},
autoSelectRows(data) {
@@ -601,6 +667,7 @@ export default {
// TODO(chris): filter component column chooser has no accessibilty features
template: `
<div class="stv-list h-100 pt-3">
test manu: currentSEM: {{semDates.start}} - {{semDates.ende}}
<div
class="tabulator-container d-flex flex-column h-100"
:class="{'has-filter': filter.length}"
+34 -5
View File
@@ -1,9 +1,12 @@
export function idTagFormatter (id, tagData, tagComponent, typeId)
export function idTagFormatter (id, tagData, tagComponent, typeId, semesterStart=null, semesterEnd=null)
{
if (!id) return;
const hasSemesterFilter = !!(semesterStart && semesterEnd);
const semStart = hasSemesterFilter ? new Date(semesterStart) : null;
const semEnd = hasSemesterFilter ? new Date(semesterEnd) : null;
const parsedTags = tagData.map(tag => ({
id: tag.notiz_id,
typ_kurzbz: tag.titel?.toLowerCase(),
@@ -12,9 +15,33 @@ export function idTagFormatter (id, tagData, tagComponent, typeId)
style: tag.style,
done: tag.done,
automatisiert: tag.automatisiert,
typeId: id
typeId: id,
validFrom: tag.start ? new Date(tag.start) : null,
validTo: tag.ende ? new Date(tag.ende) : null
}));
const isInSemester = (tag) => {
if (!hasSemesterFilter) return true;
if (!tag.validFrom && !tag.validTo) return true;
if (!tag.validFrom && !tag.validTo) return true;
if (tag.validFrom && tag.validTo) {
return tag.validFrom <= semEnd && tag.validTo >= semStart;
}
if (tag.validFrom && !tag.validTo) {
return tag.validFrom <= semEnd;
}
if (!tag.validFrom && tag.validTo) {
return tag.validTo >= semStart;
}
return false;
};
let container = document.createElement('div');
container.className = "d-flex gap-1";
@@ -23,7 +50,9 @@ export function idTagFormatter (id, tagData, tagComponent, typeId)
const renderTags = () => {
container.innerHTML = '';
let filtered = parsedTags.filter(t => t != null);
let filtered = parsedTags
.filter(t => t != null)
.filter(isInSemester);
filtered.sort((a, b) => {
let adone = a.done ? 1 : 0;
+103 -10
View File
@@ -1,28 +1,115 @@
export function tagFormatter(cell, tagComponent)
{
export function tagFormatter(
cell,
tagComponent,
semesterStart = null,
semesterEnd = null
) {
// support both call versions
// 1. previous Tabulator cell: old version
// 2. custom enriched object: with start and end for tags plus semesterDates
// for check if valid tag
const isTabulatorCell =
cell && typeof cell.getValue === 'function';
const normalized = isTabulatorCell
? {
tags: cell.getValue() || [],
start: null,
ende: null,
rowData: cell.getRow().getData(),
}
: {
tags: cell.tags || [],
start: cell.start || null,
ende: cell.ende || null,
rowData: cell.rowData || {},
};
const tags = normalized.tags || [];
if (!tags.length) {
return "";
}
const mappedData = tagComponent.tags.map(tag => ({
typ_kurzbz: tag.tag_typ_kurzbz,
automatisiert: tag.automatisiert
automatisiert: tag.automatisiert,
validFrom: normalized.start
? new Date(normalized.start)
: null,
validTo: normalized.ende
? new Date(normalized.ende)
: null
}));
const hasSemesterFilter =
!!(semesterStart && semesterEnd);
let tags = cell.getValue();
if (!tags) return;
const semStart = hasSemesterFilter ? new Date(semesterStart) : null;
const semEnd = hasSemesterFilter ? new Date(semesterEnd) : null;
const isInSemester = (tag) => {
if (!hasSemesterFilter) {
return true;
}
if (!tag.validFrom && !tag.validTo) {
return true;
}
if (tag.validFrom && tag.validTo) {
return (
tag.validFrom <= semEnd &&
tag.validTo >= semStart
);
}
if (tag.validFrom && !tag.validTo) {
return tag.validFrom <= semEnd;
}
if (!tag.validFrom && tag.validTo) {
return tag.validTo >= semStart;
}
return false;
};
// parse tags if needed
let parsedTags =
typeof tags === 'string'
? JSON.parse(tags)
: tags;
let container = document.createElement('div');
container.className = "d-flex gap-1";
let parsedTags = JSON.parse(tags);
let maxVisibleTags = 2;
const rowData = cell.getRow().getData();
const rowData = normalized.rowData;
if (rowData._tagExpanded === undefined) {
rowData._tagExpanded = false;
}
const renderTags = () => {
container.innerHTML = '';
parsedTags = parsedTags.filter(item => item !== null);
parsedTags = parsedTags.filter(tag => {
const mapped = mappedData.find(
m => m.typ_kurzbz === tag.typ_kurzbz
);
if (!mapped) {
return true;
}
return isInSemester(mapped);
});
parsedTags.sort((a, b) => {
let adone = a.done ? 1 : 0;
@@ -34,10 +121,14 @@ export function tagFormatter(cell, tagComponent)
}
return b.id - a.id;
});
const tagsToShow = rowData._tagExpanded ? parsedTags : parsedTags.slice(0, maxVisibleTags);
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;
@@ -60,8 +151,10 @@ export function tagFormatter(cell, tagComponent)
container.appendChild(tagElement);
});
if (parsedTags.length > maxVisibleTags) {
// show expand button
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";