<template lang="pug">
TLayout(
class="identificationModule"
: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)
.row.p-0.m-0
//.col-12.m-0.mt-2
// .filter-title {{$t('identification.filters_puce_title')}}
.col-12.p-0.m-0
.row.p-0.m-0
.col-6.m-0
.filter-item
label.filter-label {{$t('identification.filters_puce_title')}}
b-form-input(class="puceNumberInput" v-model="puceFilterValue" :placeholder="$t('identification.filters_puce_input_placeholder')" size="sm")
.col-12.m-0.mt-2
.filter-title {{$t('identification.filters_title')}}
.col-12.p-0.m-0
.row.p-0.m-0
.col-6.m-0
.filter-item
label.filter-label {{$t('identification.filters_is_identified')}}
b-form-select(v-model="identifiedFilterValue" :options="filterOptions" size="sm")
.col-6.m-0
.filter-item
label.filter-label {{$t('identification.filters_is_stopped')}}
b-form-select(v-model="stoppedFilterValue" :options="filterOptions" size="sm")
.col-6.m-0
.filter-item
label.filter-label {{$t('identification.filters_is_high_point')}}
b-form-select(v-model="highPointFilterValue" :options="filterOptionsHighPoint" size="sm")
.col-6.m-0
.filter-item
label.filter-label {{$t('identification.filters_is_blacklist')}}
b-form-select(v-model="blacklistFilterValue" :options="filterOptions" size="sm")
template(v-slot:search-module-results)
.row.m-0.p-0(v-show="sortedItems.length===0&&!$store.state.search_module.isSearchInProgress")
.col-12
span {{$t('identification.no_results')}}
IdentificationListToolbar.mb-2(v-show="sortedItems.length>0" @map="executeGlobalToolbarAction('map')"
@table="executeGlobalToolbarAction('table')"
@markers="executeGlobalToolbarAction('markers')"
)
IdentificationList(v-show="sortedItems.length>0" :items="sortedItems"
:highlightedSelectionId="highlightedSelectionId"
@toolbar="onToolbarAction"
ref="il1"
)
template(v-slot:right_menu)
div.table-layout(v-if="mode==='table'")
IdentificationTable(:items="detailedItems"
:mode="mode"
@mode="switchMode"
:headerText="tableHeaderText"
)
LocationIdentBacsMap(v-if="mode==='table_map'||mode==='list'" ref="map")
template(v-slot:right_menu_bottom)
IdentificationTable(v-if="mode==='table_map'"
:mode="mode"
:items="detailedItems"
:headerText="tableHeaderText"
@mode="switchMode"
ref="il2"
)
</template>
<script>
import Vue from 'vue'
import IdentificationTable from '@c/identification/IdentificationTable.vue'
import LocationIdentBacsMap from '@c/location/LocationIdentBacs/LocationIdentBacsMap.vue'
import IdentificationList from '@c/identification/IdentificationList.vue'
import IdentificationListToolbar from '@c/identification/IdentificationListToolbar.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 { getIdentificationSearchSelectionLimit } from '@/services/search-service.js'
import moment from 'moment'
import { splitOperation } from '@/utils/promise.js'
import i18n from '@/i18n'
import {
fetchLeveesSynthesis,
fetchLeveesDetails,
createContainerHelpersMixin,
} from '@/services/identification-service.js'
export default {
name: 'SensorsModule',
componentType: 'container',
components: {
SearchWrapper,
TLayout,
IdentificationTable,
LocationIdentBacsMap,
IdentificationList,
IdentificationListToolbar,
Sidebar,
},
mixins: [createContainerHelpersMixin(), Vue.$mixins.userRightsMixin],
provide() {
let self = this
let searchFormTabs = ['disabled']
if (this.hasFeatureRight('identification_search_vehicle')) {
searchFormTabs.push({
label: i18n.t(`common.Véhicule`),
value: 'vehicle',
})
}
if (this.hasFeatureRight('identification_search_circuit')) {
searchFormTabs.push({
label: i18n.t(`common.Circuit`),
value: 'circuit',
})
}
if (searchFormTabs.length > 1) {
searchFormTabs.splice(0, 1)
}
return {
/**
* 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 >
getIdentificationSearchSelectionLimit()
if (willCurrentDateSelectionExceedSelectionLimit) {
window.alert(
`La sélection pour la recherche a été réduite à ${getIdentificationSearchSelectionLimit()} éléments`
)
let maxDateRangeLength = Math.round(
(getIdentificationSearchSelectionLimit() - 1) /
selectedElementsIds.length -
1
)
limitDateSelectionRangeToNDays(maxDateRangeLength)
}
},
searchModuleCanToggleFreesearch: false,
searchFormTabs,
}
},
data() {
let filterOptions = [
{ text: this.$t('identification.filters_value_yes'), value: 1 },
{ text: this.$t('identification.filters_value_no'), value: 0 },
{ text: this.$t('identification.filters_value_tous'), value: '' },
]
let filterOptionsHighPoint = [
...filterOptions,
{ text: this.$t('identification.filters_value_unknown'), value: 'nc' },
]
return {
mode: 'list',
menuCollapsed: false,
//Synthesis dataset
items: [
/**{
date,
dateFormatted
vehicleId,
vehicleName: getNestedValue(item, "vehicule_nom"),
vehicleImmatriculation: getNestedValue(
item,
"vehicule_immatriculation"
),
vehicleCategory: getNestedValue(item, "categorie_nom"),
//Synthesis by circuit
circuits,
} */
],
//Levee details dataset (segregated by circuit)
detailsGroupedByCircuit: [
/** {
dateFormatted: subset.dateFormatted,
date: subset.date,
vehicleId: subset.vehicleId,
circuitId: subset.circuitId,
items: results,
} */
],
isFetchDetailsInProgress: false,
//detailedItems (computed) (use can choose to view table data at different levels)
detailedItemsBy: 'total', //total/date/vehicle/circuit
detailedItemsByDateValue: null,
detailedItemsByVehicleIdValue: null,
detailedItemsByCircuitIdValue: null,
//Custom search filters
puceFilterValue: '',
identifiedFilterValue: '',
stoppedFilterValue: '',
highPointFilterValue: '',
blacklistFilterValue: '',
filterOptions,
filterOptionsHighPoint,
}
},
computed: {
...mapGetters({
activeSearchFormTabName: 'search_module/activeSearchFormTabName',
selection: 'search_module/getSelection',
}),
/**
* Dataset for table mode
* The user can choose to view levees details data in table mode at different levels (total/date/vehicle/circuit)
*/
detailedItems() {
if (this.detailedItemsBy === 'total') {
return this.detailsGroupedByCircuit
.map((g) => g.items)
.reduce((a, v) => a.concat(v), [])
}
if (this.detailedItemsBy === 'date') {
return this.detailsGroupedByCircuit
.filter((g) => {
return g.dateFormatted == this.detailedItemsByDateValue
})
.map((g) => g.items)
.reduce((a, v) => a.concat(v), [])
}
if (this.detailedItemsBy === 'vehicle') {
return this.detailsGroupedByCircuit
.filter((g) => {
return (
g.dateFormatted == this.detailedItemsByDateValue &&
g.vehicleId == this.detailedItemsByVehicleIdValue
)
})
.map((g) => g.items)
.reduce((a, v) => a.concat(v), [])
}
if (this.detailedItemsBy === 'circuit') {
return this.detailsGroupedByCircuit
.filter((g) => {
let areCircuitsSame =
(g.circuitId === 0 &&
this.detailedItemsByCircuitIdValue === '') ||
g.circuitId == this.detailedItemsByCircuitIdValue
return (
g.dateFormatted == this.detailedItemsByDateValue &&
g.vehicleId == this.detailedItemsByVehicleIdValue &&
areCircuitsSame
)
})
.map((g) => g.items)
.reduce((a, v) => a.concat(v), [])
}
return []
},
/**
* Used to fetch details from a subset (total/vehicle/circuit)
*/
filteredSynthesisItems() {
if (this.detailedItemsBy === 'total') {
return this.items
}
if (this.detailedItemsBy === 'date') {
return this.items.filter(
(item) => item.dateFormatted === this.detailedItemsByDateValue
)
}
if (this.detailedItemsBy === 'vehicle') {
return this.items.filter(
(item) =>
item.dateFormatted === this.detailedItemsByDateValue &&
item.vehicleId == this.detailedItemsByVehicleIdValue
)
}
if (this.detailedItemsBy === 'circuit') {
return this.items.filter(
(item) =>
item.dateFormatted === this.detailedItemsByDateValue &&
item.vehicleId == this.detailedItemsByVehicleIdValue &&
(!this.detailedItemsByCircuitIdValue ||
!!item.circuits.find(
(circ) => circ.circuitId == this.detailedItemsByCircuitIdValue
))
)
}
return []
},
sortedItems() {
return [...this.items].sort((a, b) =>
a.timestamp < b.timestamp ? 1 : -1
)
},
/**
* We highlight the current selection (date/date-vehicle/date-vehicle-circuit)
*/
highlightedSelectionId() {
return `${this.detailedItemsBy}_${this.detailedItemsByDateValue || 'x'}_${
this.detailedItemsByVehicleIdValue || 'x'
}_${this.detailedItemsByCircuitIdValue || 'x'}`
},
isCurrentSelectionGreaterThanLimit() {
let dateRanges = this.selection.selectedDateRanges
let selectedElementsIds = this.$store.getters[
'search_module/getSelectedIdsByType'
](this.activeSearchFormTabName)
return (
selectedElementsIds.length * dateRanges.length >
getIdentificationSearchSelectionLimit()
)
},
},
watch: {
/**
* If table map mode, show markers in the map as soon as data is ready.
*/
detailedItems() {
this.prepareMapMarkers()
},
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() {
this.clearMapMarkers()
this.$store.state.search_module.isSearchInProgress = false //Reset if search is in progress in a previous module
},
destroyed() {
this.$store.dispatch('simpliciti_map/resetStore', null)
},
methods: {
executeGlobalToolbarAction(name) {
this.onToolbarAction({
action: name,
type: 'total',
})
},
/**
* There are three possible toolbars actions:
* map: Switch to table+map
* table: Siwtch to table
*/
async onToolbarAction(options) {
this.detailedItemsBy = options.type
this.detailedItemsByDateValue = options.dateFormatted
this.detailedItemsByVehicleIdValue = options.vehicleId
this.detailedItemsByCircuitIdValue = options.circuitId
if (['map', 'table'].includes(options.action)) {
this.switchModeAtLevel(options)
}
if (options.action === 'markers') {
await this.clearMapMarkers()
this.$loader.show()
let result = await this.fetchDetailsUsingSynthesisDataset({
//Also render on the map as soon as data becomes available
onDataAvailable: () => {
//Wait "detailedItems" to be computed
//this.$nextTick(() => {
//this.prepareMapMarkers();
//});
},
})
//Use case: If the user click the loupe/markers-icon twice, the data is already cached and prepareMapMarkers is not called because watch is not triggered
if (result.length === 0) {
this.prepareMapMarkers()
}
console.log('onToolbarAction::markers', {
result,
})
this.$loader.hide()
//this.$nextTick(() => {
//this.prepareMapMarkers();
//});
}
},
/**
* Will trigger a layout change (state.mode) to table_map or table depending on options.action
* @param {Object} options.action map/table
*/
switchModeAtLevel(options = {}) {
this.$nextTick(() => {
this.switchMode(
{
map: 'table_map',
table: 'table',
}[options.action]
)
})
},
async clearMapMarkers() {
await this.$store.dispatch('simpliciti_map/setDataset', {
type: 'identificationBacsMarkers',
data: [],
})
},
prepareMapMarkers() {
console.log('prepareMapMarkers', {
count: this.detailedItems.length,
})
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`,
})
}
//Wait for vue to compute detailedItems
this.$nextTick(() => {
this.$store.dispatch('simpliciti_map/setDataset', {
type: 'identificationBacsMarkers',
data: this.detailedItems.slice(0, 5000),
})
})
},
/**
* This will load details dataset for table/map
*
* It will use computed 'filteredSynthesisItems' to fetch a subset, optionally (i.g: The user clicks to show markers from a circuit on the map)
*/
async fetchDetailsUsingSynthesisDataset(options = {}) {
return new Promise((resolve, reject) => {
let self = this
if (this.isFetchDetailsInProgress) {
this.$log.warn(
'fetchDetailsUsingSynthesisDataset::skip (in progress)'
)
return
} else {
this.isFetchDetailsInProgress = true
}
console.log('fetchDetailsUsingSynthesisDataset::splitOperation')
splitOperation({
sequential: true,
generateSubsets() {
let subsets = []
self.filteredSynthesisItems.forEach((item) => {
item.circuits.forEach((synthesisItem) => {
if (
!self.detailsGroupedByCircuit.find((grouped) => {
return (
grouped.dateFormatted == item.dateFormatted &&
grouped.vehicleId == item.vehicleId &&
grouped.circuitId == synthesisItem.circuitId
)
})
) {
//Filter a single circuit (If user click a toolbar action at circuit level)
if (
//Is filter by circuit mode
self.detailedItemsBy === 'circuit' &&
//Has a circuit id as filter value ("" = 0)
![undefined, null].includes(
self.detailedItemsByCircuitIdValue
) &&
//Not the same circuit
synthesisItem.circuitId !==
self.detailedItemsByCircuitIdValue &&
//Not the same circuit
!(
self.detailedItemsByCircuitIdValue === '' &&
synthesisItem.circuitId === 0
)
) {
return
}
subsets.push({
dateFormatted: item.dateFormatted,
date: item.date,
vehicleId: item.vehicleId,
circuitId: !synthesisItem.circuitId
? 0
: synthesisItem.circuitId,
})
}
})
})
console.log(
'fetchDetailsUsingSynthesisDataset::splitOperation::subsets',
{
count: subsets.length,
}
)
return subsets
},
async handleSubset(subset) {
return await fetchLeveesDetails(
subset.date,
subset.vehicleId,
subset.circuitId,
{
filters: {
puceNumber: self.puceFilterValue,
identified: self.identifiedFilterValue,
stopped: self.stoppedFilterValue,
highPoint: self.highPointFilterValue,
blacklisted: self.blacklistFilterValue,
},
}
)
},
withSubsetResult(results, subset) {
self.detailsGroupedByCircuit.push(
Object.freeze({
dateFormatted: subset.dateFormatted,
date: subset.date,
vehicleId: subset.vehicleId,
circuitId: subset.circuitId,
items: results,
})
)
options &&
options.onDataAvailable &&
options.onDataAvailable(results)
},
})
.then((r) => resolve(r))
.finally(() => {
this.isFetchDetailsInProgress = false
})
})
},
/**
* Fetch synthesis dataset
* - Splits requests by vehicle/date (1-1)
*/
async performsSearch() {
this.detailsGroupedByCircuit = []
this.items = []
this.clearMapMarkers()
let dateRanges = this.selection.selectedDateRanges
//vehiclers or circuits
let selectedElementsIds = this.$store.getters[
'search_module/getSelectedIdsByType'
](this.activeSearchFormTabName)
//Validation: Limit vehicles selection
if (
selectedElementsIds.length * dateRanges.length >
getIdentificationSearchSelectionLimit()
) {
this.$alertPopup.showSelectionLimitWarning(
selectedElementsIds.length * dateRanges.length,
getIdentificationSearchSelectionLimit()
)
return
}
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.show()
let dateFrom = dateRanges[0][0]
let dateTo = dateRanges[len - 1][1]
this.dateFromFormatted = this.$date.formatDatetimeWithSeconds(dateFrom)
this.dateToFormatted = this.$date.formatDatetimeWithSeconds(dateTo)
let dates = dateRanges
.map((range) => range[0])
.sort((a, b) => (a.getTime() < b.getTime() ? 1 : -1))
await fetchLeveesSynthesis({
elementIds: [...selectedElementsIds],
elementType: this.activeSearchFormTabName,
dates,
filters: {
puceNumber: this.puceFilterValue,
identified: this.identifiedFilterValue,
stopped: this.stoppedFilterValue,
highPoint: this.highPointFilterValue,
blacklisted: this.blacklistFilterValue,
},
handleResults: (results) => {
this.items = [...this.items, ...results]
if (
this.mode !== 'list' &&
this.items.length > 0 &&
!this.menuCollapsed
) {
this.menuCollapsed = true
}
},
})
//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.hide()
},
async onSearchModuleSelectionChange() {
this.performsSearch()
},
onSearchModuleViewChange(view) {
//React to search module view changes (selection / results)
},
onSearchModuleClearSelection() {
//Clear custom filters
this.puceFilterValue = ''
this.identifiedFilterValue = ''
this.stoppedFilterValue = ''
this.highPointFilterValue = ''
this.blacklistFilterValue = ''
},
switchMode(mode) {
this.mode = mode
console.log('switchMode', {
mode,
})
if (this.mode === 'list') {
this.$nextTick(() => {
this.menuCollapsed = false
})
} else {
//Table/Table+Map will prepare details dataset async
this.fetchDetailsUsingSynthesisDataset()
this.$nextTick(() => {
this.menuCollapsed = true
})
}
},
},
}
</script>
<style lang="scss">
.identificationModule {
.table-layout {
height: calc(100% - 20px);
}
.filter-label {
font: normal normal normal 9px/14px Open Sans;
letter-spacing: 0px;
color: var(--color-tundora);
margin: 0px;
}
.filter-title {
font: normal normal normal 11px/14px Open Sans;
letter-spacing: 0px;
color: var(--color-tundora);
margin: 0px;
}
.puceNumberInput::placeholder {
font-size: 0.8rem;
}
}
</style>
Source