Source

services/entities/location-main-search-item.js

import { linestringsToPolylines } from '@/mixins/map.js'
import moment from 'moment'
import {
  formatDatetimeWithSeconds,
  datetimeToTimestamp,
} from '@/utils/dates.js'
import {
  getNestedValue,
  getNestedValueFormattedDatetimeMilliseconds,
  getNestedValueFormattedTime,
} from '@/utils/object.js'
import { generateShortId } from '@/utils/crypto.js'
import { APIV2ResponseDatetimeFormat } from '@/config/simpliciti-apis.js'
const R = require('ramda')

export function normalizeCategory(c) {
  return Object.freeze({
    id: getNestedValue(c, 'id'),
    name: getNestedValue(c, 'label'),
    parent_id: getNestedValue(c, 'parentId'),
  })
}

/**
 * Normalizes an API response item from real-time/history by vehicle/driver/circuit (APIV2)
 *
 * @param {*} item
 * @param {*} options
 * @returns
 */
export default function normalizeEntity(item, options = {}) {
  const vehicleId = getNestedValue(item, ['vehicule.id', 'vehicule_id'])
  const vehicleCategory = options.vehicleCategory || {}
  const vehicleCategoryName =
    getNestedValue(
      item,
      ['vehicule.categorie_nom', 'vehicule_categorie_nom', 'categorie_nom'],
      null
    ) ||
    vehicleCategory.name ||
    ''

  //options.getNormalizedItem = () => normalizedItem

  // Take linestring raw data from linestring_troncons if available (#35102)
  item.linestring = getNestedValue(
    item,
    [
      'linestring_troncons' /*history by circuit*/,
      'linestring' /*history by vehicle/driver*/,
    ],
    '',
    {
      transform(value) {
        if (value instanceof Array) {
          //{couleur:'',linestring:''}
          return value.map((valueItem) => {
            if (typeof valueItem === 'string') {
              //Use case: History by vehicle/driver
              return {
                linestring: valueItem,
                color: null,
              }
            } else {
              //Use case: History by circuit
              return {
                linestring: getNestedValue(valueItem, 'linestring', ''),
                color: getNestedValue(valueItem, 'couleur', null),
              }
            }
          })
        } else {
          return value
        }
      },
    }
  )

  //A trip history colliding a zone interdiction might produce multiple linestrings/polylines.
  let relatedPolylines = (options.relatedPolylines =
    item.linestring === undefined
      ? []
      : getPolylinesFromItemLinestring(item, {
          ...options,
          color: getNestedValue(item, 'couleur'),
          transform(obj) {
            obj.itemId = vehicleId
            return obj
          },
        }))

  let normalizedItem = {}
  return (normalizedItem = Object.freeze({
    //Common (Mixed)f
    searchType: options.isHistoryMode ? 'history' : 'realtime',
    id: vehicleId,
    type: options.selectionType,
    label: (options.label = getLocationItemLabel(item, options.selectionType)),

    //Temp reel
    speed: (options.speed = getNestedValue(item, 'temps_reel.vitesse')),
    statusColor: getNestedValue(item, ['temps_reel.status_couleur', 'couleur']),
    address: {
      streetNumber: getNestedValue(item, 'temps_reel.adresse.numero_rue'),
      streetAddress: getNestedValue(item, [
        'temps_reel.adresse.rue',
        'adresse_deb',
      ]),
      zipCode: getNestedValue(item, [
        'temps_reel.adresse.code_postal',
        'cp_deb',
      ]),
      city: getNestedValue(item, ['temps_reel.adresse.ville', 'ville_deb']),
    },
    addressFormatted: (() => {
      let address = {
        streetNumber: getNestedValue(item, 'temps_reel.adresse.numero_rue'),
        streetAddress: getNestedValue(item, [
          'temps_reel.adresse.rue',
          'adresse_deb',
        ]),
        zipCode: getNestedValue(item, [
          'temps_reel.adresse.code_postal',
          'cp_deb',
        ]),
        city: getNestedValue(item, ['temps_reel.adresse.ville', 'ville_deb']),
      }
      let r = `${address.streetNumber} ${address.streetAddress}`.trim()
      r = (r ? r + `, ` : r + ``) + address.zipCode
      r = (r ? r + `, ` : r + ``) + address.city
      return r
    })(),
    lat: getNestedValue(item, 'temps_reel.latitude'),
    lng: getNestedValue(item, 'temps_reel.longitude'),
    cap: getNestedValue(item, 'temps_reel.cap'),

    //Sensor infos (Temp reel)
    ...parseLocationItemSensors(item),

    //Vehicle (Mixed)
    vehicleId: parseInt(
      getNestedValue(item, ['vehicule.id', 'vehicule_id'], null)
    ),
    vehicleName: getNestedValue(item, ['vehicule.nom', 'vehicule_nom'], null),
    vehicleCategoryClassName: getNestedValue(item, [
      'vehicule.categorie_classe',
    ]),
    vehicleCategoryId: getNestedValue(vehicleCategory, 'id', null),
    vehicleCategoryName,
    vehicleStatusColor: getNestedValue(item, 'temps_reel.status_couleur'),
    vehicleContactOn: getNestedValue(item, 'temps_reel.contact', false),
    vehicleStatusLabel: getNestedValue(item, 'temps_reel.status_libelle', ''),
    vehicleMatriculation: getNestedValue(
      item,
      ['vehicle.immatriculation', 'vehicule_immatriculation'],
      ''
    ),

    //Circuit (Mixed)
    circuitId: getNestedValue(
      item,
      ['circuit.id', 'circuit_id', 'executions_circuits.0.circuit_id'],
      null
    ),
    circuitExecutions: getNestedValue(item, 'executions_circuits', [], {
      transform(value) {
        return (value || []).map((item) => {
          return {
            executionId: item.executioncircuit_id,
            circuitId: item.circuit_id,
            startDate: moment(item.date_debut),
            endDate: item.date_fin
              ? moment(item.date_fin)
              : moment(item.date_debut).hour(23).minute(59).second(59),
            circuitName: item.nomcourt,
          }
        })
      },
    }),
    circuitName: getNestedValue(
      item,
      ['circuit.nom_court', 'circuit_nom_court'],
      null
    ),
    circuitCategoryName: getNestedValue(item, 'circuit_categorie', ''),
    circuitExecutionId: getNestedValue(
      item,
      ['circuit.execution_id', 'executioncircuit_id'],
      null
    ),
    circuitStartDate: getNestedValue(item, 'circuit.debut_execution', null),
    circuitStartDateFormatted: getNestedValueFormattedDatetimeMilliseconds(
      item,
      'circuit.debut_execution'
    ),
    circuitCompletionRate: getNestedValue(
      item,
      ['circuit.taux_realisation', 'taux_realisation_circuit'],
      0
    ),
    circuitDriversCount: (() => {
      let team = getNestedValue(item, 'fdr.equipage.membres', [])
      return team.length
    })(),
    /**
     * Use now as end date if null is supplied
     */
    circuitServiceDuration: (() => {
      let start = moment(getNestedValue(item, 'fdr.service_debut_reel', null))
      let end = moment(getNestedValue(item, 'fdr.service_fin_reel', null))
      if (!start.isValid() || !end.isValid()) {
        return ''
      }
      let ms = end.diff(start)
      let d = moment.duration(ms)
      let hours = Math.floor(d.asHours())
      let rightPart = moment.utc(ms)
      if (isNaN(hours) || !rightPart.isValid()) {
        return ''
      }
      return hours + rightPart.format('[ h ]mm[ m ]ss[ s ]')
    })(),

    missionId: getNestedValue(item, ['mission.id'], null),

    //Driver (Temp reel)
    driverId: getNestedValue(item, ['driverId', 'chauffeur.id'], null),
    driverName: getNestedValue(
      item,
      ['chauffeur.nom', 'driverName', 'nom_chauffeur'],
      null
    ),
    driverBadgeId: getNestedValue(item, 'chauffeur.badge_id', null),
    driverCardNumber: getNestedValue(item, 'chauffeur.numero_carte', null),
    driverPhone: getNestedValue(item, 'chauffeur.tel_portable', null),
    driverDate: getNestedValue(item, 'chauffeur.dateheure', null),
    driverDateFormatted: getNestedValueFormattedDatetimeMilliseconds(
      item,
      'chauffeur.dateheure'
    ),
    driverChronoDate: getNestedValue(item, 'chauffeur.chrono_dateheure', null),
    driverChronoEnabled: getNestedValue(item, 'chauffeur.chrono_etat', null),
    driverChronoStatus: getDriverChronoStatus(
      getNestedValue(item, 'chauffeur.chrono_imputation', null)
    ),
    driverChornoStatusOriginal: getNestedValue(
      item,
      'chauffeur.chrono_imputation',
      null
    ),
    driverPlace: getNestedValue(item, 'chauffeur.site_nom', null),

    //History
    ...parseHistoryItem(item, options),

    //Mixed
    relatedPolylines,

    //@TODO: Refactor/Remove, use vehicleCategoryId instead
    //Warning: Location - List sort results by this attribute, meaning drivers/circuits will be sorted by vehicle category id (BUG!?)
    categoryId: getNestedValue(vehicleCategory, 'id', null),

    ...parseLocationItemDates(item, options),

    //@todo: Refactor and remove raw attribute
    raw: item,
  }))
}

