diff --git a/public/js/TableWidget.js b/public/js/TableWidget.js index ec93422fc..4fa89aa0b 100644 --- a/public/js/TableWidget.js +++ b/public/js/TableWidget.js @@ -582,7 +582,16 @@ var FHC_TableWidget = { options.columns = arrayTabulatorColumns; options.data = data.dataset; - options.persistence = (typeof options.persistence == 'undefined') ? true : options.persistence; // enables persistence (default store in localStorage if available, else in cookie) + + let defaultPersistence = { + sort: true, + columns: true, + filter: false, + headerFilter: false, + group: false, + page: false, + } + options.persistence = (typeof options.persistence == 'undefined') ? defaultPersistence : options.persistence; // enables persistence (default store in localStorage if available, else in cookie) options.persistenceID = (typeof options.persistenceID == 'undefined') ? data.tableUniqueId : options.persistenceID; // persistenceID to store persistence data seperately for multiple tables options.movableColumns = (typeof options.movableColumns == 'undefined') ? true : options.movableColumns; // allows changing column order options.tooltipsHeader = (typeof options.tooltipsHeader == 'undefined') ? true : options.tooltipsHeader; // set header tooltip with column title diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index aa6d29a09..9c2a13529 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -20,7 +20,6 @@ import FilterConfig from './Filter/Config.js'; import FilterColumns from './Filter/Columns.js'; import TableDownload from './Table/Download.js'; import collapseAutoClose from '../../directives/collapseAutoClose.js'; -import { defaultHeaderFilter } from '../../tabulator/filters/defaultHeaderFilter.js'; import moduleLayoutFitDataStretchFrozen from '../../tabulator/layouts/fitDataStretchFrozen.js'; @@ -84,6 +83,7 @@ export const CoreFilterCmpt = { uuid: 0, // FilterCmpt properties filterName: null, + filterActive: false, fields: null, dataset: null, datasetMetadata: null, @@ -103,7 +103,15 @@ export const CoreFilterCmpt = { tabulator: null, tableBuilt: false, tabulatorHasSelector: false, - selectedData: [] + selectedData: [], + persistence: { + sort: true, + columns: true, + filter: false, + headerFilter: false, + group: false, + page: false, + } }; }, computed: { @@ -210,12 +218,11 @@ export const CoreFilterCmpt = { layout: "fitDataStretchFrozen", movableColumns: true, columnDefaults:{ - tooltip: true, - headerFilterFunc: defaultHeaderFilter, + tooltip: true }, placeholder, reactiveData: true, - persistence: true + persistence: this.persistence, }, ...(this.tabulatorOptions || {})}; if (!this.tableOnly) { @@ -281,11 +288,15 @@ export const CoreFilterCmpt = { const cols = this.tabulator.getColumns(); this.fields = cols.map(col => col.getField()); this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField()); - + if (this.tabulator.options.persistence.headerFilter) + this._setHeaderFilter(); }); } + this.tabulator.on("dataFiltered", filters => { + this.filterActive = filters.length > 0; + }); }, updateTabulator() { if (this.tabulator) { @@ -299,6 +310,23 @@ export const CoreFilterCmpt = { this.tabulatorHasSelector = this.tabulatorOptions.selectable || this.filteredColumns.filter(el => el.formatter == 'rowSelection').length; this.tabulator.setColumns(this.filteredColumns); this.tabulator.setData(this.filteredData); + this._setHeaderFilter() + }, + clearFilters() + { + let existingFilters = this.tabulator.getHeaderFilters(); + existingFilters.forEach(filter => { + this.tabulator.setHeaderFilterValue(filter.field, ""); + }); + this.tabulator.clearFilter(); + this.filterActive = false; + }, + _setHeaderFilter() + { + const existingFilters = this.tabulator.getHeaderFilters(); + existingFilters.forEach(filter => { + this.tabulator.setHeaderFilterValue(filter.field, filter.value); + }); }, /** * @@ -581,16 +609,21 @@ export const CoreFilterCmpt = { alert('"nwNewEntry" listener is mandatory when sideMenu is true'); this.uuid = _uuid++; this.$emit('uuidDefined', this.uuid) - if (!this.tableOnly) - this.getFilter(); // get the filter data }, mounted() { - this.initTabulator(); + + this.initTabulator().then(() => { + if (!this.tableOnly) { + this.selectedFilter = window.location.hash ? window.location.hash.slice(1) : null; + this.getFilter(); // get the filter data + } + }); + }, template: ` + + + diff --git a/public/js/tabulator/filters/Dates.js b/public/js/tabulator/filters/Dates.js index 7c9f268f0..429d63e1a 100644 --- a/public/js/tabulator/filters/Dates.js +++ b/public/js/tabulator/filters/Dates.js @@ -7,26 +7,49 @@ Tabulator.extendModule('filter', 'filters', { "dates": (headerValue, rowValue) => { if (!headerValue) return true; - let v = new Date(rowValue); - if (Array.isArray(headerValue)) { - if (headerValue[1]) { - return v >= headerValue[0] && v <= headerValue[1].setHours(23, 59, 59, 999); + + let rowDate = new Date(rowValue); + + if (Array.isArray(headerValue)) + { + let startDate = new Date(headerValue[0]); + if (headerValue[1]) + { + let endDate = new Date(headerValue[1]); + + endDate.setHours(23, 59, 59, 999); + + return rowDate >= startDate && rowDate <= endDate; } - return v.toDateString() == headerValue[0].toDateString(); + + return rowDate.toDateString() === startDate.toDateString(); } - return v.toDateString() == headerValue.toDateString(); + let singleDate = new Date(headerValue); + return rowDate.toDateString() === singleDate.toDateString(); } }); + function dateFilter(cell, onRendered, success) { let div = document.createElement('div'); + let initialValue = null; + + let val = cell.getValue(); + + if (Array.isArray(val)) + { + const start = val[0] ? new Date(val[0]) : null; + const end = val[1] ? new Date(val[1]) : null; + initialValue = [start, end]; + } + Vue.createApp({ components: { PrimevueCalendar: primevue.calendar }, data() { return { - val: null + val: initialValue } }, watch: { @@ -34,10 +57,16 @@ function dateFilter(cell, onRendered, success) { success(n); } }, - template: `` + template: ` + ` }).use(primevue.config.default).mount(div); return div; } - -export { dateFilter as 'dateFilter' }; +export { dateFilter }; \ No newline at end of file diff --git a/public/js/tabulator/filters/defaultHeaderFilter.js b/public/js/tabulator/filters/defaultHeaderFilter.js index facce437a..e69de29bb 100644 --- a/public/js/tabulator/filters/defaultHeaderFilter.js +++ b/public/js/tabulator/filters/defaultHeaderFilter.js @@ -1,87 +0,0 @@ -function parseFilterExpression(expression){ - const includeGroups = []; - const excludeTerms = []; - const comparisons = []; - - if( typeof expression !== 'string' ) { - comparisons.push({ operator: '=', number: expression }); - return { includeGroups, excludeTerms, comparisons }; - } - - const andParts = expression.split('&&').map(part => part.trim()); - - andParts.forEach(part => { - const orTerms = part.split('||').map(p => p.trim()); - const orRegexes = []; - - orTerms.forEach(term => { - - const comparisonMatch = term.match(/^(<=|>=|<|>|=|!=)\s*(\d+)$/); - - if (comparisonMatch) - { - const operator = comparisonMatch[1]; - const number = parseFloat(comparisonMatch[2]); - - comparisons.push({ operator, number }); - } - else if (term.startsWith('!')) - { - const excludeTerm = term.substring(1).trim().replace(/\*/g, '.*'); - excludeTerms.push(new RegExp(excludeTerm, 'i')); - } - else - { - const includeTerm = term.replace(/\*/g, '.*'); - orRegexes.push(new RegExp(includeTerm, 'i')); - } - }); - - if (orRegexes.length > 0) - { - includeGroups.push(orRegexes); - } - }); - - return { includeGroups, excludeTerms, comparisons }; -} - -export function defaultHeaderFilter(headerValue, rowValue) -{ - const { includeGroups, excludeTerms, comparisons } = parseFilterExpression(headerValue); - - const includes = includeGroups.every(group => - group.some(regex => regex.test(rowValue)) - ); - - const excludes = excludeTerms.every(regex => !regex.test(rowValue)); - - const comparisonCheck = comparisons.every(({ operator, number }) => { - let value = rowValue; - - if (!isNaN(number) && typeof number !== 'boolean') - { - value = parseFloat(rowValue); - if (isNaN(value)) return false; - } - - switch (operator) { - case '<': - return value < number; - case '>': - return value > number; - case '<=': - return value <= number; - case '>=': - return value >= number; - case '=': - return value === number; - case '!=': - return value !== number; - default: - return false; - } - }); - - return includes && excludes && comparisonCheck; -} diff --git a/public/js/tabulator/filters/extendedHeaderFilter.js b/public/js/tabulator/filters/extendedHeaderFilter.js new file mode 100644 index 000000000..d8ead88bc --- /dev/null +++ b/public/js/tabulator/filters/extendedHeaderFilter.js @@ -0,0 +1,125 @@ +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) +{ + if (typeof headerValue === 'boolean') + { + return rowValue === headerValue; + } + + const collections = parseFilterExpression(headerValue); + + 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) { + + } +} +export function tagHeaderFilter(headerValue, rowValue) { + + let data; + + try { + data = typeof rowValue === 'string' ? JSON.parse(rowValue) : rowValue; + } catch (error) { + return false; + } + + let combinedText; + + if (Array.isArray(data)) + { + combinedText = data + .map(item => `${item.beschreibung} ${item.notiz}`) + .join(' '); + } + else if (typeof data === 'object' && data !== null) + { + combinedText = `${data.beschreibung} ${data.notiz}`; + } + else + { + combinedText = String(data); + } + + return extendedHeaderFilter(headerValue, combinedText) +} + diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index d92127cde..14bb60e0e 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -1815,6 +1815,26 @@ $phrases = array( ) ) ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'filterdelete', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Filter löschen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Clear filter', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'anrechnung',