/**
* @namespace Stores
* @category Stores
* @module search-module-store
* */
import moment from 'moment'
import Vue from 'vue'
import flushPromises from 'flush-promises'
import { getQueryStringValue } from '@/utils/querystring'
import { normalizeCategory } from '@/services/entities/location-main-search-item'
import vehicleService from '@/services/vehicle-service'
import driverService from '@/services/driver-service'
import circuitService from '@/services/circuit-service'
import { getNestedValue } from '@/utils/object.js'
const shouldLog =
(getQueryStringValue('verbose') || '').includes('1') ||
(getQueryStringValue('verbose') || '').includes('search')
const R = require('ramda')
const storage = (Vue.$localStorage &&
Vue.$localStorage.fromNamespace('search_module')) || {
getItem() {},
setItem() {},
}
const stateKeys = {
vehicle: 'selectedVehiclesIds',
driver: 'selectedDriversIds',
circuit: 'selectedCircuitsIds',
}
/**
* @TODO refactor/move into service/utils
*/
function pushIfMissing(state, listName, value) {
if (state[listName].indexOf(value) === -1) {
state[listName].push(value)
}
}
/**
* @TODO refactor/move into service/utils
*/
function spliceIfFound(state, listName, value) {
if (state[listName].indexOf(value) !== -1) {
state[listName].splice(state[listName].indexOf(value), 1)
}
}
/**
* @TODO refactor/move into service/utils
*/
function spliceRangeIfFound(state, listName, range) {
let index = -1
state[listName].forEach((currRange, currIndex) => {
if (currRange[0] == range[0] && currRange[1] == range[1]) {
index = currIndex
}
})
state[listName].splice(index, 1)
}
function newState() {
return {
/**
* We keep stats from last search selection to be able to show searchDaysCount after selection is cleared
* see: searchDaysCount
*/
lastSelectionMetadata: {
searchDaysCount: 0,
selectedVehiclesIdsCount: [],
selectedDriversIdsCount: [],
selectedCircuitsIdsCount: [],
},
initialized: false,
/**
* timestamp updated when the user click the "Valider" button
*/
validatedAt: 0,
/**
* Reference data and current selection
*/
vehicleCategories: [],
vehicles: [],
selectedVehiclesIds: [],
driverCategories: [],
drivers: [],
selectedDriversIds: [],
circuitCategories: [],
circuits: [],
selectedCircuitsIds: [],
selectedDateRanges: [],
/**
* Used for location main search (see: premier_dernier_jour request parameter)
*/
isFirstAndLastDay: true,
/**
* Is data first fetch?
* @TODO refactor/remove if unused
*/
vehiclesFirstFetch: false,
driversFirstFetch: false,
circuitsFirstFetch: false,
/**
* A flag to know whenever to show a back arrow to switch back to results view (Form mode)
*/
hasResults: false,
/**
* The current selected tab in form mode (vehicle/driver/circuit)
*/
activeSearchFormTabName: '',
/**
* Will populate with the current mode (free_search/advanced_search)
* free_search: floating search on top of map
* advanced_search: legacy search form (like georedv2)
*/
mode: '',
/**
* Flag to show a loader icon inside the "Valider" button
*/
isSearchInProgress: false,
/**
* Current component view (selection/results)
*/
view: 'selection',
}
}
export default {
namespaced: true,
state: newState(),
getters: {
//Used by SearchResults (Location module) to compute total text
searchDaysCount: (state) => state.lastSelectionMetadata.searchDaysCount,
activeSearchFormTabSelectedItemsCountFromLastSearch(state, getters) {
return (
state.lastSelectionMetadata[
stateKeys[getters.activeSearchFormTabName || 'vehicle'] + 'Count'
] || 0
)
},
/** */
currentMode: (state) => state.mode,
isCurrentTabVehicles(state, getters) {
return getters['activeSearchFormTabName'] === 'vehicle'
},
isCurrentTabAnyOf(state, getters) {
return (arr) => arr.includes(getters['activeSearchFormTabName'])
},
isCurrentTabCircuit(state, getters) {
return getters['activeSearchFormTabName'] === 'circuit'
},
activeSearchFormTabName(state) {
return state.activeSearchFormTabName || 'vehicle'
},
isInitialLoadingComplete(state) {
return {
vehicles: state.vehiclesFirstFetch,
drivers: state.driversFirstFetch,
circuits: state.circuitsFirstFetch,
}
},
isValidSelection(state) {
return (
state.selectedVehiclesIds.length > 0 ||
state.selectedDriversIds.length > 0 ||
state.selectedCircuitsIds.length > 0
)
},
hasResults(state) {
return state.hasResults
},
getSelectedIdsByType(state) {
return (type) => state[stateKeys[type]]
},
isItemSelected: (state, getters) => (type, id) =>
getters['getSelectedIdsByType'](type).findIndex((_id) => _id == id) >= 0,
getSelectionLength: (state) =>
(state.selectedVehiclesIds.length +
state.selectedDriversIds.length +
state.selectedCircuitsIds.length) *
(state.selectedDateRanges.length || 1),
getSelection: (state) => ({
selectedVehiclesIds: state.selectedVehiclesIds,
selectedDriversIds: state.selectedDriversIds,
selectedCircuitsIds: state.selectedCircuitsIds,
selectedDateRanges: state.selectedDateRanges,
}),
/**
* @todo Refactor/Remove if multiple selections, this is prone to errors
* @param {*} state
* @returns
*/
getSelectionType(state) {
if (state.selectedVehiclesIds.length > 0) {
return 'vehicle'
}
if (state.selectedDriversIds.length > 0) {
return 'driver'
}
if (state.selectedCircuitsIds.length > 0) {
return 'circuit'
}
},
getSelectionAsAString: (state) => {
let vehicleList = state.vehicles
.filter((v) => state.selectedVehiclesIds.indexOf(v.id) !== -1)
.map((v) => v.name)
if (vehicleList.length > 0) {
vehicleList = JSON.stringify(vehicleList.map((v) => v).join(','))
} else {
vehicleList = ''
}
let driversList = state.drivers
.filter((d) => state.selectedDriversIds.indexOf(d.id) !== -1)
.map((d) => d.name)
if (driversList.length > 0) {
driversList = JSON.stringify(driversList.map((d) => d).join(','))
} else {
driversList = ''
}
let circuitsList = state.circuits
.filter((c) => state.selectedCircuitsIds.indexOf(c.id) !== -1)
.map((c) => c.name)
if (circuitsList.length > 0) {
circuitsList = JSON.stringify(circuitsList.map((d) => d).join(','))
} else {
circuitsList = ''
}
let dates = ''
if (state.selectedDateRanges.length > 0) {
state.selectedDateRanges.forEach((date) => {
dates += JSON.stringify(date)
})
}
let searchList = [vehicleList, driversList, circuitsList, dates]
.map((val) => val)
.join(',')
return JSON.stringify(searchList)
},
hasSelectedDates: (state) => state.selectedDateRanges.length > 0,
/**
* Used to decide whenever to show a cancel button
* @param {*} state
* @returns
*/
hasSelection: (state) =>
state.selectedVehiclesIds.length > 0.0 ||
state.selectedDriversIds.length > 0.0 ||
state.selectedCircuitsIds.length > 0.0 ||
state.selectedDateRanges.length > 0.0,
getValidatedTimestamp: (state) => state.validatedAt,
getVehicleCategories(state) {
return state.vehicleCategories
},
getVehicles(state) {
return state.vehicles
},
getVehicleById: (state) => (vehicleId) =>
state.vehicles.find((v) => v.id == vehicleId),
getSelectedVehicles(state) {
return state.vehicles.filter(
(v) => state.selectedVehiclesIds.indexOf(v.id) !== -1
)
},
getDriverCategories(state) {
return state.driverCategories
},
getDrivers(state) {
return state.drivers
},
getSelectedDrivers(state) {
return state.drivers.filter(
(v) => state.selectedDriversIds.indexOf(v.id) !== -1
)
},
getCircuitCategories(state) {
return state.circuitCategories
},
getCircuits(state) {
return state.circuits
},
getSelectedCircuits(state) {
return state.circuits.filter(
(v) => state.selectedCircuitsIds.indexOf(v.id) !== -1
)
},
getSelectedDateRanges(state) {
return state.selectedDateRanges
},
},
mutations: {
setIsFirstAndLastDay(state, value) {
state.isFirstAndLastDay = value
},
setMode(state, mode) {
state.mode = mode || 'advanced_search'
},
selectAll(state, type) {
state[stateKeys[type]] = state[`${type}s`].map((item) => item.id)
},
activeSearchFormTabName(state, activeSearchFormTabName) {
state.activeSearchFormTabName = activeSearchFormTabName
},
setFirstFetch(state, { type, value }) {
state[
{
vehicle: 'vehiclesFirstFetch',
driver: 'driversFirstFetch',
circuit: 'circuitsFirstFetch',
}[type]
] = value
},
initializeState(state) {
Object.assign(state, newState())
//console.log('initializeState')
},
setInitialized(state, value) {
state.initialized = value
},
setVehicleCategories(state, value) {
state.vehicleCategories = value
},
setVehicles(state, value = []) {
state.vehicles = value
},
selectVehicle(state, id) {
pushIfMissing(state, 'selectedVehiclesIds', id)
},
deselectVehicle(state, id) {
spliceIfFound(state, 'selectedVehiclesIds', id)
},
setDrivers(state, value = []) {
state.drivers = value
},
setDriverCategories(state, value) {
state.driverCategories = value
},
selectDriver(state, id) {
pushIfMissing(state, 'selectedDriversIds', id)
},
deselectDriver(state, id) {
spliceIfFound(state, 'selectedDriversIds', id)
},
setCircuits(state, value = []) {
state.circuits = value
},
setCircuitCategories(state, value) {
state.circuitCategories = value
},
selectCircuit(state, id) {
pushIfMissing(state, 'selectedCircuitsIds', id)
},
deselectCircuit(state, id) {
spliceIfFound(state, 'selectedCircuitsIds', id)
},
/**
* @TODO: Consider the end time set in the date picker form.
*/
selectDateRange(state, range) {
if (!(range instanceof Array) || range.length !== 2) {
throw Error('INVALID_DATE_RANGE')
}
range[1] = moment(range[1])._d
//splice data ranges who collide with this range
state.selectedDateRanges
.filter((currRange) => {
//case 1: currRange inside new range
//case 2: new range inside currRange
return (
(moment(currRange[0]).isSameOrAfter(range[0]) &&
moment(currRange[1]).isSameOrBefore(range[1])) ||
(moment(range[0]).isSameOrAfter(currRange[0]) &&
moment(range[1]).isSameOrBefore(currRange[1]))
)
})
.forEach((r) => spliceRangeIfFound(state, 'selectedDateRanges', r))
state.selectedDateRanges.push(range)
//sort by
state.selectedDateRanges = state.selectedDateRanges.sort((a, b) => {
return moment(a[0]).isBefore(moment(b[0])) ? -1 : 1
})
console.log('selectDateRange', state.selectedDateRanges)
},
deselectDateRange(state, range) {
spliceRangeIfFound(state, 'selectedDateRanges', range)
},
updateLastSelectionMetadata(state) {
state.lastSelectionMetadata.searchDaysCount =
state.selectedDateRanges.length
state.lastSelectionMetadata.selectedVehiclesIdsCount =
state.selectedVehiclesIds.length
state.lastSelectionMetadata.selectedDriversIdsCount =
state.selectedDriversIds.length
state.lastSelectionMetadata.selectedCircuitsIdsCount =
state.selectedCircuitsIds.length
},
validateSearch(state) {
state.validatedAt = Date.now()
},
clearDateSelection(state) {
state.selectedDateRanges = []
console.log('clearDateSelection')
},
clearSelection(state, type = null) {
if (type === null) {
state.selectedVehiclesIds = []
state.selectedDriversIds = []
state.selectedCircuitsIds = []
state.selectedDateRanges = []
//console.log('clearSelection::all')
} else {
state[stateKeys[type]] = []
//console.log('clearSelection')
}
},
setHasResults(state, value) {
state.hasResults = value
state.view = 'results'
},
toggleItems(state, { value, ids, type }) {
if (type === 'date_range') {
return
}
let copy = [].concat(state[stateKeys[type]])
let id = 0
for (let x in ids) {
id = ids[x]
if (value) {
if (copy.indexOf(id) === -1) {
copy.push(id)
}
} else {
if (copy.indexOf(id) !== -1) {
copy.splice(copy.indexOf(id), 1)
}
}
}
state[stateKeys[type]] = copy
},
restoreStateFromCache(state, cachedState) {
//Localforage treat Date object as string
cachedState.selectedDateRanges = cachedState.selectedDateRanges.map(
(range) => {
return [new Date(range[0]), new Date(range[1])]
}
)
Object.assign(state, cachedState)
shouldLog && Vue.$log.debug('restoreStateFromCache')
},
},
actions: {
/**
* Update state flag
*/
activeSearchFormTabName({ commit }, activeSearchFormTabName) {
commit('activeSearchFormTabName', activeSearchFormTabName)
},
/**
* Toggle selection (multiple)
*/
toggleItems({ state, commit }, { type, value, ids }) {
if (
state.activeSearchFormTabName != type &&
['vehicle', 'driver', 'circuit'].includes(type)
) {
commit('activeSearchFormTabName', type)
}
commit('toggleItems', {
type,
value,
ids,
})
},
resetStore({ commit }) {
//commit("setInitialized", false); //Re-initialize reference data as well? (Not sure)
commit('initializeState')
//console.log('searchModule::resetStore')
},
/**
* Called once from SearchModule component (self-responsability)
* Called from location_module store (Location module requires search module data)
* Called from LocationModule each time layout updates (to set mode)
* @returns
*/
async initialize({ dispatch, commit, state, rootGetters }, options = {}) {
//wait for initialization in case was already called
await flushPromises()
if (!state.initialized) {
//Try to load state from cache
let cachedState = null
/**
* @TODO: Temporally disabled (Load last search selection after full refresh)
try{
cachedState = (await storage.getItem(`state_${rootGetters["auth/loginNameClientIdEncoded"]}`))||null
}catch(err){
Vue.$log.warn('cachedState',err.stack)
}
*/
if (!cachedState) {
commit('initializeState')
commit('setInitialized', true)
shouldLog && Vue.$log.debug('search_module', 'initializing')
} else {
shouldLog &&
Vue.$log.debug('search_module', 'initializing from cache')
commit('restoreStateFromCache', cachedState)
commit('updateLastSelectionMetadata')
}
await dispatch('fetchVehicles')
await dispatch('fetchDrivers')
await dispatch('fetchCircuits')
}
if (options.mode) {
commit('setMode', options.mode)
}
},
/**
* Set state flag
*/
setHasResults({ commit }, value) {
console.debug('setHasResults', value)
commit('setHasResults', value)
},
/**
* Set state flag
*/
setIsFirstAndLastDay({ commit }, value) {
commit('setIsFirstAndLastDay', value)
},
/**
* Same as reset store?
* @TODO refactor/remove if unused
*/
clearSelection({ commit }, type) {
commit('clearSelection', type)
},
clearDateSelection({ commit }) {
commit('clearDateSelection')
},
/**
* Triggered when the user click "Valider" or Search action button
* - Updates timestamp in state
*/
validateSearch({ commit, state, rootGetters }) {
commit('updateLastSelectionMetadata')
commit('validateSearch')
//Save current selection in local cache
storage.setItem(
`state_${rootGetters['auth/loginNameClientIdEncoded']}`,
R.omit(
[
'drivers',
'driverCategories',
'vehicles',
'vehicleCategories',
'circuits',
'circuitCategories',
],
JSON.parse(JSON.stringify(state))
)
)
},
/**
* Select single item (vehicle/driver/circuit)
*/
selectItem({ commit, state }, { value, type }) {
const mutations = {
vehicle: 'selectVehicle',
driver: 'selectDriver',
circuit: 'selectCircuit',
date_range: 'selectDateRange',
}
//SearchBar: Switch the form tab depending the selected item
if (
state.activeSearchFormTabName != type &&
['vehicle', 'driver', 'circuit'].includes(type)
) {
commit('activeSearchFormTabName', type)
}
commit(mutations[type], value)
console.log('selectItem', {
mutationName: mutations[type],
value,
})
},
deselectItem({ commit }, { value, type }) {
const mutations = {
vehicle: 'deselectVehicle',
driver: 'deselectDriver',
circuit: 'deselectCircuit',
date_range: 'deselectDateRange',
}
commit(mutations[type], value)
},
async fetchVehicles({ commit, state }) {
commit(
'setVehicleCategories',
await vehicleService.fetchVehicleCategories(normalizeCategory)
)
let items = await vehicleService.fetchVehicles(
vehicleService.normalizeVehicle
)
let ids = items.map((i) => i.id)
items = items.filter((i, ii) => ids.indexOf(i.id) == ii)
commit('setVehicles', items)
commit('setFirstFetch', {
type: 'vehicle',
value: true,
})
},
async fetchDrivers({ commit, state }) {
commit(
'setDriverCategories',
await driverService.fetchDriverCategories(normalizeCategory)
)
let response = await driverService.fetchDrivers()
commit(
'setDrivers',
response &&
response.map((i) => {
let categoryId = null
//No category = null (Bug?)
if (!i._links.category.href) {
//tries to set "sans category" category
categoryId = (
state.driverCategories.find(
(c) => c.name.toLowerCase().indexOf('sans ') !== -1
) || {}
).id
} else {
categoryId = i._links.category.href.match(/\d+/)[0]
}
let result = Object.freeze({
id: i.id,
name: i.name,
categoryId: categoryId,
//Bug: Sometimes, category name fail to compute
categoryName:
state.driverCategories.find((dc) => dc.id == categoryId)
?.name || categoryId,
//original: i
})
return result
})
)
commit('setFirstFetch', {
type: 'driver',
value: true,
})
},
async fetchCircuits({ commit, state }) {
commit(
'setCircuitCategories',
await circuitService.fetchCircuitCategories((c) => {
return normalizeCategory(c)
})
)
let response = await circuitService.fetchCircuits()
let circuits = []
if (response) {
response.forEach((i) => {
if (i.archive == false) {
let categoryId = null
if (!i._links?.category?.href) {
categoryId = (
state.circuitCategories.find(
(c) => c.name.toLowerCase().indexOf('sans ') !== -1
) || {}
).id
} else {
categoryId = i._links.category.href.match(/\d+/)[0]
}
circuits.push(
Object.freeze({
id: getNestedValue(i, ['id', '_links.self.href'], null, {
transform(id, originalValue, rawItem) {
if ((id || '').toString().includes('/')) {
console.warn(
`Attribute id (Circuit Id) wasn't found in the APIV3 response (Circuits). Computed from _links.self.href (Intermediate solution)`
)
id = id.split('/').pop()
}
if (!id) {
console.error(
'No id found (Circuits) in APIV3 response',
{
rawItem,
}
)
}
return id
},
}),
name: i.shortName,
categoryId: categoryId,
categoryName: i.categoryName,
})
)
}
})
}
commit('setCircuits', circuits)
commit('setFirstFetch', {
type: 'circuit',
value: true,
})
},
},
}
Source