Source

store/location_module/main_search.js

import Vue from 'vue'
import moment from 'moment'
import { createCircuitColorsManager } from '@/services/location-service.js'
import { generateShortId } from '@/utils/crypto.js'
import APIUrls from '@/config/simpliciti-apis.js'

const LocationServiceTripHistoryColorManager = createCircuitColorsManager()

/**
 * User can stop search in progress
 */
Vue.use({
  install() {
    let scope = (Vue.$locationModuleSearchManager = {
      _searchs: [],
      createNew() {
        let item = {
          id: generateShortId('id-'),
          isCanceled: false,
          isRemoved: false,
        }
        scope._searchs.push(item)
        return {
          item,
          remove() {
            item.isRemoved = true
            scope.remove(item)
          },
          shouldContinue() {
            if (!item.isCanceled) {
              return true
            } else {
              !item.isRemoved && scope.remove(item)
              return false
            }
          },
        }
      },
      remove(idOrItem) {
        let id = idOrItem.id || id
        console.warn('location: search abort (untracking)', id)
        scope._searchs.splice(
          scope._searchs.findIndex((item) => item.id == id),
          1
        )
      },
      cancelAnySearchInProgress() {
        scope._searchs.forEach((item) => {
          item.isCanceled = true
        })
      },
    })
  },
})

import {
  parseLocationItem,
  getHistoryAPIQueryStringDatePart,
  getItemsFromHistoryVehicleDriverAPIV2Response,
  getItemsFromHistoryCircuitAPIV2Response,
  getItemsFromHistoryDriverAPIV2Response,
} from './helpers'
const sequential = require('promise-sequential')

const searchModuleSelectedKeys = {
  vehicle: 'selectedVehiclesIds',
  driver: 'selectedDriversIds',
  circuit: 'selectedCircuitsIds',
}

function hasSelectionTypeAnySelectedID(selectionType, searchSelection) {
  return (
    searchSelection[searchModuleSelectedKeys[selectionType]] &&
    searchSelection[searchModuleSelectedKeys[selectionType]].length > 0
  )
}

/***
 * @namespace Stores
 * @category Stores
 * @module location-module-store-main-search
 * @memberof location-module-store
 */
