From 4ed87b5c2b59eb3b5acdfaa7ab56c23fc57ff7b0 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Sat, 27 Jun 2026 12:29:28 +0200 Subject: [PATCH] add custom tag filter --- .../Stv/Studentenverwaltung/List.js | 258 ++------ .../tabulator/filters/extendedHeaderFilter.js | 595 +++++++++++++----- 2 files changed, 497 insertions(+), 356 deletions(-) diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js index e70c48d83..3079eb080 100644 --- a/public/js/components/Stv/Studentenverwaltung/List.js +++ b/public/js/components/Stv/Studentenverwaltung/List.js @@ -1,7 +1,7 @@ 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, buildTagOptionsFromRows, customTagFilter, setTagHeaderFilterValue, tagHeaderFilter } from "../../../tabulator/filters/extendedHeaderFilter.js"; import { addTagInTable, deleteTagInTable, @@ -64,8 +64,11 @@ export default { } return { - initialTagData: [], - selectedTagOptions: [], + tagFilterState: { + initialOptions: [], + selectedOptions: [], + }, + selectedColumnValues: [], tabulatorOptions: { columns: [ { title: "UID", field: "uid", headerFilter: true }, @@ -398,7 +401,7 @@ export default { { event: "dataProcessed", handler: (data) => { - console.log("dataProcessed", data); + //console.log("dataProcessed", data); this.getAllRows(); this.autoSelectRows(data); }, @@ -406,7 +409,7 @@ export default { { event: "dataLoaded", handler: (data) => { - console.log("dataLoaded", data); + //console.log("dataLoaded", data); if (Array.isArray(data)) { this.count = data.length; this.allPrestudents = data.map((item) => item.prestudent_id); @@ -414,12 +417,13 @@ export default { this.count = 0; this.allPrestudents = []; } + this.refreshTagHeaderFilterInitialOptions(Array.isArray(data) ? data : []); }, }, { event: "dataFiltered", handler: (filters, rows) => { - console.log("dataFiltered", filters, rows); + //console.log("dataFiltered", filters, rows); this.filteredcount = rows.length; }, }, @@ -430,14 +434,14 @@ export default { { event: "dataTreeRowExpanded", handler: (data) => { - console.log("dataTreeRowExpanded", data); + //console.log("dataTreeRowExpanded", data); this.getExpandedRows(); }, }, { event: "dataTreeRowCollapsed", handler: (data) => { - console.log("dataTreeRowCollapsed", data); + //console.log("dataTreeRowCollapsed", data); this.getExpandedRows(); }, }, @@ -448,7 +452,7 @@ export default { { event: "columnWidth", handler: (column) => { - console.log("columnWidth", column); + //console.log("columnWidth", column); if (column.getField() !== "tags") return; column.getCells().forEach((cell) => { @@ -465,7 +469,6 @@ export default { selectedcount: 0, //tags expanded: [], - selectedColumnValues: [], tagEndpoint: ApiTag, currentEndpoint: null, headerFilterActive: false, @@ -561,175 +564,13 @@ export default { title: "Tags", field: "tags", tooltip: false, - headerFilter: "list", + headerFilter: customTagFilter, headerFilterParams: { - valuesLookup: () => { - console.log("headerFilterParams.valuesLookup called"); - const options = new Map(); - - this.$refs.table.tabulator.getData("active").forEach((row) => { - let tags = row.tags; - - if (typeof tags === "string") { - try { - tags = JSON.parse(tags); - } catch { - return; - } - } - - if (!Array.isArray(tags)) return; - - tags - .filter((tag) => tag && tag.done !== true) - .forEach((tag) => { - options.set(tag.beschreibung, { - label: tag.beschreibung, - value: tag.beschreibung, - style: tag.style, - }); - }); - }); - - return [ - { - isHeader: true, - label: "test", - subOptions: [...options.values()], - }, - ]; - }, - itemFormatter: (value, label, item) => { - console.log("value, label, item", value, label, item); - if (!item.isHeader) return ""; - - let table = document.createElement("table"); - table.className = "table table-sm mb-0"; - table.appendChild(document.createElement("thead")); - table - .querySelector("thead") - .appendChild(document.createElement("tr")); - - let firstHeading = document.createElement("th"); - firstHeading.innerHTML = "" + value + ""; - table.querySelector("tr").appendChild(firstHeading); - - let secondHeading = document.createElement("th"); - secondHeading.innerHTML = "AND"; - table.querySelector("tr").appendChild(secondHeading); - - let thirdHeading = document.createElement("th"); - thirdHeading.innerHTML = "OR"; - table.querySelector("tr").appendChild(thirdHeading); - - let fourthHeading = document.createElement("th"); - fourthHeading.innerHTML = "NOT"; - table.querySelector("tr").appendChild(fourthHeading); - - table.appendChild(document.createElement("tbody")); - - item.subOptions.forEach((option) => { - let row = document.createElement("tr"); - - let firstColumn = document.createElement("td"); - - let optionLabel = document.createElement("span"); - optionLabel.className = "tag " + option.style; - optionLabel.innerText = option.label; - firstColumn.appendChild(optionLabel); - row.appendChild(firstColumn); - - let secondColumn = document.createElement("td"); - let firstCheckbox = document.createElement("input"); - firstCheckbox.type = "checkbox"; - firstCheckbox.label = "test"; - firstCheckbox.style.cursor = "pointer"; - firstCheckbox.checked = this.selectedTagOptions.some( - (tag) => tag.value === option.value && tag.connector === "AND", - ); - firstCheckbox.addEventListener("click", (event) => { - event.stopPropagation(); - if (event.target.checked) { - this.selectedTagOptions.push({ - label: option.label, - value: option.value, - connector: "AND", - }); - } else { - this.selectedTagOptions = this.selectedTagOptions.filter( - (tag) => - tag.value !== option.value || tag.connector !== "AND", - ); - } - console.log("selectedTagOptions", this.selectedTagOptions); - }); - secondColumn.appendChild(firstCheckbox); - row.appendChild(secondColumn); - - let thirdColumn = document.createElement("td"); - let secondCheckbox = document.createElement("input"); - secondCheckbox.type = "checkbox"; - secondCheckbox.label = "test"; - secondCheckbox.style.cursor = "pointer"; - secondCheckbox.checked = this.selectedTagOptions.some( - (tag) => tag.value === option.value && tag.connector === "OR", - ); - secondCheckbox.addEventListener("click", (event) => { - event.stopPropagation(); - if (event.target.checked) { - this.selectedTagOptions.push({ - label: option.label, - value: option.value, - connector: "OR", - }); - } else { - this.selectedTagOptions = this.selectedTagOptions.filter( - (tag) => - tag.value !== option.value || tag.connector !== "OR", - ); - } - }); - thirdColumn.appendChild(secondCheckbox); - row.appendChild(thirdColumn); - - let fourthColumn = document.createElement("td"); - let thirdCheckbox = document.createElement("input"); - thirdCheckbox.type = "checkbox"; - thirdCheckbox.label = "test"; - thirdCheckbox.style.cursor = "pointer"; - thirdCheckbox.checked = this.selectedTagOptions.some( - (tag) => tag.value === option.value && tag.connector === "NOT", - ); - thirdCheckbox.addEventListener("click", (event) => { - event.stopPropagation(); - console.log(event.target.checked); - if (event.target.checked) { - this.selectedTagOptions.push({ - label: option.label, - value: option.value, - connector: "NOT", - }); - } else { - this.selectedTagOptions = this.selectedTagOptions.filter( - (tag) => - tag.value !== option.value || tag.connector !== "NOT", - ); - } - }); - fourthColumn.appendChild(thirdCheckbox); - row.appendChild(fourthColumn); - - row.addEventListener("click", (event) => { - event.stopPropagation(); - }); - - table.querySelector("tbody").appendChild(row); - }); - return table; - }, listOnEmpty: true, autocomplete: true, sort: "asc", + initialOptions: this.tagFilterState.initialOptions, + selectedOptions: this.tagFilterState.selectedOptions, }, headerFilterFunc: tagHeaderFilter, headerFilterFuncParams: { field: "tags" }, @@ -746,33 +587,18 @@ export default { this.translateTabulator(); } }, - selectedTagOptions: { + "tagFilterState.selectedOptions": { handler() { - let combinedFilterStatement = null; + const selectedOptions = this.tagFilterState.selectedOptions; + const combinedFilterStatement = buildTagHeaderFilterExpression( + selectedOptions, + ); - this.selectedTagOptions.forEach((tagOption) => { - const { value, connector } = tagOption; - - let mappedConnector = connector === "AND" ? "&&" : connector === "OR" ? "||" : "!"; - if (connector === "NOT") { - combinedFilterStatement = combinedFilterStatement - ? `${combinedFilterStatement} && ${mappedConnector + value}` - : value; - } - else { - combinedFilterStatement = combinedFilterStatement - ? `${combinedFilterStatement} ${mappedConnector} ${value}` - : value; - } - }); - - this.$refs.table.tabulator.setHeaderFilterValue( - "tags", + setTagHeaderFilterValue( + this.$refs.table.tabulator, combinedFilterStatement, ); - this.$emit("filterActive", this.selectedTagOptions.length > 0); - console.log("selectedTagOptions changed:"); - console.log(combinedFilterStatement); + this.$emit("filterActive", selectedOptions.length > 0); }, deep: true, }, @@ -868,7 +694,7 @@ export default { this.$refs.new.open(); }, rowSelectionChanged(data, rows, selected, deselected) { - console.log("rowSelectionChanged", data, rows, selected, deselected); + //console.log("rowSelectionChanged", data, rows, selected, deselected); this.selectedcount = data.length; if (selected.length > 0 || deselected.length > 0) { @@ -915,8 +741,8 @@ export default { updateUrl(endpoint, first) { this.lastSelected = first ? undefined : this.selected; - /* console.log('function param endpoint: ' + JSON.stringify(endpoint)); - console.log('current endpoint: ' + JSON.stringify(this.currentEndpoint));*/ + /* //console.log('function param endpoint: ' + JSON.stringify(endpoint)); + //console.log('current endpoint: ' + JSON.stringify(this.currentEndpoint));*/ if (endpoint === undefined && this.currentEndpoint === null) { endpoint = { url: "" }; @@ -1021,7 +847,7 @@ export default { } }, handleRowClick(e, row) { - console.log("handleRowClick", e, row); + //console.log("handleRowClick", e, row); // TODO(chris): this should be in the filter component if (this.focusObj) { let el = row.getElement(); @@ -1035,12 +861,34 @@ export default { //methods tags addedTag(addedTag) { addTagInTable(addedTag, this.allRows, "prestudent_id"); + this.refreshTagHeaderFilterInitialOptions(); }, deletedTag(deletedTag) { + console.log("deletedTag", deletedTag); deleteTagInTable(deletedTag, this.allRows); + this.refreshTagHeaderFilterInitialOptions(); }, updatedTag(updatedTag) { updateTagInTable(updatedTag, this.allRows); + this.refreshTagHeaderFilterInitialOptions(); + }, + refreshTagHeaderFilterInitialOptions(rows) { + const data = + rows || this.$refs.table?.tabulator?.getData() || []; + + const options = buildTagOptionsFromRows(data); + this.tagFilterState.initialOptions.splice( + 0, + this.tagFilterState.initialOptions.length, + ...options, + ); + + const availableValues = new Set(options.map((option) => option.value)); + for (let i = this.tagFilterState.selectedOptions.length - 1; i >= 0; i--) { + if (!availableValues.has(this.tagFilterState.selectedOptions[i].value)) { + this.tagFilterState.selectedOptions.splice(i, 1); + } + } }, getAllRows() { this.allRows = this.$refs.table.tabulator.getRows(); @@ -1053,7 +901,7 @@ export default { this.headerFilterActive = filterActive; }, handleMouseDown(e, row) { - console.log("handleMouseDown", e, row); + //console.log("handleMouseDown", e, row); let data = row.getData(); let id = data.uid ?? data.prestudent_id ?? data.person_id; @@ -1065,12 +913,12 @@ export default { isAlreadySelected && this.selected?.length ? this.selected : [data]; }, handleDataLoading() { - console.log("handleDataLoading"); + //console.log("handleDataLoading"); this.oldScrollLeft = this.$refs.table.tabulator.rowManager.scrollLeft; this.oldScrollTop = this.$refs.table.tabulator.rowManager.scrollTop; }, handleRenderComplete() { - console.log("handleRenderComplete"); + //console.log("handleRenderComplete"); const table = this.$refs.table.tabulator.element.querySelector( ".tabulator-tableholder", ); diff --git a/public/js/tabulator/filters/extendedHeaderFilter.js b/public/js/tabulator/filters/extendedHeaderFilter.js index 148ddb7fa..d9f6faf7a 100644 --- a/public/js/tabulator/filters/extendedHeaderFilter.js +++ b/public/js/tabulator/filters/extendedHeaderFilter.js @@ -1,163 +1,456 @@ -function parseFilterExpression(expression) -{ - const collections = []; +let activeTagDropdownCleanup = null; +const TAG_FILTER_CONNECTORS = { + AND: { + key: "AND", + label: "AND", + operator: "&&", + }, + OR: { + key: "OR", + label: "OR", + operator: "||", + }, + NOT: { + key: "NOT", + label: "NOT", + operator: "!", + }, +}; +const TAG_FILTER_CONNECTOR_ORDER = [ + TAG_FILTER_CONNECTORS.AND, + TAG_FILTER_CONNECTORS.OR, + TAG_FILTER_CONNECTORS.NOT, +]; - try { - const orParts = expression.split('||').map(part => part.trim()); +export function setTagHeaderFilterValue(tabulator, value) { + tabulator?.setHeaderFilterValue("tags", value); - orParts.forEach(part => { + const input = tabulator + ?.getColumn("tags") + ?.getElement() + ?.querySelector(".tabulator-header-filter input"); - const andParts = part.split('&&').map(p => p.trim()); - - const collection = { positives: [], negatives: [] }; - - andParts.forEach(term => { - - const comparisonMatch = term.match(/^(<=|>=|<|>|=|!=)\s*(-?\d+(?:[.,]\d+)?)$/); - - if (comparisonMatch) - { - const operator = comparisonMatch[1]; - const numberStr = comparisonMatch[2].replace(',', '.'); - const number = parseFloat(numberStr); - collection.positives.push({ type: 'comparison', operator, number }); - } - else if (term.startsWith('!')) - { - const excludeTerm = term.substring(1).trim().replace(/\*/g, '.*'); - collection.negatives.push({ type: 'regex', regex: new RegExp(excludeTerm, 'i') }); - } - else - { - const includeTerm = term.replace(/\*/g, '.*'); - collection.positives.push({ type: 'regex', regex: new RegExp(includeTerm, 'i') }); - } - }); - collections.push(collection); - }); - } catch (e) {} - return collections; + if (input) { + input.value = value ?? ""; + } } -export function extendedHeaderFilter(headerValue, rowValue, rowData, filterParams) -{ - const fields = Array.isArray(filterParams?.field) - ? filterParams.field - : [filterParams?.field]; +export function buildTagHeaderFilterExpression(selectedTagOptions) { + let expression = null; - if (fields.length > 1 && rowData) - { - rowValue = fields - .map(f => rowData[f] ?? '') - .filter(Boolean) - .join(' '); - } - if (typeof headerValue === 'boolean') - { - return rowValue === headerValue; - } + selectedTagOptions.forEach(({ value, connector }) => { + const connectorDef = TAG_FILTER_CONNECTORS[connector]; + if (!connectorDef) return; - const collections = parseFilterExpression(headerValue); + if (connectorDef.key === TAG_FILTER_CONNECTORS.NOT.key) { + expression = expression + ? `${expression} ${TAG_FILTER_CONNECTORS.AND.operator} ${connectorDef.operator}${value}` + : `${connectorDef.operator}${value}`; + } else { + expression = expression + ? `${expression} ${connectorDef.operator} ${value}` + : value; + } + }); - function matchValue(value) - { - try { - return collections.some(collection => { - - let positives = collection.positives.length === 0 || collection.positives.every(condition => { - - if (condition.type === 'comparison') - { - let value = parseFloat(rowValue); - if (isNaN(value)) return false; - - switch (condition.operator) { - case '<': - return value < condition.number; - case '>': - return value > condition.number; - case '<=': - return value <= condition.number; - case '>=': - return value >= condition.number; - case '=': - return value === condition.number; - case '!=': - return value !== condition.number; - default: - return false; - } - } - else if (condition.type === 'regex') - { - return condition.regex.test(rowValue); - } - return false; - }); - - let negatives = collection.negatives.every(condition => { - return !condition.regex.test(rowValue); - }); - - return positives && negatives; - }); - } catch (e) { - - } - } - - if (matchValue(rowValue)) - return true; - - if (rowData && filterParams) - { - const childrenField = filterParams?.children || '_children'; - const field = filterParams?.field; - - const children = rowData[childrenField]; - if (Array.isArray(children)) - { - for (let child of children) - { - let childValue = child[field]; - if (extendedHeaderFilter(headerValue, childValue, child, filterParams)) - { - return true; - } - } - } - } - - return false; + return expression; } -export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams) -{ - console.log('tagHeaderFilter', headerValue, rowValue, rowData, filterParams); - let data; - console.log('rowValue', rowValue); - try { - data = typeof rowValue === 'string' ? JSON.parse(rowValue) : rowValue; - } catch (error) { - return false; - } - let combinedText; +export function buildTagOptionsFromRows(rows = []) { + const options = new Map(); - if (Array.isArray(data)) - { - combinedText = data - .filter(item => item?.done !== true) - .map(item => `${item?.beschreibung} ${item?.notiz}`) - .join(' '); - } - else if (typeof data === 'object' && data !== null) - { - combinedText = data?.erledigt === false ? `${data?.beschreibung} ${data?.notiz}` : ''; - } - else - { - combinedText = String(data); - } + rows.forEach((row) => { + let tags = row.tags; - return extendedHeaderFilter(headerValue, combinedText, rowData, filterParams) -} \ No newline at end of file + if (typeof tags === "string") { + try { + tags = JSON.parse(tags); + } catch { + return; + } + } + + if (!Array.isArray(tags)) return; + + tags + .filter((tag) => tag && tag.done !== true) + .forEach((tag) => { + options.set(tag.beschreibung, { + label: tag.beschreibung, + value: tag.beschreibung, + style: tag.style, + }); + }); + }); + + return [...options.values()]; +} + +export function customTagFilter(cell, onRendered, success, cancel, params) { + const container = document.createElement("div"); + container.style.width = "100%"; + + const input = document.createElement("input"); + input.style.width = "100%"; + input.type = "text"; + let dropdownMenu = null; + + Object.defineProperty(container, "value", { + get: () => input.value, + set: (value) => { + input.value = value ?? ""; + }, + }); + + const positionDropdown = () => { + if (!dropdownMenu) return; + + const rect = input.getBoundingClientRect(); + const width = Math.min(Math.max(rect.width, 360), window.innerWidth - 16); + const left = Math.min( + Math.max(rect.left, 8), + window.innerWidth - width - 8, + ); + + dropdownMenu.style.position = "fixed"; + dropdownMenu.style.left = `${left}px`; + dropdownMenu.style.top = `${rect.bottom + 2}px`; + dropdownMenu.style.width = `${width}px`; + dropdownMenu.style.maxHeight = "50vh"; + dropdownMenu.style.overflowY = "auto"; + dropdownMenu.style.zIndex = "2000"; + }; + + const closeDropdown = () => { + if (dropdownMenu) { + dropdownMenu.remove(); + dropdownMenu = null; + } + + if (activeTagDropdownCleanup === closeDropdown) { + activeTagDropdownCleanup = null; + } + + window.removeEventListener("scroll", positionDropdown, true); + window.removeEventListener("resize", positionDropdown); + document.removeEventListener("mousedown", handleOutsideClick, true); + }; + + const handleOutsideClick = (event) => { + if ( + dropdownMenu?.contains(event.target) || + container.contains(event.target) + ) { + return; + } + + closeDropdown(); + }; + + const clearFilter = () => { + input.value = ""; + + if (Array.isArray(params.selectedOptions)) { + params.selectedOptions.splice( + 0, + params.selectedOptions.length, + ); + } + + success(""); + closeDropdown(); + }; + + input.addEventListener("input", () => { + success(input.value); + }); + input.addEventListener("change", () => { + success(input.value); + }); + input.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + + event.preventDefault(); + event.stopPropagation(); + clearFilter(); + }); + input.addEventListener("focus", (e) => { + e.stopPropagation(); + + if (activeTagDropdownCleanup) { + activeTagDropdownCleanup(); + } + + dropdownMenu = getRebuiltDropdown( + params.initialOptions, + params.selectedOptions, + ); + dropdownMenu.addEventListener("mousedown", (event) => { + event.stopPropagation(); + }); + + document.body.appendChild(dropdownMenu); + activeTagDropdownCleanup = closeDropdown; + + positionDropdown(); + + window.addEventListener("scroll", positionDropdown, true); + window.addEventListener("resize", positionDropdown); + document.addEventListener("mousedown", handleOutsideClick, true); + }); + + input.addEventListener("blur", (e) => { + }); + + container.appendChild(input); + + return container; +} + +function getRebuiltDropdown(initialOptions, selectedOptions) { + let dropdownTable = generateDropdownTable( + initialOptions || [], + selectedOptions || [], + ); + + const menu = document.createElement("div"); + menu.className = "dropdown-menu show"; + + menu.appendChild(dropdownTable); + + return menu; +} + +function generateDropdownTable(options, selectedTagOptions) { + const table = document.createElement("table"); + table.className = "table table-sm mb-0"; + + // HEADER + const thead = document.createElement("thead"); + const headRow = document.createElement("tr"); + + const headers = [ + "Tag", + ...TAG_FILTER_CONNECTOR_ORDER.map((connector) => connector.label), + ]; + + headers.forEach((h) => { + const th = document.createElement("th"); + th.innerHTML = `${h}`; + headRow.appendChild(th); + }); + + thead.appendChild(headRow); + table.appendChild(thead); + + // BODY + const tbody = document.createElement("tbody"); + options.forEach((option) => { + const row = document.createElement("tr"); + + // LABEL + const labelTd = document.createElement("td"); + const label = document.createElement("span"); + label.className = "tag " + option.style; + label.innerText = option.label; + labelTd.appendChild(label); + row.appendChild(labelTd); + + function makeCheckbox(connector) { + const td = document.createElement("td"); + const cb = document.createElement("input"); + + cb.type = "checkbox"; + cb.style.cursor = "pointer"; + + cb.checked = selectedTagOptions.some( + (t) => t.value === option.value && t.connector === connector.key, + ); + + cb.addEventListener("click", (event) => { + event.stopPropagation(); + + if (event.target.checked) { + selectedTagOptions.push({ + label: option.label, + value: option.value, + connector: connector.key, + }); + } else { + const idx = selectedTagOptions.findIndex( + (t) => t.value === option.value && t.connector === connector.key, + ); + + if (idx !== -1) { + selectedTagOptions.splice(idx, 1); + } + } + + }); + + td.appendChild(cb); + return td; + } + + TAG_FILTER_CONNECTOR_ORDER.forEach((connector) => { + row.appendChild(makeCheckbox(connector)); + }); + + tbody.appendChild(row); + }); + + table.appendChild(tbody); + + return table; +} + +function parseFilterExpression(expression) { + const collections = []; + + try { + const orParts = expression.split("||").map((part) => part.trim()); + + orParts.forEach((part) => { + const andParts = part.split("&&").map((p) => p.trim()); + + const collection = { positives: [], negatives: [] }; + + andParts.forEach((term) => { + const comparisonMatch = term.match( + /^(<=|>=|<|>|=|!=)\s*(-?\d+(?:[.,]\d+)?)$/, + ); + + if (comparisonMatch) { + const operator = comparisonMatch[1]; + const numberStr = comparisonMatch[2].replace(",", "."); + const number = parseFloat(numberStr); + collection.positives.push({ type: "comparison", operator, number }); + } else if (term.startsWith("!")) { + const excludeTerm = term.substring(1).trim().replace(/\*/g, ".*"); + collection.negatives.push({ + type: "regex", + regex: new RegExp(excludeTerm, "i"), + }); + } else { + const includeTerm = term.replace(/\*/g, ".*"); + collection.positives.push({ + type: "regex", + regex: new RegExp(includeTerm, "i"), + }); + } + }); + collections.push(collection); + }); + } catch (e) {} + return collections; +} + +export function extendedHeaderFilter( + headerValue, + rowValue, + rowData, + filterParams, +) { + const fields = Array.isArray(filterParams?.field) + ? filterParams.field + : [filterParams?.field]; + + if (fields.length > 1 && rowData) { + rowValue = fields + .map((f) => rowData[f] ?? "") + .filter(Boolean) + .join(" "); + } + if (typeof headerValue === "boolean") { + return rowValue === headerValue; + } + + const collections = parseFilterExpression(headerValue); + + function matchValue(value) { + try { + return collections.some((collection) => { + let positives = + collection.positives.length === 0 || + collection.positives.every((condition) => { + if (condition.type === "comparison") { + let value = parseFloat(rowValue); + if (isNaN(value)) return false; + + switch (condition.operator) { + case "<": + return value < condition.number; + case ">": + return value > condition.number; + case "<=": + return value <= condition.number; + case ">=": + return value >= condition.number; + case "=": + return value === condition.number; + case "!=": + return value !== condition.number; + default: + return false; + } + } else if (condition.type === "regex") { + return condition.regex.test(rowValue); + } + return false; + }); + + let negatives = collection.negatives.every((condition) => { + return !condition.regex.test(rowValue); + }); + + return positives && negatives; + }); + } catch (e) {} + } + + if (matchValue(rowValue)) return true; + + if (rowData && filterParams) { + const childrenField = filterParams?.children || "_children"; + const field = filterParams?.field; + + const children = rowData[childrenField]; + if (Array.isArray(children)) { + for (let child of children) { + let childValue = child[field]; + if ( + extendedHeaderFilter(headerValue, childValue, child, filterParams) + ) { + return true; + } + } + } + } + + return false; +} +export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams) { + let data; + + try { + data = typeof rowValue === "string" ? JSON.parse(rowValue) : rowValue; + } catch (error) { + return false; + } + + let combinedText; + + if (Array.isArray(data)) { + combinedText = data + .filter((item) => item?.done !== true) + .map((item) => `${item?.beschreibung} ${item?.notiz}`) + .join(" "); + } else if (typeof data === "object" && data !== null) { + combinedText = + data?.erledigt === false ? `${data?.beschreibung} ${data?.notiz}` : ""; + } else { + combinedText = String(data); + } + + return extendedHeaderFilter(headerValue, combinedText, rowData, filterParams); +}