af83/chouette-core

View on GitHub
app/packs/src/journey_patterns/components/JourneyPattern.js

Summary

Maintainability
D
2 days
Test Coverage
import React from 'react'
import PropTypes from 'prop-types'
import { Path } from 'path-parser'
import Alpine from 'alpinejs'

import actions from '../actions'
import xCrsfToken from '../../helpers/xCrsfToken'

const path = new Path('/workbenches/:workbenchId/referentials/:referentialId/lines/:lineId/routes/:routeId')
const vehicleJourneysPath = new Path('/workbenches/:workbenchId/referentials/:referentialId/routes/:routeId/vehicle_journeys')
const params = path.partialTest(location.pathname)
class MutationBuilder {
  constructor(fetchingApi, fetchJourneyPatterns, enterEditMode) {
    this.fetchingApi = fetchingApi
    this.fetchJourneyPatterns = fetchJourneyPatterns
    this.enterEditMode = enterEditMode
  }

   prepare(url, action) {
    return () => fetch(url, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-CSRF-Token': xCrsfToken
      }
    })
      .then(res => res.json())
      .then(async () => {
        await this.fetchJourneyPatterns()
        await this.enterEditMode()

        this.handleResponse(action, 'notice')
      })
      .catch(() => this.handleResponse(action, 'error'))
  }

  handleResponse = (action, status) => {
    Alpine.store('flash').add({
      type: 'success',
      text: I18n.t(`flash.actions.${action}.${status}`, {
        resource_name: I18n.t(`activerecord.models.journey_pattern.one`)
      })
    })
  }
}
export default function JourneyPattern({
  editMode,
  enterEditMode,
  fetchingApi,
  fetchRouteCosts,
  fetchJourneyPatterns,
  index,
  onCheckboxChange,
  onDeleteJourneyPattern,
  onOpenEditModal,
  onUpdateJourneyPatternCosts,
  showHeader,
  status,
  value: journeyPattern
}) {
  let previousSpId = undefined
  const basePath = `${path.build(params)}/journey_patterns/${journeyPattern.id}`
  const shape = journeyPattern.shape || {}

  const hasShape = !!shape.id
  const canEditShape = !!shape?.has_waypoints

  const mutationBuilder = new MutationBuilder(fetchingApi, fetchJourneyPatterns, enterEditMode)
  const duplicateJourneyPattern = mutationBuilder.prepare(`${basePath}/duplicate`, 'duplicate')
  const unassociateShape = mutationBuilder.prepare(`${basePath}/unassociate_shape`, 'update')

  const updateCosts = ({ target: { dataset, name, value } }) => {
    const costs = {
      [dataset.costsKey]: {
        [name]: parseFloat(value)
      }
    }
    onUpdateJourneyPatternCosts(costs)
  }

  const vehicleJourneyURL = jpOid => {
    const vjURL = vehicleJourneysPath.build(params) + '?jp=' + jpOid

    return <a href={vjURL}>{I18n.t('journey_patterns.journey_pattern.vehicle_journey_at_stops')}</a>
  }

  const hasFeature = key => status.features[key]

  const cityNameChecker = (sp, i) => showHeader((sp.stop_area_object_id || sp.object_id) + "-" + i)

  const spNode = (sp, headlined) => (
    <div
      className={(headlined) ? 'headlined' : ''}
    >
      <div className={'link '}></div>
      <span className='has_radio'>
        <input
          onChange={onCheckboxChange}
          type='checkbox'
          id={sp.position}
          checked={sp.checked}
          disabled={(journeyPattern.deletable || status.policy['journey_patterns.update'] == false || editMode == false) ? 'disabled' : ''}
        >
        </input>
        <span className='radio-label'></span>
      </span>
    </div>
  )

  const isDisabled = action => !status.policy[`journey_patterns.${action}`]

  const totals = (onlyCommercial = false) =>{
    let totalTime = 0
    let totalDistance = 0
    let from = null
    journeyPattern.stop_points.map((stopPoint, i) => {
      let usePoint = stopPoint.checked
      if (onlyCommercial && (i == 0 || i == journeyPattern.stop_points.length - 1) && stopPoint.kind == "non_commercial") {
        usePoint = false
      }
      if (from && usePoint) {
        let [_costsKey, _costs, time, distance] = getTimeAndDistanceBetweenStops(from, stopPoint.id)
        totalTime += time
        totalDistance += distance
      }
      if (usePoint) {
        from = stopPoint.id
      }
    })
    return [formatTime(totalTime), formatDistance(totalDistance)]
  }

  const getTimeAndDistanceBetweenStops = (from, to) => {
    const costsKey = from + "-" + to
    const costs = getCosts(costsKey)
    const time = costs['time'] || 0
    const distance = costs['distance'] || 0
    return [costsKey, costs, time, distance]
  }

  const getCosts = costsKey => {
    const cost = journeyPattern.costs[costsKey]

    if (cost) return cost

    if (!journeyPattern.id) { fetchRouteCosts(costsKey) }

    return { distance: 0, time: 0 }
  }

  const formatDistance = distance => {
    if (distance < 1000) {
      return distance + " m"
    }
    else {
      return (distance / 1000).toFixed(2) + " km"
    }
  }

  const formatTime = time => {
    time = Number(time);
    var h = Math.floor(time / 3600);
    var m = Math.floor(time % 3600 / 60);
    var s = Math.floor(time % 3600 % 60);

    var hDisplay = h > 0 ? h + (" h ") : "";
    var mDisplay = m > 0 ? m + (" m ") : "";
    var sDisplay = s > 0 ? s + (" sec ") : "";
    return hDisplay + mDisplay + sDisplay;
  }

  const renderShapeEditorButtons = () => {
    const { id } = journeyPattern

    if (!hasFeature('shape_editor_experimental') || !editMode || !id) return []

    if (!hasShape) {
      return [
        <li key={`create_shape_${id}`}>
          <button
            type='button'
            onClick={onCreateShape}
          >
            {I18n.t('journey_patterns.actions.create_shape')}
          </button>
        </li>
      ]
    } else {
      return [
        ...canEditShape ?
          [
            <li key={`edit_shape_${id}`}>
              <button
                type='button'
                onClick={onEditShape}
              >
                {I18n.t('journey_patterns.actions.edit_shape')}
              </button>
            </li>
          ] :
          [],
        <li key={`unassociate_shape_${id}`}>
          <button
            type="button"
            onClick={unassociateShape}
          >
            {I18n.t('journey_patterns.actions.unassociate_shape')}
          </button>
        </li>
      ]
    }
  }

  const onCreateShape = () => { location.replace(`${basePath}/shapes/new`) }

  const onEditShape = () => { location.replace(`${basePath}/shapes/edit`) }

  const [totalTime, totalDistance] = totals(false)
  const [commercialTotalTime, commercialTotalDistance] = totals(true)

  const { deletable, id, object_id, short_id, stop_points } = journeyPattern

  return (
    <div className={'t2e-item' + (journeyPattern.deletable ? ' disabled' : '') + (object_id ? '' : ' to_record') + (journeyPattern.errors ? ' has-error' : '') + (hasFeature('costs_in_journey_patterns') ? ' with-costs' : '')}>
      <div className='th'>
        <div className='strong mb-xs'>{object_id ? short_id : '-'}</div>
        <div>{journeyPattern.registration_number}</div>
        <div>{I18n.t('journey_patterns.show.stop_points_count', { count: actions.getChecked(stop_points).length })}</div>
        {hasFeature('costs_in_journey_patterns') &&
          <div className="small row totals">
            <span className="col-md-5"><i className="fas fa-arrows-alt-h"></i>{totalDistance}</span>
            <span className="col-md-6"><i className="fa fa-clock"></i>{totalTime}</span>
          </div>
        }
        {hasFeature('costs_in_journey_patterns') &&
          <div className="small row totals commercial">
            <span className="col-md-5"><i className="fas fa-arrows-alt-h"></i>{commercialTotalDistance}</span>
            <span className="col-md-6"><i className="fa fa-clock"></i>{commercialTotalTime}</span>
          </div>
        }
        <div className={deletable ? 'btn-group disabled' : 'btn-group'}>
          <div
            className={deletable ? 'btn dropdown-toggle disabled' : 'btn dropdown-toggle'}
            data-toggle='dropdown'
          >
            <span className='fa fa-cog'></span>
          </div>
          <ul className='dropdown-menu'>
            <li key={`edit_journey_pattern_${id}`}>
              <button
                type='button'
                onClick={onOpenEditModal}
                data-toggle='modal'
                data-target='#JourneyPatternModal'
              >
                {editMode ? I18n.t('actions.edit') : I18n.t('actions.show')}
              </button>
            </li>
            {editMode && !!id && (
              <li key={`duplicate_journey_pattern_${id}`}>
                <button
                  type='button'
                  onClick={() => duplicateJourneyPattern()}
                >
                  {I18n.t('actions.duplicate')}
                </button>
              </li>
            )}
            {renderShapeEditorButtons()}
            <li key={`see_vehicle_journeys_${id}`} className={object_id ? '' : 'disabled'}>
              {object_id ? vehicleJourneyURL(object_id) : <a>{I18n.t('journey_patterns.journey_pattern.vehicle_journey_at_stops')}</a>}
            </li>
            <li key={`delete_journey_pattern_${id}`} className={'delete-action' + (isDisabled('destroy') || !editMode ? ' disabled' : '')}>
              <button
                type='button'
                className="disabled"
                disabled={isDisabled('destroy') || !editMode}
                onClick={(e) => {
                  e.preventDefault()
                  onDeleteJourneyPattern(index)
                }
                }
              >
                <span className='fa fa-trash'></span>{I18n.t('actions.destroy')}
              </button>
            </li>
          </ul>
        </div>
      </div>

      {stop_points.map((stopPoint, i) => {
        let costs = null
        let costsKey = null
        let time = null
        let distance = null
        let time_in_words = null
        if (previousSpId && stopPoint.checked) {
          [costsKey, costs, time, distance] = getTimeAndDistanceBetweenStops(previousSpId, stopPoint.id)
          time_in_words = formatTime(time)
        }
        if (stopPoint.checked) {
          previousSpId = stopPoint.id
        }
        let headlined = cityNameChecker(stopPoint, i)
        return (
          <div key={i} className={(stopPoint.checked ? 'activated' : 'deactivated') + (editMode ? ' edit-mode' : '')}>
            <div className={'td' + (headlined ? ' with-headline' : '')}>
              {spNode(stopPoint, headlined)}
            </div>
            {hasFeature('costs_in_journey_patterns') && costs && <div className='costs' id={'costs-' + id + '-' + costsKey}>
              {editMode && <div>
                <p>
                  <input type="number" value={costs['distance'] || 0} min='0' name="distance" step="1" onChange={updateCosts} data-costs-key={costsKey} />
                  <span>m</span>
                </p>
                <p>
                  <input type="number" value={costs['time'] || 0} min='0' name="time" onChange={updateCosts} data-costs-key={costsKey} />
                  <span>sec</span>
                </p>
              </div>}
              {!editMode && <div>
                <p><i className="fas fa-arrows-alt-h"></i>{formatDistance(costs['distance'] || 0)}</p>
                <p><i className="fa fa-clock"></i>{time_in_words}</p>
              </div>}
            </div>}
          </div>
        )
      })}
    </div>
  )
}

JourneyPattern.propTypes = {
  value: PropTypes.object,
  index: PropTypes.number,
  onCheckboxChange: PropTypes.func.isRequired,
  onOpenEditModal: PropTypes.func.isRequired,
  onDeleteJourneyPattern: PropTypes.func.isRequired,
  showHeader: PropTypes.func.isRequired,
  fetchRouteCosts: PropTypes.func.isRequired,
  onDuplicateJourneyPattern: PropTypes.func.isRequired,
  fetchJourneyPatterns: PropTypes.func.isRequired
}