<template lang="pug">
TLayout(
:syncWithVuex="false"
:sidebar="true"
:menu="true"
:menuCollapsed="menuCollapsed"
:menuFullCollapse="true"
:menuToggle="true"
:rightMenu="true"
:rightMenuBottom="mode==='table_map'"
)
template(v-slot:sidebar)
Sidebar
template(v-slot:menu)
SearchWrapper(
@search-input="onSearchModuleSelectionChange"
@search-view-change="onSearchModuleViewChange"
@search-clear="onSearchModuleClearSelection"
)
template(v-slot:search-module-filters)
//Main search filters (event types categories and custom information)
.row.p-0.m-0
.col-12.m-0.mt-2.p-0(v-show="operationAnomaliesTypes.length>0")
.filter_item
label.filter-label
span {{$t('events.search_filters.anomaly_title')}}
em.fas.fa-info(:title="$t('events.search_filters.anomaly_title')+' ('+$t('events.filters_tootip')+')'" v-b-tooltip.hover.right style="padding-left: 5px;")
b-form-select(v-model="selectedAnomaliesTypes" multiple :options="operationAnomaliesTypes" size="sm"
style="resize: vertical;"
)
.col-12.m-0.mt-2.p-0(v-show="operationMessageTypes.length>0")
.filter_item
label.filter-label
span {{$t('events.search_filters.operation_message_title')}}
em.fas.fa-info(:title="$t('events.search_filters.operation_message_title')+' ('+$t('events.filters_tootip')+')'" v-b-tooltip.hover.right style="padding-left: 5px;")
b-form-select(v-model="selectedOperationMessageTypes" multiple :options="operationMessageTypes" size="sm"
style="resize: vertical;"
)
.col-12.m-0.mt-2.p-0(v-show="operationStatusTypes.length>0")
.filter_item
label.filter-label
span {{$t('events.search_filters.status_title')}}
em.fas.fa-info(:title="$t('events.search_filters.status_title')+' ('+$t('events.filters_tootip')+')'" v-b-tooltip.hover.right style="padding-left: 5px;")
b-form-select(v-model="selectedStatusTypes" multiple :options="operationStatusTypesComputed" size="sm"
style="resize: vertical;"
)
//.col-12.m-0.mt-2.p-0
.filter_item
label.filter-label
span {{$t('events.search_filters.extra_information_title')}}
//em.fas.fa-info(:title="$t('events.search_filters.extra_information_title')" v-b-tooltip.hover.right style="padding-left: 5px;")
.radio
input(type="checkbox" name="extraInfo" v-model="isAggregateEventGroupBinEnabled" @click="e=>aggregateEventGroupSelect(e,'bin')" )
label.pl-1 {{$t('events.search_filters.bin_group_label')}}
.radio
input(type="checkbox" name="extraInfo" v-model="isAggregateEventGroupRoundEnabled" @click="e=>aggregateEventGroupSelect(e,'round')")
label.pl-1 {{$t('events.search_filters.round_group_label')}}
template(v-slot:search-module-results)
.row.m-0.p-0(v-show="listItems.length===0&&!$store.state.search_module.isSearchInProgress")
.col-12
span {{$t('events.no_results')}}
EventsListToolbar.mb-2(v-show="listItems.length>0"
@map="()=>onToolbarAction({action: 'map',type: 'total'})"
@table="()=>onToolbarAction({action: 'table',type: 'total'})"
@markers="()=>onToolbarAction({action: 'markers',type: 'total'})"
)
EventsList(v-show="listItems.length>0" :items="listItems"
:highlightedSelectionId="highlightedSelectionId"
@toolbar="onToolbarAction"
ref="il1"
)
template(v-slot:right_menu)
div.table-layout(v-if="mode==='table'")
EventsTable(:items="detailedItems"
:mode="mode"
@mode="switchMode"
:headerText="tableHeaderText"
:showRoundColumns="isAggregateEventGroupRoundEnabled"
:showBinColumns="isAggregateEventGroupBinEnabled"
)
LocationEventsMap(v-if="mode==='table_map'||mode==='list'" ref="map")
template(v-slot:right_menu_bottom)
EventsTable(v-if="mode==='table_map'"
:mode="mode"
:items="detailedItems"
:headerText="tableHeaderText"
@mode="switchMode"
:showRoundColumns="isAggregateEventGroupRoundEnabled"
:showBinColumns="isAggregateEventGroupBinEnabled"
)
</template>
<script>
import Vue from 'vue'
import EventsTable from '@/components/events/EventsTable.vue'
import LocationEventsMap from '@/components/location/LocationEvents/LocationEventsMap.vue'
import EventsList from '@/components/events/EventsList.vue'
import EventsListToolbar from '@/components/events/EventsListToolbar.vue'
import Sidebar from '@c/shared/Sidebar/Sidebar.vue'
import TLayout from '@c/shared/TLayout/TLayout.vue'
import SearchWrapper from '@c/shared/SearchModule/SearchWrapper/SearchWrapper.vue'
import { mapGetters } from 'vuex'
import { getEventsSearchSelectionLimit } from '@/services/search-service.js'
import { getQueryStringValue } from '@/utils/querystring'
import moment from 'moment'
import i18n from '@/i18n'
import {
fetchEvents,
fetchEventsCumulation,
getEventTypesCategories,
} from '@/services/events-service.js'
import { getEnvValue } from '@/services/env-service.js'
import { normalizeString } from '@/utils/string.js'
const canGroupEventsByAll = getEnvValue('events_enable_group_all', '1') === '1' //Allow user to select bin and round (group=all) (Enabled by default)
export const EventsModuleIsolatedConfig = [
'EventsModule',
() =>
/* webpackChunkName: "testing" */
import('@/views/EventsModule.vue'),
{
props: {
//Custom props
},
beforeEnter: (to, from, next) => {
//Custom logic before routing in
return !!next && next()
},
},
]
function eventsFilterMixin() {
return {
data() {
return {
//Filters data
operationMessageTypes: [],
operationAnomaliesTypes: [],
operationStatusTypes: [],
//Custom search filters
selectedOperationMessageTypes: [],
selectedAnomaliesTypes: [],
selectedStatusTypes: [],
}
},
computed: {
isAggregateEventGroupBinEnabled() {
return this.$store.getters['settings/getParameter'](
'isAggregateEventGroupBinEnabled'
)
},
isAggregateEventGroupRoundEnabled() {
return this.$store.getters['settings/getParameter'](
'isAggregateEventGroupRoundEnabled'
)
},
selectedAggregateEventGroups() {
if (
this.isAggregateEventGroupBinEnabled &&
this.isAggregateEventGroupBinEnabled &&
canGroupEventsByAll
) {
return 'all'
}
if (this.isAggregateEventGroupBinEnabled) return 'bin'
if (this.isAggregateEventGroupRoundEnabled) return 'round'
return ''
},
/**
* Translate label on the fly
*/
operationStatusTypesComputed() {
return this.operationStatusTypes.map((item) => {
return {
value: item.value,
text: getI18nLabelForEventTypeCategoryLabel(
item.text,
'event_types.status.'
),
}
})
},
},
methods: {
/* aggregateEventGroupSelect(e, type) {
if (e.target.value && type === 'round' && !canGroupEventsByAll) {
this.isAggregateEventGroupBinEnabled = false
}
if (e.target.value && type === 'bin' && !canGroupEventsByAll) {
this.isAggregateEventGroupRoundEnabled = false
}
}, */
},
created() {
getEventTypesCategories((dataSubset) => {
const transformEventTypeCategoryItem = (item) => ({
value: item.code,
text: item.label,
})
dataSubset.forEach((item) => {
switch (item.type) {
case 'operation_message':
this.operationMessageTypes.push(
transformEventTypeCategoryItem(item)
)
break
case 'anomaly':
this.operationAnomaliesTypes.push(
transformEventTypeCategoryItem(item)
)
break
case 'status':
this.operationStatusTypes.push(
transformEventTypeCategoryItem(item)
)
break
default:
break
}
})
/*
//Generate a JSON with missing keys
let json = {}
this.operationAnomaliesTypes.forEach((item) => {
let i18nCode = getI18nLabelForEventTypeCategoryLabel(item.text, 'event_types.anomaly.',true)
let exists = i18n.te(i18nCode)
if (!exists) {
json[i18nCode] = ''
}
})
console.warn('Missing event types operationAnomaliesTypes i18n codes')
console.log(JSON.stringify(json, null, 4))*/
})
},
}
}
function getI18nLabelForEventTypeCategoryLabel(
text,
prefix,
returnCodeInstead = false
) {
const input = text
const options = {
replaceSpaces: true,
convertToLowercase: true,
removeParentheses: true,
removeAccents: true,
prefix,
}
const i18nCode = normalizeString(input, options)
if (returnCodeInstead) {
return i18nCode
}
const exists = i18n.te(i18nCode)
if (exists) {
//console.warn('i18n code found', i18nCode, text, i18n.t(i18nCode))
return i18n.t(i18nCode)
} else {
//console.warn('i18n code not found', i18nCode, text)
return text
}
}
/**
* Computes "tableHeaderText"
*/
function tableHeaderMixin() {
return {
data() {
return {
//Necessary to compute table header text
dateFromFormatted: '',
dateToFormatted: '',
//detailedItemsBy: external
//detailedItemsByItem: external
}
},
computed: {
tableHeaderText() {
if (this.detailedItemsBy === 'total') {
return this.$t('events.table.header_text.total', {
fromDate: this.dateFromFormatted,
toDate: this.dateToFormatted,
})
}
if (this.detailedItemsBy === 'date') {
return this.$t('events.table.header_text.date', {
date: this.detailedItemsByItem.dateItem.label,
})
}
if (this.detailedItemsBy === 'date-aggregate') {
return this.$t('events.table.header_text.date_aggregate', {
date: this.detailedItemsByItem.dateItem.label,
type: this.detailedItemsByItem.eventGroup.type,
code: this.detailedItemsByItem.eventGroup.code,
})
}
if (
this.detailedItemsBy === 'subitem' &&
this.activeSearchFormTabName === 'vehicle'
) {
return this.$t('events.table.header_text.vehicle', {
vehicleName: this.detailedItemsByItem.subItem.label,
date: this.detailedItemsByItem.dateItem.label,
})
}
if (
this.detailedItemsBy === 'subitem' &&
this.activeSearchFormTabName === 'circuit'
) {
return this.$t('events.table.header_text.circuit', {
circuitName: this.detailedItemsByItem.subItem.label,
date: this.detailedItemsByItem.dateItem.label,
})
}
if (
this.detailedItemsBy === 'aggregate' &&
this.activeSearchFormTabName === 'vehicle'
) {
return this.$t('events.table.header_text.vehicle_aggregate', {
vehicleName: this.detailedItemsByItem.subItem.label,
date: this.detailedItemsByItem.dateItem.label,
type: this.detailedItemsByItem.eventGroup.type,
code: this.detailedItemsByItem.eventGroup.code,
})
}
if (
this.detailedItemsBy === 'aggregate' &&
this.activeSearchFormTabName === 'circuit'
) {
return this.$t('events.table.header_text.circuit_aggregate', {
circuitName: this.detailedItemsByItem.subItem.label,
date: this.detailedItemsByItem.dateItem.label,
type: this.detailedItemsByItem.eventGroup.type,
code: this.detailedItemsByItem.eventGroup.code,
})
}
return ''
},
},
methods: {
/**
* Uses this.items (external)
*/
getVehicleName(id) {
return (
((this.items || []).find((i) => i.vehicleId == id) || {})
.vehicleName || ''
)
},
getCircuitName(vehicleId, circuitId) {
let synthesisVehicleItem =
(this.items || []).find((i) => i.vehicleId == vehicleId) || {}
let circuitSynthesis =
(
synthesisVehicleItem.circuits ||
synthesisVehicleItem.synthesis ||
[]
).find((s) => s.circuitId == circuitId) || {}
return circuitSynthesis.circuitName || ''
},
},
}
}
export default {
name: 'EventsModule',
componentType: 'container',
components: {
SearchWrapper,
TLayout,
EventsTable,
LocationEventsMap,
EventsList,
EventsListToolbar,
Sidebar,
},
mixins: [
eventsFilterMixin(),
tableHeaderMixin(),
Vue.$mixins.userRightsMixin,
],
provide() {
let self = this
let searchFormTabs = ['disabled']
if (
this.hasFeatureRight('events_search_vehicle') ||
(getQueryStringValue('test') === '1' && !this.$env.isProduction())
) {
searchFormTabs.push({
label: i18n.t(`common.Véhicule`),
value: 'vehicle',
})
}
if (
this.hasFeatureRight('events_search_circuit') ||
(getQueryStringValue('test') === '1' && !this.$env.isProduction())
) {
searchFormTabs.push({
label: i18n.t(`common.Circuit`),
value: 'circuit',
})
}
if (searchFormTabs.length > 1) {
searchFormTabs.splice(0, 1)
}
return {
showCustomFiltersByDefault: false,
/**
* Provide handler to react to date picker predefined options change and limit date range selection if necessary
* @function onSearchModuleDatePickerPredefinedSelectionClick
* @see SMDatePicker.vue Date picker will inject this handler
*/
onSearchModuleDatePickerPredefinedSelectionClick({
length,
limitDateSelectionRangeToNDays,
}) {
let selectedElementsIds = self.$store.getters[
'search_module/getSelectedIdsByType'
](self.activeSearchFormTabName)
let willCurrentDateSelectionExceedSelectionLimit =
selectedElementsIds.length * length > getEventsSearchSelectionLimit()
if (willCurrentDateSelectionExceedSelectionLimit) {
window.alert(
`La sélection pour la recherche a été réduite à ${getEventsSearchSelectionLimit()} éléments`
)
let maxDateRangeLength = Math.round(
(getEventsSearchSelectionLimit() - 1) / selectedElementsIds.length -
1
)
limitDateSelectionRangeToNDays(maxDateRangeLength)
}
},
searchModuleCanToggleFreesearch: false,
searchFormTabs,
}
},
data() {
return {
mode: 'list',
menuCollapsed: false,
//search selection
elementIdsUsedDuringLastSearch: [],
datesUsedDuringLastSearch: [],
//Synthesis dataset (see: event-service.js:fetchEventsCumulation)
items: [],
isFetchDetailsInProgress: false,
//detailedItems (computed) (use can choose to view table data at different levels)
detailedItemsBy: 'total', //total/date/date-aggregate/subitem/aggregate
detailedItemsByItem: null, //metadata {dateItem:{},subItem:{},eventGroup:{}}
detailedItems: [],
}
},
computed: {
...mapGetters({
activeSearchFormTabName: 'search_module/activeSearchFormTabName',
selection: 'search_module/getSelection',
}),
listItems() {
return [...this.items].sort((a, b) =>
a.timestamp < b.timestamp ? 1 : -1
)
},
/**
* We highlight the current selection (date/date-aggregate/subitem/aggregate)
* @todo Support date/date-aggregate type
*/
highlightedSelectionId() {
let dateId = this.detailedItemsByItem?.dateItem?.id || 'x'
let subItemId = this.detailedItemsByItem?.subItem?.id || 'x' //vehicle/circuit
let aggregateId = this.detailedItemsByItem?.eventGroup?.id || 'x'
return `${this.detailedItemsBy}_${dateId}_${subItemId}_${aggregateId}`
},
},
watch: {
isAggregateEventGroupBinEnabled() {
this.forceTableReload()
},
isAggregateEventGroupRoundEnabled() {
this.forceTableReload()
},
/* aggregateSettingsUpdated() {
let currentmode = this.mode
console.log('aggregateSettingsUpdated', currentmode)
if (this.mode != 'list') {
this.switchMode('list')
this.switchMode(currentmode)
}
}, */
/**
* If table-map/list mode, show markers in the map as soon as data is ready.
*/
detailedItems() {
if (this._prepareMarkersTimeout) {
clearTimeout(this._prepareMarkersTimeout)
}
this._prepareMarkersTimeout = setTimeout(() => {
this.prepareMapMarkers()
clearTimeout(this._prepareMarkersTimeout)
}, 500)
},
/**
* Toggle sidebar result view on/off (list)
*/
items() {
if (this.items.length === 0) {
this.$store.dispatch('search_module/setHasResults', false)
} else {
if (this.$store.getters['search_module/hasResults']) {
this.$store.dispatch('search_module/setHasResults', false)
this.$nextTick(() =>
this.$store.dispatch('search_module/setHasResults', true)
)
} else {
this.$store.dispatch('search_module/setHasResults', true)
}
}
},
},
mounted() {
//By default, map is empty
this.clearMapMarkers()
},
destroyed() {
this.$store.dispatch('simpliciti_map/resetStore', null)
this.$store.dispatch('search_module/resetStore')
},
methods: {
forceTableReload(interval = 250) {
let currentmode = this.mode
if (this.mode !== 'list') {
console.log(currentmode)
this.switchMode('list')
//Must wait nextTick finish before set new switchmode
setTimeout(() => {
this.switchMode(currentmode)
}, interval)
}
},
getFetchEventOptions() {
let elementIds = []
let dates = []
let filters = {}
if (this.selectedAggregateEventGroups) {
filters.group = this.selectedAggregateEventGroups
}
if (this.detailedItemsBy === 'total') {
dates = this.datesUsedDuringLastSearch
elementIds = this.elementIdsUsedDuringLastSearch
}
if (this.detailedItemsBy === 'date') {
dates = [this.detailedItemsByItem.dateItem.id]
elementIds = this.elementIdsUsedDuringLastSearch
}
if (this.detailedItemsBy === 'date-aggregate') {
/* Event by date grouped by code/type */
dates = [this.detailedItemsByItem.dateItem.id]
elementIds = this.elementIdsUsedDuringLastSearch
filters.type = this.detailedItemsByItem.eventGroup.type
filters.code = this.detailedItemsByItem.eventGroup.code
}
if (this.detailedItemsBy === 'subitem') {
/* Vehicle */
dates = [this.detailedItemsByItem.dateItem.id]
elementIds = [this.detailedItemsByItem.subItem.id]
}
if (['total', 'date', 'subitem'].includes(this.detailedItemsBy)) {
filters.code = [
...this.selectedOperationMessageTypes,
...this.selectedAnomaliesTypes,
...this.selectedStatusTypes,
]
}
if (this.detailedItemsBy === 'aggregate') {
/* Event by code/type */
dates = [this.detailedItemsByItem.dateItem.id]
elementIds = [this.detailedItemsByItem.subItem.id]
filters.type = this.detailedItemsByItem.eventGroup.type
filters.code = this.detailedItemsByItem.eventGroup.code
}
dates = dates
.map((range) => (range instanceof Array ? range[0] : range))
.sort((a, b) =>
(a._d || a).getTime() < (b._d || b).getTime() ? 1 : -1
)
if (
this.selectedAnomaliesTypes.length === 0 &&
this.selectedStatusTypes.length === 0 &&
this.selectedOperationMessageTypes.length === 0 &&
filters.type === undefined
) {
filters.type = 0
}
return {
elementIds,
dates,
filters,
}
},
/**
* Fetch events
* The user can click:
* - Buttons in the list - top toolbar (i.g table mode)
* - Buttons in the list - date label (i.g table mode)
* - Buttons in the list - date label -> event code/type (i.g table mode)
* @param {*} options
*/
async fetchEvents() {
let options = this.getFetchEventOptions()
let { elementIds, dates, filters } = options
this.detailedItems = []
if (this.isFetchDetailsInProgress) {
this.$log.warn('fetchEvents::skip (in progress)')
return
} else {
this.isFetchDetailsInProgress = true
}
fetchEvents({
elementType: this.activeSearchFormTabName,
elementIds,
dates,
filters,
handleResults: (results) => {
this.detailedItems = [...this.detailedItems, ...results]
},
}).finally(() => {
this.isFetchDetailsInProgress = false
})
},
/**
* There are three possible toolbars actions:
* map: Switch to table+map
* table: Siwtch to table
*/
async onToolbarAction(options) {
this.detailedItemsBy = options.type
this.detailedItemsByItem = options.item
//Async
this.fetchEvents().finally(() => {
this.isFetchDetailsInProgress = false
})
//Switch layout to map/table
if (['map', 'table'].includes(options.action)) {
this.switchMode(
{
map: 'table_map',
table: 'table',
}[options.action]
)
}
},
async clearMapMarkers() {
await this.$store.dispatch('simpliciti_map/setDataset', {
type: 'eventsMarkers',
data: [],
})
},
prepareMapMarkers() {
//Limit map rendering
if (this.detailedItems.length > 5000) {
this.$store.dispatch('alert/addAlert', {
type: 'info',
title: this.$t('identification.map.render_limit_message_title'),
text: this.$t('identification.map.render_limit_message_body', {
length: this.detailedItems.length,
}),
//text: `Affichage de 5000 éléments sur les ${this.detailedItems.length} résultats`,
})
}
this.$store.dispatch('simpliciti_map/setDataset', {
type: 'eventsMarkers',
data: this.detailedItems.slice(0, 5000),
})
},
/**
* Fetch synthesis dataset
* - Splits requests by vehicle/date (1-1)
*/
async performsSearch() {
this.clearMapMarkers()
let dateRanges = this.selection.selectedDateRanges
//vehiclers or circuits
let selectedElementsIds = this.$store.getters[
'search_module/getSelectedIdsByType'
](this.activeSearchFormTabName)
//Test: Predefined selection (berto provence)
if (getQueryStringValue('test') === '1' && !this.$env.isProduction()) {
selectedElementsIds = [6447, 29206, 42701]
dateRanges = [
[
moment('2022-04-29T00:00:00Z').minute(0).hour(0).second(0),
moment('2022-04-29T00:00:00Z').minute(59).hour(23).second(59),
],
]
}
//Validation: Limit vehicles selection
if (
selectedElementsIds.length * (dateRanges.length || 1) >
getEventsSearchSelectionLimit()
) {
this.$alertPopup.showSelectionLimitWarning(
selectedElementsIds.length * (dateRanges.length || 1),
getEventsSearchSelectionLimit()
)
return
}
this.items = [] //This will also triger the results view
let len = dateRanges.length
if (len === 0) {
dateRanges = [
[
moment().hour(0).minute(0).second(0)._d,
moment().hour(23).minute(59).second(59)._d,
],
]
len = dateRanges.length
}
this.$store.state.search_module.isSearchInProgress = true
this.$loader && this.$loader.show()
let dateFrom = dateRanges[0][0]
let dateTo = dateRanges[len - 1][1]
this.dateFromFormatted = moment(dateFrom).isValid()
? moment(dateFrom).format('DD/MM/YYYY HH:mm:ss')
: ''
this.dateToFormatted = moment(dateTo).isValid()
? moment(dateTo).format('DD/MM/YYYY HH:mm:ss')
: ''
let dates = dateRanges
.map((range) => range[0])
.sort((a, b) => (a.getTime() < b.getTime() ? 1 : -1))
this.elementIdsUsedDuringLastSearch = selectedElementsIds
this.datesUsedDuringLastSearch = dates
//TODO: handle pagination using pooling or split requests by elements
this.items = await fetchEventsCumulation(selectedElementsIds, dates, {
elementType: this.activeSearchFormTabName,
filters: {
...(this.selectedAnomaliesTypes.length === 0 &&
this.selectedStatusTypes.length === 0 &&
this.selectedOperationMessageTypes.length === 0
? {
type: 0,
}
: {}),
//We want to filter by event code (agregat.event.code equals ref.anomalies.code)
code: this.selectedAnomaliesTypes
.concat(this.selectedStatusTypes)
.concat(this.selectedOperationMessageTypes),
},
})
//Switch to results (even if no results from API, user should see the message indicating no results)
this.$nextTick(() =>
this.$store.dispatch('search_module/setHasResults', true)
)
//Hide loader in search module search button
this.$store.state.search_module.isSearchInProgress = false
//Hide big loader
this.$loader && this.$loader.hide()
},
async onSearchModuleSelectionChange() {
this.performsSearch()
},
onSearchModuleViewChange(view) {
//React to search module view changes (selection / results)
if (view === 'selection') {
this.mode = 'list'
}
},
onSearchModuleClearSelection() {
//Clear custom filters
this.selectedOperationMessageTypes = []
this.selectedAnomaliesTypes = []
this.selectedStatusTypes = []
},
switchMode(mode) {
this.$nextTick(() => {
this.mode = mode
if (this.mode === 'list') {
this.menuCollapsed = false
} else {
this.menuCollapsed = true
}
})
},
},
}
</script>
<style lang="scss" scoped>
.table-layout {
height: calc(100% - 20px);
}
.filter-label {
font: normal normal normal 11px/14px Open Sans;
letter-spacing: 0px;
color: var(--color-tundora);
margin: 0px;
}
.radio label {
font: normal normal normal 11px/14px Open Sans;
letter-spacing: 0px;
color: var(--color-tundora);
margin: 0px;
}
</style>
Source