function stringDatetimeToDate(date, inputFormat = 'YYYY[-]MM[-]DD HH:mm:ss') {
  return moment(date, inputFormat)._d
}

/**
 *
 * Parse dates fields from both real-time/history
 *
 * @param {*} item
 * @param {*} options
 * @returns
 */
function parseLocationItemDates(item, options) {
  let date = getNestedValue(item, [
    'temps_reel.dateheure',
    'dh_vehicule_deb',
    'dh_exec_circuit_debut',
    'dh_activ_deb',
  ])
  return {
    date: stringDatetimeToDate(date),
    timestamp: stringDatetimeToDate(date).getTime(),
    dateFormatted: formatDatetimeWithSeconds(stringDatetimeToDate(date)),
    dateElapsedFormatted: (() => {
      let then = stringDatetimeToDate(date)
      let ms = moment().diff(moment(then))
      let d = moment.duration(ms)
      return (
        Math.floor(d.asHours()) + moment.utc(ms).format('[ h ]mm[ m ]ss[ s ]')
      )
    })(),

    //History mode only
    ...(options.isHistoryMode && options.selectionType === 'circuit'
      ? {
          circuitDatetimeFrom: (options.circuitDatetimeFrom =
            stringDatetimeToDate(
              getNestedValue(item, 'dh_exec_circuit_debut')
            )),
          circuitDatetimeTo: (options.circuitDatetimeTo = stringDatetimeToDate(
            getNestedValue(item, 'dh_exec_circuit_fin')
          )),
          circuitTimeFrom: getNestedValueFormattedTime(
            item,
            'dh_exec_circuit_debut'
          ),
          circuitTimeTo: getNestedValueFormattedTime(
            item,
            'dh_exec_circuit_fin'
          ),
        }
      : {}),
  }
}