export default {
  actions: {
    /**
     * @internal
     * Split by items feature: Requests are split if items (vehicle/driver/circuit) are > 5
     * Merge strategy: Merge results if not first request
     * Sequential: Yes
     * Parallel: No
     *
     * @returns
     */
    async performSearchDivideRequests({ dispatch, rootGetters }, payload = {}) {
      return new Promise(async (resolve, reject) => {
        const { selectionType } = payload
        let searchSelection = rootGetters['search_module/getSelection']
        let allIds =
          payload.ids ||
          searchSelection[searchModuleSelectedKeys[selectionType]]
        let groups = []
        let group = []
        let groupLengths = {
          vehicle:
            process.env
              .VUE_APP_API_REALTIME_BY_VEHICULE_MAX_ITEMS_PER_REQUEST || 5,
          driver:
            process.env
              .VUE_APP_API_REALTIME_BY_VEHICULE_MAX_ITEMS_PER_REQUEST || 5,
          circuit:
            process.env
              .VUE_APP_API_REALTIME_BY_VEHICULE_MAX_ITEMS_PER_REQUEST || 5,
        }

        allIds.forEach((id) => {
          group.push(id)
          if (group.length == groupLengths[selectionType]) {
            groups.push(group)
            group = []
          }
        })
        if (group.length > 0) {
          groups.push(group)
        }

        let stats = {
          requestsDone: 0,
          groupsCount: groups.length,
          requestsTotal: groups.length,
          totalReqTime: 0,
          searchStart: moment(),
        }

        let searchItem = Vue.$locationModuleSearchManager.createNew()

        LocationServiceTripHistoryColorManager.resetState()

        //Vue.$log.debug("performSearchDivideRequests::groups", groups.length);
        await sequential(
          groups.map((ids, index) => {
            return async () => {
              if (!searchItem.shouldContinue()) {
                console.warn(
                  'location: search abort (skip series)',
                  searchItem.id
                )
                return null
              }

              let r = await dispatch('performSearch', {
                ...payload,
                selectionType,
                ids,
                isSplitByItems: true,
                merge: index > 0,
                stats,
                searchItem,
                metadata: {
                  splitGroup: `${index + 1}/${groups.length}`,
                  group: index + 1,
                },
              })
              return r
            }
          })
        )

        searchItem.remove()

        if (!searchItem.shouldContinue()) {
          console.warn('location: search abort (resolve)', searchItem.id)
          resolve()
          return
        }

        //Used by unit-test
        if (payload.onSuccessSplitItems) {
          await payload.onSuccessSplitItems({
            dispatch,
            rootGetters,
          })
        }

        resolve()
      })
    },
    stopSearchInProgress() {},
    /**
     * Performs search by vehicle/circuit/drivers using real-time/history API.
     * @returns
     */
    async performSearch(
      { dispatch, rootGetters, rootState, commit },
      payload = {}
    ) {
      if (!payload.selectionType) {
        throw new Error('selectionType required')
      }

      const { selectionType } = payload

      let searchSelection = {
        ...rootGetters['search_module/getSelection'],
      }

      if (payload.selectedDateRanges) {
        searchSelection.selectedDateRanges = payload.selectedDateRanges
      }

      //Do not check/validate selected ids in search_module store if mock (jest)
      if (
        payload.isMock !== true &&
        !payload.categoryIds &&
        !hasSelectionTypeAnySelectedID(selectionType, searchSelection)
      ) {
        return
      }

      if (payload.isSplitByItems !== true) {
        return dispatch('performSearchDivideRequests', payload)
      }

      let allIds =
        payload.categoryIds ||
        payload.ids ||
        searchSelection[searchModuleSelectedKeys[selectionType]]

      let infos = []

      const baseUrlData = {
        vehicle: [
          APIUrls.APIV2_TEMPS_REEL_BY_VEHICLE,
          APIUrls.APIV2_HISTORIQUE_BY_VEHICLE_DATES,
        ],
        driver: [
          APIUrls.APIV2_TEMPS_REEL_BY_DRIVER,
          APIUrls.APIV2_HISTORIQUE_BY_DRIVER_DATES,
        ],
        circuit: [
          APIUrls.APIV2_TEMPS_REEL_BY_CIRCUIT,
          APIUrls.APIV2_HISTORIQUE_BY_CIRCUIT_DATES,
        ],
      }

      const isHistoryMode = searchSelection.selectedDateRanges.length > 0
      const baseUrl = baseUrlData[selectionType][isHistoryMode ? 1 : 0]

      let extraParams = ''
      if (isHistoryMode) {
        /*
         * Split by date feature: Requests are split if multiple dates
         * Merge strategy: Merge results if not first request from first group (split by items feature)
         * Sequential: No
         * Parallel: Yes
         * */

        let dateRanges = searchSelection.selectedDateRanges
        if (dateRanges.length > 1) {
          let firstSplitByDateSetResultPromise = null

          payload.stats.requestsTotal =
            payload.stats.groupsCount * dateRanges.length

          //Execute in parallel
          await Promise.all(
            dateRanges.map((singleDateRange, index) => {
              let lastDateRange = dateRanges[dateRanges.length - 1]

              //Time-range is stored in the last date range
              singleDateRange[0] = moment(singleDateRange[0])
                .hour(moment(lastDateRange[0]).hour())
                .minute(moment(lastDateRange[0]).minute())._d
              singleDateRange[1] = moment(singleDateRange[1])
                .hour(moment(lastDateRange[1]).hour())
                .minute(moment(lastDateRange[1]).minute())._d

              return (async () => {
                /*
                Vue.$log.debug(
                  "performSearch:split-by-date executing date range: ",
                  singleDateRange.map((date) =>
                    moment(date).format("YYYY-MM-DD HH:mm:ss")
                  ),
                  "for items",
                  allIds
                );*/

                const requestId = generateShortId('req-')

                let r = await dispatch('performSearch', {
                  ...payload,
                  requestId,
                  selectionType,
                  ids: allIds,
                  selectedDateRanges: [singleDateRange],
                  merge: payload.merge,
                  stats: payload.stats,
                  metadata: {
                    ...(payload.metadata || {}),
                    splitGroupDate: `${index + 1}/${dateRanges.length}`,
                    reqCount: payload.stats.requestsTotal,
                  },
                  //Override Assign/Merge
                  setHandler: async (infos) => {
                    //Update completed metadata (debug only)
                    if (!Vue.$env.isProduction()) {
                      commit(
                        'api/updateCompletedRequestMetadata',
                        {
                          id: requestId,
                          payload: {
                            shouldMergeResults:
                              payload.merge || firstSplitByDateSetResultPromise,
                            req: `${payload.stats.requestsDone}/${payload.stats.requestsTotal}`,
                            totalReqTime: moment
                              .utc(payload.stats.totalReqTime)
                              .format('HH:mm:ss.SSS'),
                            searchElapsed: moment
                              .utc(moment().diff(payload.stats.searchStart))
                              .format('HH:mm:ss.SSS'),
                          },
                        },
                        {
                          root: true,
                        }
                      )
                    }

                    //Set (Assign/Merge) can be mock
                    if (payload.mockSet) {
                      await mockSet({ commit, dispatch }, infos)
                    } else {
                      const shouldMerge =
                        payload.merge || !!firstSplitByDateSetResultPromise

                      if (firstSplitByDateSetResultPromise) {
                        await firstSplitByDateSetResultPromise
                      }

                      let setPromise = dispatch('setResultsFromAPIResponse', {
                        selectionType,
                        infos,
                        disableInitializeSearchModule:
                          payload.disableInitializeSearchModule,
                        merge: shouldMerge,
                      })

                      if (!firstSplitByDateSetResultPromise) {
                        firstSplitByDateSetResultPromise = setPromise
                      }
                    }
                  },
                })
                return r
              })() //Promise.all expects promises; sequential expects ()=>Promise
            })
          )

          //If split by date, return immediately
          return
        } else {
          const isFirstAndLastDay = rootState.search_module.isFirstAndLastDay
          extraParams += `${getHistoryAPIQueryStringDatePart(
            searchSelection.selectedDateRanges,
            isFirstAndLastDay
          )}`
          extraParams += '&linestring=1&linestring_troncons=1' // Bring circuit execution linestrings
        }

        if (['vehicle', 'driver'].includes(selectionType)) {
          extraParams += '&linestring_multiple=true'
        }
      }

      //Override fetch/set
      if (payload.mockFetchAndSet !== undefined) {
        payload.mockFetchAndSet(payload)
        return
      }

      //Fetch can be override
      if (payload.mockFetch) {
        infos = await payload.mockFetch(payload)
      } else {
        //I.g: If selectionType=circuit and payload.categoryIds, paramName equals circuit_categorie_id
        let paramName = {
          vehicle: ['vehicule_id', 'vehicule_categorie_id'],
          driver: ['chauffeur_id', 'chauffeur_categorie_id'],
          circuit: ['circuit_id', 'circuit_categorie_id'],
        }[selectionType][payload.ids ? 0 : 1]

        if (!Vue.$env.isProduction()) {
          payload.reqStart = moment()
        }

        let fetchPromise = dispatch(
          'api/post',
          {
            name: `main_search_${isHistoryMode ? 'history' : 'realtime'}`,
            url: `${baseUrl}?${paramName}=${allIds}${extraParams}`,
            debugHasResultsHandler: (apiResponse) =>
              getNormalizedResults(apiResponse, selectionType, rootGetters)
                .length > 0,
            debugMetadata: !Vue.$env.isProduction() && {
              shouldMergeResults: payload.merge,
              ...(payload.metadata || {}),
              totalReqTime: moment
                .utc(payload.stats.totalReqTime)
                .format('HH:mm:ss.SSS'),
            },
            debugRequestId: payload.requestId,
          },
          {
            root: true,
          }
        )

        infos = (await fetchPromise).data
      }
      payload.stats.requestsDone++

      if (!Vue.$env.isProduction()) {
        payload.stats.totalReqTime =
          payload.stats.totalReqTime + moment().diff(payload.reqStart)
      }

      if (payload.searchItem && !payload.searchItem.shouldContinue()) {
        console.warn(
          'location: search abort (skip result)',
          payload.searchItem.id
        )
        return
      }

      //Set (Assign/Merge) can be override
      if (payload.setHandler) {
        return payload.setHandler(infos)
      }

      //Set (Assign/Merge) can be mock
      if (payload.mockSet) {
        await mockSet({ commit, dispatch }, infos)
      } else {
        //Results are Assigned/Merged into store
        await dispatch('setResultsFromAPIResponse', {
          selectionType,
          infos,
          disableInitializeSearchModule: payload.disableInitializeSearchModule,
          merge: payload.merge !== undefined ? payload.merge : false,
        })
      }
    },
    /**
     *
     * Transform/Normalize and set/merge real-time/history APIV2 responses into Location store
     *
     * @param {Object} VuexContext
     * @param {String} options.selectionType Required (vehicle/driver/circuit)
     * @param {Array} options.infos Required
     * @param {Boolean} options.merge Whenever to merge results into existing results or overwrite them
     */
    async setResultsFromAPIResponse(
      { commit, rootGetters, state, dispatch },
      options = {}
    ) {
      if (options.infos === null) {
        return
      }

      //Only for unit test
      if (options.disableInitializeSearchModule !== true) {
        //Normalization requires search_module to be initialized
        await dispatch(
          'search_module/initialize',
          {},
          {
            root: true,
          }
        )
      } else {
        Vue.$log.info('disableInitializeSearchModule')
      }

      let { selectionType, infos } = options

      const normalizedResults = getNormalizedResults(
        infos,
        selectionType,
        rootGetters
      )

      commit(options.merge ? 'mergeResults' : 'setResults', {
        type: selectionType,
        value: normalizedResults,
      })

      let hasResults =
        state.vehicleResults.length > 0 ||
        state.driverResults.length > 0 ||
        state.circuitResults.length > 0

      dispatch('search_module/setHasResults', hasResults, {
        root: true,
      })
    },
  },
}

/**
 * Normalizes API responses into a flat array
 *
 * -APIV2 Responses are not flat and there are different (history/realtime)
 */
function getNormalizedResults(apiResponse, selectionType, rootGetters) {
  let options = { selectionType }
  options.isHistoryMode = !(apiResponse instanceof Array)
  let flatResults = []
  if (options.isHistoryMode) {
    if (['vehicle'].includes(selectionType)) {
      flatResults = getItemsFromHistoryVehicleDriverAPIV2Response(apiResponse)
    }

    if (['driver'].includes(selectionType)) {
      flatResults = getItemsFromHistoryDriverAPIV2Response(apiResponse)
    }

    if (['circuit'].includes(selectionType)) {
      flatResults = getItemsFromHistoryCircuitAPIV2Response(apiResponse)
    }

    flatResults = flatResults.map((item) => {
      item.couleur =
        LocationServiceTripHistoryColorManager.getUnusedColorAsString(
          item.vehicule_id
        )
      //console.info('Grabing color for vehicle', item.vehicule_id, item.couleur)
      return item
    })
  } else {
    flatResults = apiResponse //Realtime API already brings a flat array
  }

  return (flatResults || []).map((item) =>
    parseLocationItem(item, options, rootGetters)
  )
}