import { Option, Category, Element, ElementModerationState, MenuFilter } from '../../classes/classes' import { anyToArray } from '../../utils/array' import { App } from '../../gogocarto' declare let $: any, moment, extendMoment window['moment-range'].extendMoment(moment) export class FilterModule { showOnlyFavorite_ = false showOnlyPending_ = false showOnlyModeration_ = false constructor() {} showOnlyFavorite(bool: boolean) { this.showOnlyFavorite_ = bool } showOnlyPending(bool: boolean) { this.showOnlyPending_ = bool } showOnlyModeration(bool: boolean) { this.showOnlyModeration_ = bool } checkIfElementPassFilters(element: Element): boolean { if (element.optionsValues.length == 0) return false if (element.status < -1 && App.infoBarComponent.elementVisible != element) return false // FAVORITE, PENDING, MODERATION FILTERS if (this.showOnlyFavorite_) return element.isFavorite if ( this.showOnlyModeration_ && (!element.needsModeration() || element.moderationState == ElementModerationState.PossibleDuplicate) ) return false if (App.config.isFeatureAvailable('pending')) { if (this.showOnlyPending_ && !element.isPending()) return false } else { if (element.isPending()) return false } // CUSTOM FILTERS (except taxonomy) for (const filter of App.config.menu.filters) { switch (filter.type) { case 'date': if (!this.filterDate(element, filter)) return false break case 'number': if (!this.filterNumber(element, filter)) return false break case 'area': if (!this.filterArea(element, filter)) return false break case 'value': if (!this.filterValue(element, filter)) return false break } } // TAXONOMY FILTER (at the end because it is the most costly operation) if (App.config.menu.filters.some((filter) => filter.type == 'taxonomy')) return this.filterTaxonomy(element) else return true } private filterDate(element: Element, filter: MenuFilter): boolean { const filterValue = filter.currentValue const elementDate = moment(element.data[filter.field]) let elementRange = null if (filter.options.fieldEnd) { const elementEndDate = moment(element.data[filter.options.fieldEnd]) elementRange = moment.range(elementDate.startOf('day'), elementEndDate.endOf('day')) } else { elementRange = moment.range(elementDate.clone().startOf('day'), elementDate.clone().endOf('day')) } let targetRange = null // Handle 3 digits years like 121 which actually equals to 2021 if (filterValue.year && filterValue.year < 200) { filterValue.year = filterValue.year - 100 + 2000 } // Empty filter if (!filterValue || Object.keys(filterValue).length === 0) return true // Empty value else if (!elementDate) return false // RANGE else if (filterValue.startDate && filterValue.endDate) targetRange = moment.range(moment(filterValue.startDate).startOf('day'), moment(filterValue.endDate).endOf('day')) // DATES else if (filterValue.dates) { if (filterValue.dates.length == 0) return true return filterValue.dates.some((date) => { return elementRange.overlaps(moment.range(moment(date).startOf('day'), moment(date).endOf('day'))) }) } // MONTH else if (filterValue.month !== undefined && filterValue.year) targetRange = moment.range( moment().month(filterValue.month).year(filterValue.year).startOf('month'), moment().month(filterValue.month).year(filterValue.year).endOf('month'), ) // YEAR else if (filterValue.year) targetRange = moment.range( moment().year(filterValue.year).startOf('year'), moment().year(filterValue.year).endOf('year'), ) else return true return targetRange ? targetRange.overlaps(elementRange) : true } private filterNumber(element: Element, filter: MenuFilter): boolean { const filterValue = filter.currentValue const elementValue = parseFloat(element.data[filter.field]) // Empty filter if (!filterValue || Object.keys(filterValue).length === 0) return true // Empty value else if (!elementValue) return false else if (filterValue.value != undefined) return elementValue == filterValue.value if (filterValue.min != undefined && filterValue.max != undefined) return elementValue <= filterValue.max && elementValue >= filterValue.min return true } private filterValue(element: Element, filter: MenuFilter): boolean { let filterValue = filter.currentValue?.value // Check for labels in filter options let optionInSettings = null if (filter?.options?.values) { optionInSettings = filter?.options?.values.find((option) => option.label === filterValue) } if (optionInSettings) { filterValue = optionInSettings.value } const elementValue = element.data[filter.field] // Empty filter if (!filterValue && filterValue != '') return true // Empty value else if (!elementValue && elementValue != '') return false else return anyToArray(elementValue).includes(filterValue) } private filterArea(element: Element, filter: MenuFilter): boolean { const filterValues = filter.currentValue const elementValues = [] if (element.data[filter.field]) { if (typeof element.data[filter.field] === 'object') { try { Object.values(element.data[filter.field]).forEach((data: any) => { if (typeof data === 'string') { data = JSON.parse(data) } if (data.type !== 'country') { if (data.codeEpci) { elementValues.push(data.country + '-codeEpci-' + data.codeEpci) } if (data.postCode) { elementValues.push(data.country + '-' + data.postCode + '-' + data.name) } else { elementValues.push(data.country + '-' + data.name) } } else { elementValues.push(data.name) } }) } catch (error) { console.error(element.id + ' - ' + filter.field + ': Failed to parse json', error) } } else { console.error(element.id + ' - ' + filter.field + ' is not an object.') } } if (!filterValues || Object.keys(filterValues).length === 0 || !elementValues) { return true } else { const filterHasPostCode = Object.keys(filterValues).includes('postCode') const filterAreaResponse = Object.values(filterValues).some((filterValue: string, index) => { if (!filterValue) { return false } else { const filterKey = Object.keys(filterValues)[index] let filterValueArray = null switch (filterKey) { case 'codeEpci': filterValue = 'codeEpci-' + filterValue break case 'name': if (filterHasPostCode) { filterValue = filterValues['postCode'] + '-' + filterValue } break case 'departements': case 'regions': filterValueArray = JSON.parse(filterValue) break } if (filterValueArray) { // Epci case, with multiple departements or regions, all must be selected to pass the check filterValueArray = filterValueArray.map((filterValue) => filterValues['country'] + '-' + filterValue) return filterValueArray.every((filterValue) => elementValues.includes(filterValue)) } else { if (filterKey !== 'country') { filterValue = filterValues['country'] + '-' + filterValue } return elementValues.includes(filterValue) } } }) return filterAreaResponse } } log = false private filterTaxonomy(element: Element) { if (!App.config.menu.showOnePanePerMainOption) { return App.taxonomyModule.taxonomy.nonDisabledOptions.some( (mainOption) => element.haveOption(mainOption) && this.recursivelyCheckInOption(mainOption, element), ) } else if (App.currMainId == 'all') { const mainOptionsChecked = App.taxonomyModule.getMainOptions().filter((opt) => opt.isActive) const mainCategoryFilled = mainOptionsChecked.some((mainOption) => element.haveOption(mainOption)) const otherCategoriesFilled = App.taxonomyModule.otherRootCategories.every((category) => this.recursivelyCheckInCategory(category, element), ) return mainCategoryFilled && otherCategoriesFilled } else { const mainOption = App.taxonomyModule.getCurrMainOption() const mainOptionFilled = this.recursivelyCheckInOption(mainOption, element) const otherCategoriesFilled = App.taxonomyModule.otherRootCategories.every((category) => this.recursivelyCheckInCategory(category, element), ) return mainOptionFilled && otherCategoriesFilled } } private recursivelyCheckInOption(option: Option, element: Element): boolean { if (this.log) console.log(element.name, 'Check for option ', option.name) let result if (option.subcategories.length == 0 || (option.isDisabled && !option.isMainOption)) { if (this.log) console.log('No subcategories ') result = !option.isDisabled && element.haveOption(option) } else { result = option.subcategories.every((category) => this.recursivelyCheckInCategory(category, element)) } if (this.log) console.log('Return ', result) return result } private recursivelyCheckInCategory(category: Category, element: Element): boolean { if (this.log) console.log('--' + 'Category', category.name) if (!category.useForFiltering) return true const activeOptions = category.activeOptions let elementOptions = element.getOptionValueByCategoryId(category.id) if (App.config.menu.showOnePanePerMainOption) elementOptions = elementOptions.filter((optValue) => optValue.optionId != App.currMainId) // if this element don't have any option in this category if (elementOptions.length == 0) { if (this.log) console.log('--' + "Element don't have options in this category. Category checked ? ", category.isChecked) return category.isChecked } const isSomeOptionInCategoryActiveOptions = elementOptions.some( (optionValue) => activeOptions.indexOf(optionValue.option) > -1, ) if (this.log) console.log('--' + 'isSomeOptionInCategoryActiveOptions', isSomeOptionInCategoryActiveOptions) if (isSomeOptionInCategoryActiveOptions) return true else { if (this.log) console.log('--' + 'So we checked in suboptions', category.name) return elementOptions.some((optionValue) => this.recursivelyCheckInOption(optionValue.option, element)) } } }