function getLocationItemLabel(item, selectionType = 'vehicle') {
  let vehicleName = getNestedValue(item, ['vehicule.nom', 'vehicule_nom'], null)
  let driverName = getNestedValue(item, ['chauffeur.nom', 'driverName'], null)
  driverName = `${driverName} (${vehicleName})`
  const labels = {
    vehicle: vehicleName,
    driver: driverName,
    circuit: getNestedValue(
      item,
      ['circuit.nom_court', 'circuit_nom_court'],
      null
    ),
  }
  return labels[selectionType]
}

/**
 * Parses item from API /v2/historique/
 *
 * - Circuit: by_circuit_dates
 * - Vehicle/Driver: by_vehicule_dates/by_chauffeur_dates
 *
 * @param {Object} item
 * @param {String} options.selectionType vehicle/driver/circuit
 * @param {Array} options.relatedPolylines
 * @returns
 */
function parseHistoryItem(item, options = {}) {
  const isHistoryByCircuit =
    options.isHistoryMode && options.selectionType === 'circuit'
  const isHistoryByVehicleDriver =
    options.isHistoryMode &&
    ['driver', 'vehicle'].includes(options.selectionType)

  let historyByCircuitFields = !isHistoryByCircuit
    ? {}
    : {
        circuitDistance: getNestedValue(item, ['lg_realisee_circuit'], 0),
        /*circuitLeafletPolyline:
          options.relatedPolylines.length > 0
            ? options.relatedPolylines[0]
            : null,*/
      }

  let historyByVehicleOrDriverFields = !isHistoryByVehicleDriver
    ? {}
    : {
        totalDistance: getNestedValue(item, 'distance_troncon', 0),
        averageSpeed: getNestedValue(item, 'vitesse', 0),
        driverIdentification: getNestedValue(item, 'hdeb_activite', 0),
        driverDesidentification: getNestedValue(item, 'hfin_activite', 0),
        historyAddressFrom: (() => {
          let address = {
            streetAddress: getNestedValue(item, 'adresse_deb'),
            zipCode: getNestedValue(item, 'cp_deb'),
            city: getNestedValue(item, 'ville_deb'),
          }
          let r = `${address.streetAddress}`.trim()
          r = (r ? r + `, ` : r + ``) + address.zipCode
          r = (r ? r + `, ` : r + ``) + address.city
          return r
        })(),
        historyAddressTo: (() => {
          let address = {
            streetAddress: getNestedValue(item, 'adresse_fin'),
            zipCode: getNestedValue(item, 'cp_fin'),
            city: getNestedValue(item, 'ville_fin'),
          }
          let r = `${address.streetAddress}`.trim()
          r = (r ? r + `, ` : r + ``) + address.zipCode
          r = (r ? r + `, ` : r + ``) + address.city
          return r
        })(),
        historyStartTime: getNestedValueFormattedTime(item, 'dh_activ_deb', 0),
        historyEndTime: getNestedValueFormattedTime(item, 'dh_activ_fin', 0),

        historyStartDatetimeStamp: datetimeToTimestamp(
          getNestedValue(item, 'dh_activ_deb', 0),
          APIV2ResponseDatetimeFormat
        ),
        historyEndDatetimeStamp: datetimeToTimestamp(
          getNestedValue(item, 'dh_activ_fin', 0),
          APIV2ResponseDatetimeFormat
        ),

        historyStartDatetime: getNestedValueFormattedDatetimeMilliseconds(
          item,
          'dh_activ_deb',
          0
        ),

        historyEndDatetime: getNestedValueFormattedDatetimeMilliseconds(
          item,
          'dh_activ_fin',
          0
        ),
        totalElapsedTime: getNestedValue(item, 'duree_troncon', 0),
        totalContactOn: getNestedValue(item, 'duree_contact', 0),
        totalBrakes: getNestedValue(item, 'duree_arret', 0),
        brakesTotalAmount: getNestedValue(item, 'nb_arret', 0),

        tripHistoryPolyline:
          options.relatedPolylines.length > 0
            ? options.relatedPolylines[0]
            : null,
      }

  return {
    ...(isHistoryByCircuit ? historyByCircuitFields : {}),
    ...(isHistoryByVehicleDriver ? historyByVehicleOrDriverFields : {}),
  }
}

/**
 * Parses sensor infos (Temp reel)
 * @param {*} item
 * @returns
 */
function parseLocationItemSensors(item) {
  return {
    //Sensors ------------
    hasSensors:
      Object.keys(getNestedValue(item, 'temps_reel.capteurs', {})).length > 0,
    //Vitesse Rotation Moteur
    sensorCanRPM: getNestedValue(item, 'temps_reel.capteurs.can.vrm', null),
    //Fleet management system
    sensorCanKMFMS: getNestedValue(
      item,
      'temps_reel.capteurs.can.km_fms',
      null
    ),
    sensorCanFuelPerc: getNestedValue(
      item,
      'temps_reel.capteurs.can.niveau_carburant',
      null
    ),
    sensorCanConsumptionLiters: getNestedValue(
      item,
      'temps_reel.capteurs.can.conso',
      null
    ),
    sensorCanThrottle: getNestedValue(
      item,
      'temps_reel.capteurs.can.accelerateur',
      null
    ),
    sensorCanBatteryPerc: getNestedValue(
      item,
      'temps_reel.capteurs.can.batterie',
      null,
      {
        transform: (v) => (!isNaN(parseInt(v)) && parseInt(v) <= 4 ? null : v), //Only grab value if > 4
      }
    ),
    sensorCanBraking: getNestedValue(
      item,
      'temps_reel.capteurs.can.freinage',
      null
    ),
    sensorCanClutch: getNestedValue(
      item,
      'temps_reel.capteurs.can.embrayage',
      null
    ),
    sensorTor: getNestedValue(item, 'temps_reel.capteurs.tor', [])
      .filter((tor) => tor.etat !== null)
      .map((tor) => {
        return {
          name: tor.nom ? tor.nom : `TOR${tor.num || '?'}`,
          enabled: tor.etat === 1,
        }
      }),
    sensorAna: getNestedValue(item, 'temps_reel.capteurs.ana', [])
      .filter((ana) => ana.val !== null)
      .map((ana) => {
        return {
          name: ana.nom ? ana.nom : `ANA${ana.num || '?'}`,
          value: ana.val,
        }
      }),
  }
}

/**
 *
 * Special polyline entity used by Location - History, Location - Realtime - Circuit
 * getNormalizedItem: To be able to retrieve item details from leaflet popup (Trip-history/Circuit-exec polyline popup)
 *
 * @param {*} linestring
 * @param {*} options
 * @returns
 */
function getPolylinesFromItemLinestring(item, options = {}) {
  return linestringsToPolylines(
    [
      {
        linestring: item.linestring,
        number: generateShortId('history_'), //To be able to highlight history related polylines (Leaflet)
      },
    ],
    {
      ...options,
      transform(obj) {
        obj = {
          ...obj,
          ...((options && options.transform && options.transform(obj)) || {}),
        }
        return obj
      },
    }
  )
}

/**
 * @todo Move to chrono-service
 * @param {*} value
 * @returns
 */
function getDriverChronoStatus(value) {
  return (
    {
      Travail: 'working',
      Repos: 'resting',
      Conduite: 'driving',
      Dispo: 'available',
    }[value] || value
  )
}