aboutsummaryrefslogtreecommitdiffstats
path: root/app/javascript
diff options
context:
space:
mode:
authorcedricnjanga2017-10-06 11:50:05 +0200
committercedricnjanga2017-10-06 11:50:05 +0200
commitc4362d355d6f3b2f407e8c51bb6c0bee43f39df3 (patch)
treef6998aa0c07043f2b4b7aaf14d719831d05f1fd8 /app/javascript
parentb6f08e58fae35d5dd8a610af31c2950b37746695 (diff)
downloadchouette-core-c4362d355d6f3b2f407e8c51bb6c0bee43f39df3.tar.bz2
Add vehicle journey app
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/packs/vehicle_journeys/index.js102
-rw-r--r--app/javascript/vehicle_journeys/actions/index.js463
-rw-r--r--app/javascript/vehicle_journeys/batch.js26
-rw-r--r--app/javascript/vehicle_journeys/components/App.js38
-rw-r--r--app/javascript/vehicle_journeys/components/ConfirmModal.js40
-rw-r--r--app/javascript/vehicle_journeys/components/Filters.js168
-rw-r--r--app/javascript/vehicle_journeys/components/Navigate.js55
-rw-r--r--app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js42
-rw-r--r--app/javascript/vehicle_journeys/components/ToggleArrivals.js27
-rw-r--r--app/javascript/vehicle_journeys/components/Tools.js39
-rw-r--r--app/javascript/vehicle_journeys/components/VehicleJourney.js145
-rw-r--r--app/javascript/vehicle_journeys/components/VehicleJourneys.js156
-rw-r--r--app/javascript/vehicle_journeys/components/tools/CreateModal.js131
-rw-r--r--app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js26
-rw-r--r--app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js196
-rw-r--r--app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js167
-rw-r--r--app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js150
-rw-r--r--app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js114
-rw-r--r--app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js131
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js64
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js63
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js68
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js62
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/fr.js9
-rw-r--r--app/javascript/vehicle_journeys/containers/ConfirmModal.js30
-rw-r--r--app/javascript/vehicle_journeys/containers/Filters.js48
-rw-r--r--app/javascript/vehicle_journeys/containers/Navigate.js17
-rw-r--r--app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js28
-rw-r--r--app/javascript/vehicle_journeys/containers/ToggleArrivals.js21
-rw-r--r--app/javascript/vehicle_journeys/containers/Tools.js23
-rw-r--r--app/javascript/vehicle_journeys/containers/VehicleJourneysList.js32
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js37
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js22
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js30
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js36
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js33
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js30
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js37
-rw-r--r--app/javascript/vehicle_journeys/reducers/editMode.js10
-rw-r--r--app/javascript/vehicle_journeys/reducers/filters.js73
-rw-r--r--app/javascript/vehicle_journeys/reducers/index.js20
-rw-r--r--app/javascript/vehicle_journeys/reducers/modal.js138
-rw-r--r--app/javascript/vehicle_journeys/reducers/pagination.js37
-rw-r--r--app/javascript/vehicle_journeys/reducers/status.js17
-rw-r--r--app/javascript/vehicle_journeys/reducers/stopPointsList.js6
-rw-r--r--app/javascript/vehicle_journeys/reducers/vehicleJourneys.js227
46 files changed, 3434 insertions, 0 deletions
diff --git a/app/javascript/packs/vehicle_journeys/index.js b/app/javascript/packs/vehicle_journeys/index.js
new file mode 100644
index 000000000..38431af1d
--- /dev/null
+++ b/app/javascript/packs/vehicle_journeys/index.js
@@ -0,0 +1,102 @@
+import React from 'react'
+import { render } from 'react-dom'
+import { Provider } from 'react-redux'
+import { createStore } from 'redux'
+import vehicleJourneysApp from '../../vehicle_journeys/reducers'
+import App from '../../vehicle_journeys/components/App'
+import actions from "../../vehicle_journeys/actions"
+import { enableBatching } from '../../vehicle_journeys/batch'
+
+// logger, DO NOT REMOVE
+// var applyMiddleware = require('redux').applyMiddleware
+// var createLogger = require('redux-logger')
+// var thunkMiddleware = require('redux-thunk').default
+// var promise = require('redux-promise')
+
+var selectedJP = []
+
+if (window.journeyPatternId)
+ selectedJP.push(window.journeyPatternId)
+
+var initialState = {
+ editMode: false,
+ filters: {
+ selectedJourneyPatterns : selectedJP,
+ policy: window.perms,
+ toggleArrivals: false,
+ queryString: '',
+ query: {
+ interval: {
+ start:{
+ hour: '00',
+ minute: '00'
+ },
+ end:{
+ hour: '23',
+ minute: '59'
+ }
+ },
+ journeyPattern: {
+ published_name: ''
+ },
+ vehicleJourney: {
+ objectid: ''
+ },
+ company: {
+ name: ''
+ },
+ timetable: {
+ comment: ''
+ },
+ withoutSchedule: true,
+ withoutTimeTable: true
+ }
+
+ },
+ status: {
+ fetchSuccess: true,
+ isFetching: false
+ },
+ vehicleJourneys: [],
+ stopPointsList: window.stopPoints,
+ pagination: {
+ page : 1,
+ totalCount: 0,
+ perPage: window.vehicleJourneysPerPage,
+ stateChanged: false
+ },
+ modal: {
+ type: '',
+ modalProps: {},
+ confirmModal: {}
+ }
+}
+
+if (window.jpOrigin){
+ initialState.filters.query.journeyPattern = {
+ id: window.jpOrigin.id,
+ name: window.jpOrigin.name,
+ published_name: window.jpOrigin.published_name,
+ objectid: window.jpOrigin.objectid
+ }
+ let params = {
+ 'q[journey_pattern_id_eq]': initialState.filters.query.journeyPattern.id,
+ 'q[objectid_cont]': initialState.filters.query.vehicleJourney.objectid
+ }
+ initialState.filters.queryString = actions.encodeParams(params)
+}
+
+// const loggerMiddleware = createLogger()
+
+let store = createStore(
+ enableBatching(vehicleJourneysApp),
+ initialState,
+ // applyMiddleware(thunkMiddleware, promise, loggerMiddleware)
+)
+
+render(
+ <Provider store={store}>
+ <App />
+ </Provider>,
+ document.getElementById('vehicle_journeys_wip')
+) \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/actions/index.js b/app/javascript/vehicle_journeys/actions/index.js
new file mode 100644
index 000000000..4272c7915
--- /dev/null
+++ b/app/javascript/vehicle_journeys/actions/index.js
@@ -0,0 +1,463 @@
+import Promise from 'promise-polyfill'
+
+// To add to window
+if (!window.Promise) {
+ window.Promise = Promise;
+}
+import { batchActions } from '../batch'
+
+const actions = {
+ enterEditMode: () => ({
+ type: "ENTER_EDIT_MODE"
+ }),
+ exitEditMode: () => ({
+ type: "EXIT_EDIT_MODE"
+ }),
+ receiveVehicleJourneys : (json) => ({
+ type: "RECEIVE_VEHICLE_JOURNEYS",
+ json
+ }),
+ receiveErrors : (json) => ({
+ type: "RECEIVE_ERRORS",
+ json
+ }),
+ fetchingApi: () =>({
+ type: 'FETCH_API'
+ }),
+ unavailableServer : () => ({
+ type: 'UNAVAILABLE_SERVER'
+ }),
+ goToPreviousPage : (dispatch, pagination, queryString) => ({
+ type: 'GO_TO_PREVIOUS_PAGE',
+ dispatch,
+ pagination,
+ nextPage : false,
+ queryString
+ }),
+ goToNextPage : (dispatch, pagination, queryString) => ({
+ type: 'GO_TO_NEXT_PAGE',
+ dispatch,
+ pagination,
+ nextPage : true,
+ queryString
+ }),
+ checkConfirmModal : (event, callback, stateChanged, dispatch) => {
+ if(stateChanged === true){
+ return actions.openConfirmModal(callback)
+ }else{
+ dispatch(actions.fetchingApi())
+ return callback
+ }
+ },
+ openCreateModal : () => ({
+ type : 'CREATE_VEHICLEJOURNEY_MODAL'
+ }),
+ selectJPCreateModal : (selectedJP) => ({
+ type : 'SELECT_JP_CREATE_MODAL',
+ selectedItem: {
+ id: selectedJP.id,
+ objectid: selectedJP.object_id,
+ name: selectedJP.name,
+ published_name: selectedJP.published_name,
+ stop_areas: selectedJP.stop_area_short_descriptions
+ }
+ }),
+ openEditModal : (vehicleJourney) => ({
+ type : 'EDIT_VEHICLEJOURNEY_MODAL',
+ vehicleJourney
+ }),
+ openNotesEditModal : (vehicleJourney) => ({
+ type : 'EDIT_NOTES_VEHICLEJOURNEY_MODAL',
+ vehicleJourney
+ }),
+ toggleFootnoteModal : (footnote, isShown) => ({
+ type: 'TOGGLE_FOOTNOTE_MODAL',
+ footnote,
+ isShown
+ }),
+ openCalendarsEditModal : (vehicleJourneys) => ({
+ type : 'EDIT_CALENDARS_VEHICLEJOURNEY_MODAL',
+ vehicleJourneys
+ }),
+ selectTTCalendarsModal: (selectedTT) =>({
+ type: 'SELECT_TT_CALENDAR_MODAL',
+ selectedItem:{
+ id: selectedTT.id,
+ comment: selectedTT.comment,
+ objectid: selectedTT.objectid
+ }
+ }),
+ addSelectedTimetable: () => ({
+ type: 'ADD_SELECTED_TIMETABLE'
+ }),
+ deleteCalendarModal : (timetable) => ({
+ type : 'DELETE_CALENDAR_MODAL',
+ timetable
+ }),
+ editVehicleJourneyTimetables : (vehicleJourneys, timetables) => ({
+ type: 'EDIT_VEHICLEJOURNEYS_TIMETABLES',
+ vehicleJourneys,
+ timetables
+ }),
+ openShiftModal : () => ({
+ type : 'SHIFT_VEHICLEJOURNEY_MODAL'
+ }),
+ openDuplicateModal : () => ({
+ type : 'DUPLICATE_VEHICLEJOURNEY_MODAL'
+ }),
+ selectVehicleJourney : (index) => ({
+ type : 'SELECT_VEHICLEJOURNEY',
+ index
+ }),
+ cancelSelection : () => ({
+ type: 'CANCEL_SELECTION'
+ }),
+ addVehicleJourney : (data, selectedJourneyPattern, stopPointsList, selectedCompany) => ({
+ type: 'ADD_VEHICLEJOURNEY',
+ data,
+ selectedJourneyPattern,
+ stopPointsList,
+ selectedCompany
+ }),
+ select2Company: (selectedCompany) => ({
+ type: 'SELECT_CP_EDIT_MODAL',
+ selectedItem: {
+ id: selectedCompany.id,
+ name: selectedCompany.name,
+ objectid: selectedCompany.objectid
+ }
+ }),
+ unselect2Company: () => ({
+ type: 'UNSELECT_CP_EDIT_MODAL',
+ }),
+ editVehicleJourney : (data, selectedCompany) => ({
+ type: 'EDIT_VEHICLEJOURNEY',
+ data,
+ selectedCompany
+ }),
+ editVehicleJourneyNotes : (footnotes) => ({
+ type: 'EDIT_VEHICLEJOURNEY_NOTES',
+ footnotes
+ }),
+ shiftVehicleJourney : (addtionalTime) => ({
+ type: 'SHIFT_VEHICLEJOURNEY',
+ addtionalTime
+ }),
+ duplicateVehicleJourney : (addtionalTime, duplicateNumber, departureDelta) => ({
+ type: 'DUPLICATE_VEHICLEJOURNEY',
+ addtionalTime,
+ duplicateNumber,
+ departureDelta
+ }),
+ deleteVehicleJourneys : () => ({
+ type: 'DELETE_VEHICLEJOURNEYS'
+ }),
+ openConfirmModal : (callback) => ({
+ type : 'OPEN_CONFIRM_MODAL',
+ callback
+ }),
+ closeModal : () => ({
+ type : 'CLOSE_MODAL'
+ }),
+ resetValidation: (target) => {
+ $(target).parent().removeClass('has-error').children('.help-block').remove()
+ },
+ validateFields : (fields) => {
+ const test = []
+
+ Object.keys(fields).map(function(key) {
+ test.push(fields[key].validity.valid)
+ })
+ if(test.indexOf(false) >= 0) {
+ // Form is invalid
+ test.map(function(item, i) {
+ if(item == false) {
+ const k = Object.keys(fields)[i]
+ $(fields[k]).parent().addClass('has-error').children('.help-block').remove()
+ $(fields[k]).parent().append("<span class='small help-block'>" + fields[k].validationMessage + "</span>")
+ }
+ })
+ return false
+ } else {
+ // Form is valid
+ return true
+ }
+ },
+ toggleArrivals : () => ({
+ type: 'TOGGLE_ARRIVALS',
+ }),
+ updateTime : (val, subIndex, index, timeUnit, isDeparture, isArrivalsToggled) => ({
+ type: 'UPDATE_TIME',
+ val,
+ subIndex,
+ index,
+ timeUnit,
+ isDeparture,
+ isArrivalsToggled
+ }),
+ resetStateFilters: () => ({
+ type: 'RESET_FILTERS'
+ }),
+ toggleWithoutSchedule: () => ({
+ type: 'TOGGLE_WITHOUT_SCHEDULE'
+ }),
+ toggleWithoutTimeTable: () => ({
+ type: 'TOGGLE_WITHOUT_TIMETABLE'
+ }),
+ updateStartTimeFilter: (val, unit) => ({
+ type: 'UPDATE_START_TIME_FILTER',
+ val,
+ unit
+ }),
+ updateEndTimeFilter: (val, unit) => ({
+ type: 'UPDATE_END_TIME_FILTER',
+ val,
+ unit
+ }),
+ filterSelect2Timetable: (selectedTT) =>({
+ type: 'SELECT_TT_FILTER',
+ selectedItem:{
+ id: selectedTT.id,
+ comment: selectedTT.comment,
+ objectid: selectedTT.objectid
+ }
+ }),
+ filterSelect2JourneyPattern: (selectedJP) => ({
+ type : 'SELECT_JP_FILTER',
+ selectedItem: {
+ id: selectedJP.id,
+ objectid: selectedJP.object_id,
+ name: selectedJP.name,
+ published_name: selectedJP.published_name
+ }
+ }),
+ filterSelect2VehicleJourney: (selectedVJ) => ({
+ type : 'SELECT_VJ_FILTER',
+ selectedItem: {
+ objectid: selectedVJ.objectid
+ }
+ }),
+ createQueryString: () => ({
+ type: 'CREATE_QUERY_STRING'
+ }),
+ resetPagination: () => ({
+ type: 'RESET_PAGINATION'
+ }),
+ queryFilterVehicleJourneys: (dispatch) => ({
+ type: 'QUERY_FILTER_VEHICLEJOURNEYS',
+ dispatch
+ }),
+ resetFilters: (dispatch) => (
+ batchActions([
+ actions.resetStateFilters(),
+ actions.resetPagination(),
+ actions.queryFilterVehicleJourneys(dispatch)
+ ])
+ ),
+ filterQuery: (dispatch) => (
+ batchActions([
+ actions.createQueryString(),
+ actions.resetPagination(),
+ actions.queryFilterVehicleJourneys(dispatch)
+ ])
+ ),
+ updateTotalCount: (diff) => ({
+ type: 'UPDATE_TOTAL_COUNT',
+ diff
+ }),
+ receiveTotalCount: (total) => ({
+ type: 'RECEIVE_TOTAL_COUNT',
+ total
+ }),
+ humanOID: (oid) => oid.split(':')[2].split("-").pop(),
+ fetchVehicleJourneys : (dispatch, currentPage, nextPage, queryString) => {
+ if(currentPage == undefined){
+ currentPage = 1
+ }
+ let vehicleJourneys = []
+ let page
+ switch (nextPage) {
+ case true:
+ page = currentPage + 1
+ break
+ case false:
+ if(currentPage > 1){
+ page = currentPage - 1
+ }
+ break
+ default:
+ page = currentPage
+ break
+ }
+ let str = ".json"
+ let sep = '?'
+ if(page > 1){
+ str = '.json?page=' + page.toString()
+ sep = '&'
+ }
+ let urlJSON = window.location.pathname + str
+ if (queryString){
+ urlJSON = urlJSON + sep + queryString
+ }
+ let hasError = false
+ fetch(urlJSON, {
+ credentials: 'same-origin',
+ }).then(response => {
+ if(response.status == 500) {
+ hasError = true
+ }
+ return response.json()
+ }).then((json) => {
+ if(hasError == true) {
+ dispatch(actions.unavailableServer())
+ } else {
+ let val
+ for (val of json.vehicle_journeys){
+ var timeTables = []
+ let tt
+ for (tt of val.time_tables){
+ timeTables.push({
+ objectid: tt.objectid,
+ comment: tt.comment,
+ id: tt.id,
+ color: tt.color
+ })
+ }
+ let vjasWithDelta = val.vehicle_journey_at_stops.map((vjas, i) => {
+ actions.fillEmptyFields(vjas)
+ return actions.getDelta(vjas)
+ })
+ vehicleJourneys.push({
+ journey_pattern: val.journey_pattern,
+ published_journey_name: val.published_journey_name,
+ objectid: val.objectid,
+ footnotes: val.footnotes,
+ time_tables: timeTables,
+ vehicle_journey_at_stops: vjasWithDelta,
+ deletable: false,
+ selected: false,
+ published_journey_name: val.published_journey_name || 'non renseigné',
+ published_journey_identifier: val.published_journey_identifier || 'non renseigné',
+ company: val.company || 'non renseigné',
+ transport_mode: val.route.line.transport_mode || 'undefined',
+ transport_submode: val.route.line.transport_submode || 'undefined'
+ })
+ }
+ window.currentItemsLength = vehicleJourneys.length
+ dispatch(actions.receiveVehicleJourneys(vehicleJourneys))
+ dispatch(actions.receiveTotalCount(json.total))
+ }
+ })
+ },
+ submitVehicleJourneys : (dispatch, state, next) => {
+ dispatch(actions.fetchingApi())
+ let urlJSON = window.location.pathname + "_collection.json"
+ let hasError = false
+ fetch(urlJSON, {
+ credentials: 'same-origin',
+ method: 'PATCH',
+ contentType: 'application/json; charset=utf-8',
+ Accept: 'application/json',
+ body: JSON.stringify(state),
+ headers: {
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
+ }
+ }).then(response => {
+ if(!response.ok) {
+ hasError = true
+ }
+ return response.json()
+ }).then((json) => {
+ if(hasError == true) {
+ dispatch(actions.receiveErrors(json))
+ } else {
+ if(next) {
+ dispatch(next)
+ } else {
+ if(json.length != window.currentItemsLength){
+ dispatch(actions.updateTotalCount(window.currentItemsLength - json.length))
+ }
+ window.currentItemsLength = json.length
+ dispatch(actions.exitEditMode())
+ dispatch(actions.receiveVehicleJourneys(json))
+ }
+ }
+ })
+ },
+ // VJAS HELPERS
+ getSelected: (vj) => {
+ return vj.filter((obj) =>{
+ return obj.selected
+ })
+ },
+ simplePad: (d) => {
+ if(d.toString().length == 1){
+ return '0' + d.toString()
+ }else{
+ return d.toString()
+ }
+ },
+ pad: (d, timeUnit) => {
+ let val = d.toString()
+ if(d.toString().length == 1){
+ val = (d < 10) ? '0' + d.toString() : d.toString();
+ }
+ if(val.length > 2){
+ val = val.substr(1)
+ }
+ if(timeUnit == 'minute' && parseInt(val) > 59){
+ val = '59'
+ }
+ if(timeUnit == 'hour' && parseInt(val) > 23){
+ val = '23'
+ }
+ return val
+ },
+ encodeParams: (params) => {
+ let esc = encodeURIComponent
+ let queryString = Object.keys(params).map((k) => esc(k) + '=' + esc(params[k])).join('&')
+ return queryString
+ },
+ fillEmptyFields: (vjas) => {
+ if (vjas.departure_time.hour == null) vjas.departure_time.hour = '00'
+ if (vjas.departure_time.minute == null) vjas.departure_time.minute = '00'
+ if (vjas.arrival_time.hour == null) vjas.arrival_time.hour = '00'
+ if (vjas.arrival_time.minute == null) vjas.arrival_time.minute = '00'
+ return vjas
+ },
+ getDuplicateDelta: (original, newDeparture) => {
+ if (original.departure_time.hour != '' && original.departure_time.minute != '' && newDeparture.departure_time.hour != undefined && newDeparture.departure_time.minute != undefined){
+ return (newDeparture.departure_time.hour - parseInt(original.departure_time.hour)) * 60 + (newDeparture.departure_time.minute - parseInt(original.departure_time.minute))
+ }
+ return 0
+ },
+ getDelta: (vjas) => {
+ let delta = 0
+ if (vjas.departure_time.hour != '' && vjas.departure_time.minute != '' && vjas.arrival_time.hour != '' && vjas.departure_time.minute != ''){
+ delta = (parseInt(vjas.departure_time.hour) - parseInt(vjas.arrival_time.hour)) * 60 + (parseInt(vjas.departure_time.minute) - parseInt(vjas.arrival_time.minute))
+ }
+ vjas.delta = delta
+ return vjas
+ },
+ getShiftedSchedule: ({departure_time, arrival_time}, additional_time) => {
+ // We create dummy dates objects to manipulate time more easily
+ let departureDT = new Date (Date.UTC(2017, 2, 1, parseInt(departure_time.hour), parseInt(departure_time.minute)))
+ let arrivalDT = new Date (Date.UTC(2017, 2, 1, parseInt(arrival_time.hour), parseInt(arrival_time.minute)))
+
+ let newDepartureDT = new Date (departureDT.getTime() + additional_time * 60000)
+ let newArrivalDT = new Date (arrivalDT.getTime() + additional_time * 60000)
+
+ return {
+ departure_time: {
+ hour: actions.simplePad(newDepartureDT.getUTCHours()),
+ minute: actions.simplePad(newDepartureDT.getUTCMinutes())
+ },
+ arrival_time: {
+ hour: actions.simplePad(newArrivalDT.getUTCHours()),
+ minute: actions.simplePad(newArrivalDT.getUTCMinutes())
+ }
+ }
+ },
+}
+
+export default actions
diff --git a/app/javascript/vehicle_journeys/batch.js b/app/javascript/vehicle_journeys/batch.js
new file mode 100644
index 000000000..ea08572aa
--- /dev/null
+++ b/app/javascript/vehicle_journeys/batch.js
@@ -0,0 +1,26 @@
+// 'use strict';
+
+// Object.defineProperty(exports, "__esModule", {
+// value: true
+// });
+// exports.batchActions = batchActions;
+// exports.enableBatching = enableBatching;
+// var BATCH = exports.BATCH = 'BATCH';
+
+export function batchActions(actions) {
+ return {
+ type: 'BATCH',
+ payload: actions
+ };
+}
+
+export function enableBatching(reduce) {
+ return function batchingReducer(state, action) {
+ switch (action.type) {
+ case 'BATCH':
+ return action.payload.reduce(batchingReducer, state);
+ default:
+ return reduce(state, action);
+ }
+ }
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/App.js b/app/javascript/vehicle_journeys/components/App.js
new file mode 100644
index 000000000..8e5f7aa9d
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/App.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import VehicleJourneysList from '../containers/VehicleJourneysList'
+import Navigate from '../containers/Navigate'
+import ToggleArrivals from '../containers/ToggleArrivals'
+import Filters from '../containers/Filters'
+import SaveVehicleJourneys from '../containers/SaveVehicleJourneys'
+import ConfirmModal from '../containers/ConfirmModal'
+import Tools from '../containers/Tools'
+
+export default function App() {
+ return (
+ <div>
+
+ <div className='row'>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'>
+ <ToggleArrivals />
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right'>
+ <Navigate />
+ </div>
+ </div>
+
+ <Filters />
+ <VehicleJourneysList />
+
+ <div className='row'>
+ <div className='col-lg-12 text-right'>
+ <Navigate />
+ </div>
+ </div>
+
+ <SaveVehicleJourneys />
+ <Tools />
+
+ <ConfirmModal />
+ </div>
+ )
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/ConfirmModal.js b/app/javascript/vehicle_journeys/components/ConfirmModal.js
new file mode 100644
index 000000000..df3c96c48
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/ConfirmModal.js
@@ -0,0 +1,40 @@
+import React, { PropTypes, Component } from 'react'
+
+export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCancel, vehicleJourneys}) {
+ return (
+ <div className={'modal fade ' + ((modal.type == 'confirm') ? 'in' : '')} id='ConfirmModal'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-body'>
+ <p> Voulez-vous valider vos modifications avant de changer de page? </p>
+ </div>
+ <div className='modal-footer'>
+ <button
+ className='btn btn-default'
+ data-dismiss='modal'
+ type='button'
+ onClick={() => { onModalCancel(modal.confirmModal.callback) }}
+ >
+ Ne pas valider
+ </button>
+ <button
+ className='btn btn-danger'
+ data-dismiss='modal'
+ type='button'
+ onClick={() => { onModalAccept(modal.confirmModal.callback, vehicleJourneys) }}
+ >
+ Valider
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ConfirmModal.propTypes = {
+ vehicleJourneys: PropTypes.array.isRequired,
+ modal: PropTypes.object.isRequired,
+ onModalAccept: PropTypes.func.isRequired,
+ onModalCancel: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/Filters.js b/app/javascript/vehicle_journeys/components/Filters.js
new file mode 100644
index 000000000..db6707520
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/Filters.js
@@ -0,0 +1,168 @@
+import React, { PropTypes } from 'react'
+import MissionSelect2 from'./tools/select2s/MissionSelect2'
+import VJSelect2 from'./tools/select2s/VJSelect2'
+import TimetableSelect2 from'./tools/select2s/TimetableSelect2'
+
+export default function Filters({filters, pagination, onFilter, onResetFilters, onUpdateStartTimeFilter, onUpdateEndTimeFilter, onToggleWithoutSchedule, onToggleWithoutTimeTable, onSelect2Timetable, onSelect2JourneyPattern, onSelect2VehicleJourney}) {
+ return (
+ <div className='row'>
+ <div className='col-lg-12'>
+ <div className='form form-filter'>
+ <div className='ffg-row'>
+ {/* ID course */}
+ <div className="form-group w33">
+ <VJSelect2
+ onSelect2VehicleJourney={onSelect2VehicleJourney}
+ filters={filters}
+ isFilter={true}
+ />
+ </div>
+
+ {/* Missions */}
+ <div className='form-group w33'>
+ <MissionSelect2
+ onSelect2JourneyPattern={onSelect2JourneyPattern}
+ filters={filters}
+ isFilter={true}
+ />
+ </div>
+
+ {/* Calendriers */}
+ <div className='form-group w33'>
+ <TimetableSelect2
+ onSelect2Timetable={onSelect2Timetable}
+ hasRoute={true}
+ chunkURL={("/autocomplete_time_tables.json?route_id=" + String(window.route_id))}
+ filters={filters}
+ isFilter={true}
+ />
+ </div>
+ </div>
+
+ <div className='ffg-row'>
+ {/* Plage horaire */}
+ <div className='form-group togglable'>
+ <label className='control-label'>Plage horaire au départ de la course</label>
+ <div className='filter_menu'>
+ <div className='form-group time filter_menu-item'>
+ <label className='control-label time'>Début</label>
+ <div className='form-inline'>
+ <div className='input-group time'>
+ <input
+ type='number'
+ className='form-control'
+ min='00'
+ max='23'
+ onChange={(e) => {onUpdateStartTimeFilter(e, 'hour')}}
+ value={filters.query.interval.start.hour}
+ />
+ <span>:</span>
+ <input
+ type='number'
+ className='form-control'
+ min='00'
+ max='59'
+ onChange={(e) => {onUpdateStartTimeFilter(e, 'minute')}}
+ value={filters.query.interval.start.minute}
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group time filter_menu-item'>
+ <label className='control-label time'>Fin</label>
+ <div className='form-inline'>
+ <div className='input-group time'>
+ <input
+ type='number'
+ className='form-control'
+ min='00'
+ max='23'
+ onChange={(e) => {onUpdateEndTimeFilter(e, 'hour')}}
+ value={filters.query.interval.end.hour}
+ />
+ <span>:</span>
+ <input
+ type='number'
+ className='form-control'
+ min='00'
+ max='59'
+ onChange={(e) => {onUpdateEndTimeFilter(e, 'minute')}}
+ value={filters.query.interval.end.minute}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* Switch avec/sans horaires */}
+ <div className='form-group has_switch'>
+ <label className='control-label pull-left'>Afficher les courses sans horaires</label>
+ <div className='form-group pull-left' style={{padding: 0}}>
+ <div className='checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ onChange={onToggleWithoutSchedule}
+ checked={filters.query.withoutSchedule}
+ ></input>
+ <span className='switch-label' data-checkedvalue='Non' data-uncheckedvalue='Oui'>
+ {filters.query.withoutSchedule ? 'Oui' : 'Non'}
+ </span>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div className="ffg-row">
+ {/* Switch avec/sans calendrier */}
+ <div className='form-group has_switch'>
+ <label className='control-label pull-left'>Afficher les courses avec calendrier</label>
+ <div className='form-group pull-left' style={{padding: 0}}>
+ <div className='checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ onChange={onToggleWithoutTimeTable}
+ checked={filters.query.withoutTimeTable}
+ ></input>
+ <span className='switch-label' data-checkedvalue='Non' data-uncheckedvalue='Oui'>
+ {filters.query.withoutTimeTable ? 'Oui' : 'Non'}
+ </span>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* Actions */}
+ <div className='actions'>
+ <span
+ className='btn btn-link'
+ onClick={(e) => onResetFilters(e, pagination)}>
+ Effacer
+ </span>
+ <span
+ className='btn btn-default'
+ onClick={(e) => onFilter(e, pagination)}>
+ Filtrer
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+Filters.propTypes = {
+ filters : PropTypes.object.isRequired,
+ pagination : PropTypes.object.isRequired,
+ onFilter: PropTypes.func.isRequired,
+ onResetFilters: PropTypes.func.isRequired,
+ onUpdateStartTimeFilter: PropTypes.func.isRequired,
+ onUpdateEndTimeFilter: PropTypes.func.isRequired,
+ onSelect2Timetable: PropTypes.func.isRequired,
+ onSelect2JourneyPattern: PropTypes.func.isRequired,
+ onSelect2VehicleJourney: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/Navigate.js b/app/javascript/vehicle_journeys/components/Navigate.js
new file mode 100644
index 000000000..7493b705b
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/Navigate.js
@@ -0,0 +1,55 @@
+import React, { PropTypes, Component } from 'react'
+import actions from'../actions'
+
+export default function Navigate({ dispatch, vehicleJourneys, pagination, status, filters}) {
+ let firstPage = 1
+ let lastPage = Math.ceil(pagination.totalCount / pagination.perPage)
+ let minVJ = (pagination.page - 1) * pagination.perPage + 1
+ if (pagination.totalCount == 0){
+ minVJ = 0
+ lastPage = 1
+ }
+ let maxVJ = Math.min((pagination.page * pagination.perPage), pagination.totalCount)
+ if(status.isFetching == true) {
+ return false
+ }
+ if(status.fetchSuccess == true) {
+ return (
+ <div className="pagination">
+ Liste des horaires {minVJ} à {maxVJ} sur {pagination.totalCount}
+
+ <form className='page_links' onSubmit={e => {e.preventDefault()}}>
+ <button
+ onClick={e => {
+ e.preventDefault()
+ dispatch(actions.checkConfirmModal(e, actions.goToPreviousPage(dispatch, pagination, filters.queryString), pagination.stateChanged, dispatch))
+ }}
+ type='button'
+ data-target='#ConfirmModal'
+ className={(pagination.page == firstPage ? 'disabled ' : '') + 'previous_page'}
+ disabled={(pagination.page == firstPage ? 'disabled' : '')}
+ ></button>
+ <button
+ onClick={e => {
+ e.preventDefault()
+ dispatch(actions.checkConfirmModal(e, actions.goToNextPage(dispatch, pagination, filters.queryString), pagination.stateChanged, dispatch))
+ }}
+ type='button'
+ data-target='#ConfirmModal'
+ className={(pagination.page == lastPage ? 'disabled ' : '') + 'next_page'}
+ disabled={(pagination.page == lastPage ? 'disabled' : '')}
+ ></button>
+ </form>
+ </div>
+ )
+ } else {
+ return false
+ }
+}
+
+Navigate.propTypes = {
+ vehicleJourneys: PropTypes.array.isRequired,
+ status: PropTypes.object.isRequired,
+ pagination: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js b/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js
new file mode 100644
index 000000000..e8c27f92e
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js
@@ -0,0 +1,42 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class SaveVehicleJourneys extends Component{
+ constructor(props){
+ super(props)
+ }
+
+ render() {
+ if (this.props.filters.policy['vehicle_journeys.update'] == false) {
+ return false
+ }else{
+ return (
+ <div className='row mt-md'>
+ <div className='col-lg-12 text-right'>
+ <form className='vehicle_journeys formSubmitr ml-xs' onSubmit={e => {e.preventDefault()}}>
+ <button
+ className='btn btn-default'
+ type='button'
+ onClick={e => {
+ e.preventDefault()
+ this.props.editMode ? this.props.onSubmitVehicleJourneys(this.props.dispatch, this.props.vehicleJourneys) : this.props.onEnterEditMode()
+ }}
+ >
+ {this.props.editMode ? "Valider" : "Editer"}
+ </button>
+ </form>
+ </div>
+ </div>
+ )
+ }
+ }
+}
+
+SaveVehicleJourneys.propTypes = {
+ vehicleJourneys: PropTypes.array.isRequired,
+ page: PropTypes.number.isRequired,
+ status: PropTypes.object.isRequired,
+ filters: PropTypes.object.isRequired,
+ onEnterEditMode: PropTypes.func.isRequired,
+ onSubmitVehicleJourneys: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/ToggleArrivals.js b/app/javascript/vehicle_journeys/components/ToggleArrivals.js
new file mode 100644
index 000000000..e26ceec3a
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/ToggleArrivals.js
@@ -0,0 +1,27 @@
+import React, { PropTypes } from 'react'
+
+export default function ToggleArrivals({filters, onToggleArrivals}) {
+ return (
+ <div className='has_switch form-group inline'>
+ <label htmlFor='toggleArrivals' className='control-label'>Afficher et éditer les horaires d'arrivée</label>
+ <div className='form-group'>
+ <div className='checkbox'>
+ <label>
+ <input
+ onChange={onToggleArrivals}
+ type='checkbox'
+ checked={filters.toggleArrivals}
+ >
+ </input>
+ <span className='switch-label'></span>
+ </label>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ToggleArrivals.propTypes = {
+ filters : PropTypes.object.isRequired,
+ onToggleArrivals: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/Tools.js b/app/javascript/vehicle_journeys/components/Tools.js
new file mode 100644
index 000000000..a717408b9
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/Tools.js
@@ -0,0 +1,39 @@
+import React, { PropTypes } from 'react'
+import actions from '../actions'
+import AddVehicleJourney from '../containers/tools/AddVehicleJourney'
+import DeleteVehicleJourneys from '../containers/tools/DeleteVehicleJourneys'
+import ShiftVehicleJourney from '../containers/tools/ShiftVehicleJourney'
+import DuplicateVehicleJourney from '../containers/tools/DuplicateVehicleJourney'
+import EditVehicleJourney from '../containers/tools/EditVehicleJourney'
+import NotesEditVehicleJourney from '../containers/tools/NotesEditVehicleJourney'
+import TimetablesEditVehicleJourney from '../containers/tools/TimetablesEditVehicleJourney'
+
+export default function Tools({vehicleJourneys, onCancelSelection, filters: {policy}, editMode}) {
+ return (
+ <div>
+ {
+ (policy['vehicle_journeys.create'] && policy['vehicle_journeys.update'] && policy['vehicle_journeys.destroy'] && editMode) &&
+ <div className='select_toolbox'>
+ <ul>
+ <AddVehicleJourney />
+ <DuplicateVehicleJourney />
+ <ShiftVehicleJourney />
+ <EditVehicleJourney />
+ <TimetablesEditVehicleJourney />
+ <NotesEditVehicleJourney />
+ <DeleteVehicleJourneys />
+ </ul>
+
+ <span className='info-msg'>{actions.getSelected(vehicleJourneys).length} course(s) sélectionnée(s)</span>
+ <button className='btn btn-xs btn-link pull-right' onClick={onCancelSelection}>Annuler la sélection</button>
+ </div>
+ }
+ </div>
+ )
+}
+
+Tools.propTypes = {
+ vehicleJourneys : PropTypes.array.isRequired,
+ onCancelSelection: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/VehicleJourney.js b/app/javascript/vehicle_journeys/components/VehicleJourney.js
new file mode 100644
index 000000000..cb5407f81
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/VehicleJourney.js
@@ -0,0 +1,145 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class VehicleJourney extends Component {
+ constructor(props) {
+ super(props)
+ this.previousCity = undefined
+ }
+
+ cityNameChecker(sp) {
+ let bool = false
+ if(sp.stop_area_cityname != this.previousCity){
+ bool = true
+ this.previousCity = sp.stop_area_cityname
+ }
+
+ return bool
+ }
+
+ timeTableURL(tt) {
+ let refURL = window.location.pathname.split('/', 3).join('/')
+ let ttURL = refURL + '/time_tables/' + tt.id
+
+ return (
+ <a href={ttURL} title='Voir le calendrier'><span className='fa fa-calendar' style={{color: (tt.color ? tt.color : '')}}></span></a>
+ )
+ }
+
+ columnHasDelta() {
+ let a = []
+ this.props.value.vehicle_journey_at_stops.map((vj, i) => {
+ a.push(vj.delta)
+ })
+ let b = a.reduce((p, c) => p+c, 0)
+
+ if(b > 0) {
+ return true
+ }
+ }
+
+ isDisabled(bool1, bool2) {
+ return (bool1 || bool2)
+ }
+
+ render() {
+ this.previousCity = undefined
+
+ return (
+ <div className={'t2e-item' + (this.props.value.deletable ? ' disabled' : '') + (this.props.value.errors ? ' has-error': '')}>
+ <div className='th'>
+ <div className='strong mb-xs'>{this.props.value.objectid ? actions.humanOID(this.props.value.objectid) : '-'}</div>
+ <div>{actions.humanOID(this.props.value.journey_pattern.objectid)}</div>
+ <div>
+ {this.props.value.time_tables.map((tt, i)=>
+ <span key={i} className='vj_tt'>{this.timeTableURL(tt)}</span>
+ )}
+ </div>
+
+ {(this.props.filters.policy['vehicle_journeys.update'] == true && this.props.editMode) &&
+ <div className={(this.props.value.deletable ? 'disabled ' : '') + 'checkbox'}>
+ <input
+ id={this.props.index}
+ name={this.props.index}
+ style={{display: 'none'}}
+ onChange={(e) => this.props.onSelectVehicleJourney(this.props.index)}
+ type='checkbox'
+ disabled={this.props.value.deletable}
+ checked={this.props.value.selected}
+ ></input>
+ <label htmlFor={this.props.index}></label>
+ </div>
+ }
+
+ </div>
+ {this.props.value.vehicle_journey_at_stops.map((vj, i) =>
+ <div key={i} className='td text-center'>
+ <div className={'cellwrap' + (this.cityNameChecker(vj) ? ' headlined' : '')}>
+ {this.props.filters.toggleArrivals &&
+ <div data-headline='Arrivée à'>
+ <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false) ? 'disabled ' : '') + 'input-group time'}>
+ <input
+ type='number'
+ min='00'
+ max='23'
+ className='form-control'
+ disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', false, false)}}
+ value={vj.arrival_time['hour']}
+ />
+ <span>:</span>
+ <input
+ type='number'
+ min='00'
+ max='59'
+ className='form-control'
+ disabled={((this.isDisabled(this.props.value.deletable), vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'minute', false, false)}}
+ value={vj.arrival_time['minute']}
+ />
+ </span>
+ </div>
+ }
+ <div className={(this.columnHasDelta() ? '' : 'hidden')}>
+ {(vj.delta != 0) &&
+ <span className='sb sb-chrono sb-lg text-warning' data-textinside={vj.delta}></span>
+ }
+ </div>
+ <div data-headline='Départ à'>
+ <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false) ? 'disabled ' : '') + 'input-group time'}>
+ <input
+ type='number'
+ min='00'
+ max='23'
+ className='form-control'
+ disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', true, this.props.filters.toggleArrivals)}}
+ value={vj.departure_time['hour']}
+ />
+ <span>:</span>
+ <input
+ type='number'
+ min='00'
+ max='59'
+ className='form-control'
+ disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, "minute", true, this.props.filters.toggleArrivals)}}
+ value={vj.departure_time['minute']}
+ />
+ </span>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ )
+ }
+}
+
+VehicleJourney.propTypes = {
+ value: PropTypes.object.isRequired,
+ filters: PropTypes.object.isRequired,
+ index: PropTypes.number.isRequired,
+ onUpdateTime: PropTypes.func.isRequired,
+ onSelectVehicleJourney: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/VehicleJourneys.js b/app/javascript/vehicle_journeys/components/VehicleJourneys.js
new file mode 100644
index 000000000..6bce9766b
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/VehicleJourneys.js
@@ -0,0 +1,156 @@
+import React, { PropTypes, Component } from 'react'
+import _ from 'lodash'
+import VehicleJourney from './VehicleJourney'
+
+
+export default class VehicleJourneys extends Component {
+ constructor(props){
+ super(props)
+ this.previousCity = undefined
+ }
+ componentDidMount() {
+ this.props.onLoadFirstPage(this.props.filters)
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if(this.props.status.isFetching == false){
+ $('.table-2entries').each(function() {
+ var refH = []
+ var refCol = []
+
+ $(this).find('.t2e-head').children('div').each(function() {
+ var h = $(this).outerHeight();
+ refH.push(h)
+ });
+
+ var i = 0
+ $(this).find('.t2e-item').children('div').each(function() {
+ var h = $(this).outerHeight();
+ if(refCol.length < refH.length){
+ refCol.push(h)
+ } else {
+ if(h > refCol[i]) {
+ refCol[i] = h
+ }
+ }
+ if(i == (refH.length - 1)){
+ i = 0
+ } else {
+ i++
+ }
+ });
+
+ for(var n = 0; n < refH.length; n++) {
+ if(refCol[n] < refH[n]) {
+ refCol[n] = refH[n]
+ }
+ }
+
+ $(this).find('.th').css('height', refCol[0]);
+
+ for(var nth = 1; nth < refH.length; nth++) {
+ $(this).find('.td:nth-child('+ (nth + 1) +')').css('height', refCol[nth]);
+ }
+ });
+ }
+ }
+
+ cityNameChecker(sp) {
+ let bool = false
+ if(sp.city_name != this.previousCity){
+ bool = true
+ this.previousCity = sp.city_name
+ }
+ return (
+ <div
+ className={(bool) ? 'headlined' : ''}
+ data-headline={(bool) ? sp.city_name : ''}
+ title={sp.city_name + ' (' + sp.zip_code +')'}
+ >
+ <span><span>{sp.name}</span></span>
+ </div>
+ )
+ }
+
+ render() {
+ this.previousCity = undefined
+
+ if(this.props.status.isFetching == true) {
+ return (
+ <div className="isLoading" style={{marginTop: 80, marginBottom: 80}}>
+ <div className="loader"></div>
+ </div>
+ )
+ } else {
+ return (
+ <div className='row'>
+ <div className='col-lg-12'>
+ {(this.props.status.fetchSuccess == false) && (
+ <div className='alert alert-danger mt-sm'>
+ <strong>Erreur : </strong>
+ la récupération des missions a rencontré un problème. Rechargez la page pour tenter de corriger le problème.
+ </div>
+ )}
+
+ { _.some(this.props.vehicleJourneys, 'errors') && (
+ <div className="alert alert-danger mt-sm">
+ <strong>Erreur : </strong>
+ {this.props.vehicleJourneys.map((vj, index) =>
+ vj.errors && vj.errors.map((err, i) => {
+ return (
+ <ul key={i}>
+ <li>{err}</li>
+ </ul>
+ )
+ })
+ )}
+ </div>
+ )}
+
+ <div className={'table table-2entries mt-sm mb-sm' + ((this.props.vehicleJourneys.length > 0) ? '' : ' no_result')}>
+ <div className='t2e-head w20'>
+ <div className='th'>
+ <div className='strong mb-xs'>ID course</div>
+ <div>ID mission</div>
+ <div>Calendriers</div>
+ </div>
+ {this.props.stopPointsList.map((sp, i) =>{
+ return (
+ <div key={i} className='td'>
+ {this.cityNameChecker(sp)}
+ </div>
+ )
+ })}
+ </div>
+
+ <div className='t2e-item-list w80'>
+ <div>
+ {this.props.vehicleJourneys.map((vj, index) =>
+ <VehicleJourney
+ value={vj}
+ key={index}
+ index={index}
+ editMode={this.props.editMode}
+ filters={this.props.filters}
+ onUpdateTime={this.props.onUpdateTime}
+ onSelectVehicleJourney={this.props.onSelectVehicleJourney}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+ }
+}
+
+VehicleJourneys.propTypes = {
+ status: PropTypes.object.isRequired,
+ filters: PropTypes.object.isRequired,
+ stopPointsList: PropTypes.array.isRequired,
+ onLoadFirstPage: PropTypes.func.isRequired,
+ onUpdateTime: PropTypes.func.isRequired,
+ onSelectVehicleJourney: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/CreateModal.js b/app/javascript/vehicle_journeys/components/tools/CreateModal.js
new file mode 100644
index 000000000..5b5e2f849
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/CreateModal.js
@@ -0,0 +1,131 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../../actions'
+import MissionSelect2 from './select2s/MissionSelect2'
+import CompanySelect2 from './select2s/CompanySelect2'
+
+export default class CreateModal extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ handleSubmit() {
+ if(actions.validateFields(this.refs) == true && this.props.modal.modalProps.selectedJPModal) {
+ this.props.onAddVehicleJourney(this.refs, this.props.modal.modalProps.selectedJPModal, this.props.stopPointsList, this.props.modal.modalProps.selectedCompany)
+ this.props.onModalClose()
+ $('#NewVehicleJourneyModal').modal('hide')
+ }
+ }
+
+ render() {
+ if(this.props.status.isFetching == true) {
+ return false
+ }
+ if(this.props.status.fetchSuccess == true) {
+ return (
+ <li className='st_action'>
+ <button
+ type='button'
+ disabled={((this.props.filters.policy['vehicle_journeys.update'] == true) ? '' : 'disabled')}
+ data-toggle='modal'
+ data-target='#NewVehicleJourneyModal'
+ onClick={this.props.onOpenCreateModal}
+ >
+ <span className='fa fa-plus'></span>
+ </button>
+
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'create') ? 'in' : '') } id='NewVehicleJourneyModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>Ajouter une course</h4>
+ </div>
+
+ {(this.props.modal.type == 'create') && (
+ <form>
+ <div className='modal-body'>
+ <div className='row'>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Nom de la course</label>
+ <input
+ type='text'
+ ref='published_journey_name'
+ className='form-control'
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Nom du transporteur</label>
+ <CompanySelect2
+ company = {undefined}
+ onSelect2Company = {(e) => this.props.onSelect2Company(e)}
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label is-required'>Nom public de la mission</label>
+ <MissionSelect2
+ selection={this.props.modal.modalProps}
+ onSelect2JourneyPattern={this.props.onSelect2JourneyPattern}
+ isFilter={false}
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Numéro de train</label>
+ <input
+ type='text'
+ ref='published_journey_identifier'
+ className='form-control'
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
+ >
+ Annuler
+ </button>
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
+ >
+ Valider
+ </button>
+ </div>
+ </form>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+CreateModal.propTypes = {
+ index: PropTypes.number,
+ modal: PropTypes.object.isRequired,
+ status: PropTypes.object.isRequired,
+ stopPointsList: PropTypes.array.isRequired,
+ onOpenCreateModal: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired,
+ onAddVehicleJourney: PropTypes.func.isRequired,
+ onSelect2JourneyPattern: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js b/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js
new file mode 100644
index 000000000..0a1dedd3c
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js
@@ -0,0 +1,26 @@
+import React, { PropTypes } from 'react'
+import actions from '../../actions'
+
+export default function DeleteVehicleJourneys({onDeleteVehicleJourneys, vehicleJourneys, filters}) {
+ return (
+ <li className='st_action'>
+ <button
+ type='button'
+ disabled={(actions.getSelected(vehicleJourneys).length > 0 && filters.policy['vehicle_journeys.destroy']) ? '' : 'disabled'}
+ onClick={e => {
+ e.preventDefault()
+ onDeleteVehicleJourneys()
+ }}
+ title='Supprimer'
+ >
+ <span className='fa fa-trash'></span>
+ </button>
+ </li>
+ )
+}
+
+DeleteVehicleJourneys.propTypes = {
+ onDeleteVehicleJourneys: PropTypes.func.isRequired,
+ vehicleJourneys: PropTypes.array.isRequired,
+ filters: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js
new file mode 100644
index 000000000..0c1c81114
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js
@@ -0,0 +1,196 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../../actions'
+import _ from 'lodash'
+
+export default class DuplicateVehicleJourney extends Component {
+ constructor(props) {
+ super(props)
+ this.state = {}
+ this.onFormChange = this.onFormChange.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ }
+
+ componentWillReceiveProps() {
+ if (actions.getSelected(this.props.vehicleJourneys).length > 0) {
+ let hour = parseInt(this.getDefaultValue('hour'))
+ let miunte = parseInt(this.getDefaultValue('minute'))
+ this.setState((state, props) => {
+ return {
+ originalDT: {
+ hour: hour,
+ minute: miunte
+ },
+ duplicate_time_hh: hour,
+ duplicate_time_mm: miunte,
+ additional_time: 0,
+ duplicate_number: 1
+ }
+ })
+ }
+ }
+
+ handleSubmit() {
+ if(actions.validateFields(this.refs) == true) {
+ let newDeparture = {
+ departure_time : {
+ hour: this.state.duplicate_time_hh,
+ minute: this.state.duplicate_time_mm
+ }
+ }
+ let val = actions.getDuplicateDelta(_.find(actions.getSelected(this.props.vehicleJourneys)[0].vehicle_journey_at_stops, {'dummy': false}), newDeparture)
+ this.props.onDuplicateVehicleJourney(this.state.additional_time, this.state.duplicate_number, val)
+ this.props.onModalClose()
+ $('#DuplicateVehicleJourneyModal').modal('hide')
+ }
+ }
+
+ onFormChange(e) {
+ let {name, value} = e.target
+ this.setState((state, props) => {
+ return {
+ [name]: parseInt(value)
+ }
+ })
+ }
+
+ getDefaultValue(type) {
+ let vjas = _.find(actions.getSelected(this.props.vehicleJourneys)[0].vehicle_journey_at_stops, {'dummy': false})
+ return vjas.departure_time[type]
+ }
+
+ render() {
+ if(this.props.status.isFetching == true) {
+ return false
+ }
+ if(this.props.status.fetchSuccess == true && actions.getSelected(this.props.vehicleJourneys).length > 0) {
+ return (
+ <li className='st_action'>
+ <button
+ type='button'
+ disabled={((actions.getSelected(this.props.vehicleJourneys).length >= 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled')}
+ data-toggle='modal'
+ data-target='#DuplicateVehicleJourneyModal'
+ onClick={this.props.onOpenDuplicateModal}
+ >
+ <span className='fa fa-files-o'></span>
+ </button>
+
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'duplicate') ? 'in' : '') } id='DuplicateVehicleJourneyModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>
+ Dupliquer { actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'plusieurs courses' : 'une course' }
+ </h4>
+ </div>
+
+ {(this.props.modal.type == 'duplicate') && (
+ <form className='form-horizontal'>
+ <div className='modal-body'>
+ <div className={'form-group ' + (actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'hidden' : '' )}>
+ <label className='control-label is-required col-sm-8'>Horaire de départ indicatif</label>
+ <span className="col-sm-4">
+ <span className={'input-group time' + (actions.getSelected(this.props.vehicleJourneys).length > 1 ? ' disabled' : '')}>
+ <input
+ type='number'
+ name='duplicate_time_hh'
+ ref='duplicate_time_hh'
+ min='00'
+ max='23'
+ className='form-control'
+ value={this.state.duplicate_time_hh}
+ onChange={e => this.onFormChange(e)}
+ disabled={actions.getSelected(this.props.vehicleJourneys) && (actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')}
+ />
+ <span>:</span>
+ <input
+ type='number'
+ name='duplicate_time_mm'
+ ref='duplicate_time_mm'
+ min='00'
+ max='59'
+ className='form-control'
+ value={this.state.duplicate_time_mm}
+ onChange={e => this.onFormChange(e)}
+ disabled={actions.getSelected(this.props.vehicleJourneys) && (actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')}
+ />
+ </span>
+ </span>
+ </div>
+
+ <div className='form-group'>
+ <label className='control-label is-required col-sm-8'>Nombre de courses à créer et dupliquer</label>
+ <div className="col-sm-4">
+ <input
+ type='number'
+ style={{'width': 104}}
+ name='duplicate_number'
+ ref='duplicate_number'
+ min='1'
+ max='20'
+ value={this.state.duplicate_number}
+ className='form-control'
+ onChange={e => this.onFormChange(e)}
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ required
+ />
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label className='control-label is-required col-sm-8'>Décalage à partir duquel on créé les courses</label>
+ <span className="col-sm-4">
+ <input
+ type='number'
+ style={{'width': 104}}
+ name='additional_time'
+ ref='additional_time'
+ min='-720'
+ max='720'
+ value={this.state.additional_time}
+ className='form-control disabled'
+ onChange={e => this.onFormChange(e)}
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ required
+ />
+ </span>
+ </div>
+ </div>
+
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
+ >
+ Annuler
+ </button>
+ <button
+ className={'btn btn-primary ' + (this.state.additional_time == 0 && this.state.originalDT.hour == this.state.duplicate_time_hh && this.state.originalDT.minute == this.state.duplicate_time_mm ? 'disabled' : '')}
+ type='button'
+ onClick={this.handleSubmit}
+ >
+ Valider
+ </button>
+ </div>
+ </form>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+DuplicateVehicleJourney.propTypes = {
+ onOpenDuplicateModal: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js
new file mode 100644
index 000000000..3a4a57024
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js
@@ -0,0 +1,167 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../../actions'
+import CompanySelect2 from './select2s/CompanySelect2'
+
+export default class EditVehicleJourney extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ handleSubmit() {
+ if(actions.validateFields(this.refs) == true) {
+ var company;
+ if(this.props.modal.modalProps.selectedCompany) {
+ company = this.props.modal.modalProps.selectedCompany
+ } else if (typeof this.props.modal.modalProps.vehicleJourney.company === Object) {
+ company = this.props.modal.modalProps.vehicleJourney.company
+ } else {
+ company = undefined
+ }
+ this.props.onEditVehicleJourney(this.refs, company)
+ this.props.onModalClose()
+ $('#EditVehicleJourneyModal').modal('hide')
+ }
+ }
+
+ render() {
+ if(this.props.status.isFetching == true) {
+ return false
+ }
+ if(this.props.status.fetchSuccess == true) {
+ return (
+ <li className='st_action'>
+ <button
+ type='button'
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length == 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ data-toggle='modal'
+ data-target='#EditVehicleJourneyModal'
+ onClick={() => this.props.onOpenEditModal(actions.getSelected(this.props.vehicleJourneys)[0])}
+ >
+ <span className='fa fa-info'></span>
+ </button>
+
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'duplicate') ? 'in' : '') } id='EditVehicleJourneyModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>Informations</h4>
+ </div>
+
+ {(this.props.modal.type == 'edit') && (
+ <form>
+ <div className='modal-body'>
+ <div className='row'>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Nom de la course</label>
+ <input
+ type='text'
+ ref='published_journey_name'
+ className='form-control'
+ defaultValue={this.props.modal.modalProps.vehicleJourney.published_journey_name}
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Mission</label>
+ <input
+ type='text'
+ className='form-control'
+ value={actions.humanOID(this.props.modal.modalProps.vehicleJourney.journey_pattern.objectid) + ' - ' + (this.props.modal.modalProps.vehicleJourney.journey_pattern.name)}
+ disabled={true}
+ />
+ </div>
+ </div>
+ </div>
+
+ <div className='row'>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Numéro de train</label>
+ <input
+ type='text'
+ ref='published_journey_identifier'
+ className='form-control'
+ defaultValue={this.props.modal.modalProps.vehicleJourney.published_journey_identifier}
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Transporteur</label>
+ <CompanySelect2
+ company = {this.props.modal.modalProps.vehicleJourney.company}
+ onSelect2Company = {(e) => this.props.onSelect2Company(e)}
+ onUnselect2Company = {() => this.props.onUnselect2Company()}
+ />
+ </div>
+ </div>
+ </div>
+
+ <div className='row'>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Mode de transport</label>
+ <input
+ type='text'
+ className='form-control'
+ value={window.I18n.fr.enumerize.transport_mode[this.props.modal.modalProps.vehicleJourney.transport_mode]}
+ disabled={true}
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label'>Sous mode de transport</label>
+ <input
+ type='text'
+ className='form-control'
+ value={window.I18n.fr.enumerize.transport_submode[this.props.modal.modalProps.vehicleJourney.transport_submode]}
+ disabled={true}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
+ >
+ Annuler
+ </button>
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
+ >
+ Valider
+ </button>
+ </div>
+ </form>
+ )}
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+EditVehicleJourney.propTypes = {
+ onOpenEditModal: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js
new file mode 100644
index 000000000..1958faf5f
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js
@@ -0,0 +1,150 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../../actions'
+import _ from 'lodash'
+
+export default class NotesEditVehicleJourney extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ handleSubmit() {
+ this.props.onNotesEditVehicleJourney(this.props.modal.modalProps.vehicleJourney.footnotes)
+ this.props.onModalClose()
+ $('#NotesEditVehicleJourneyModal').modal('hide')
+ }
+
+ renderFootnoteButton(lf, vjArray){
+ let footnote_id = undefined
+ vjArray.forEach((f) => {
+ if(f.id == lf.id){
+ footnote_id = f.id
+ }
+ })
+
+ if(footnote_id){
+ return <button
+ type='button'
+ className='btn btn-outline-danger btn-xs'
+ onClick={() => this.props.onToggleFootnoteModal(lf, false)}
+ ><span className="fa fa-trash"></span> Retirer</button>
+ }else{
+ return <button
+ type='button'
+ className='btn btn-outline-primary btn-xs'
+ onClick={() => this.props.onToggleFootnoteModal(lf, true)}
+ ><span className="fa fa-plus"></span> Ajouter</button>
+ }
+ }
+
+ filterFN() {
+ return _.filter(window.line_footnotes, (lf, i) => {
+ let bool = true
+ _.map(this.props.modal.modalProps.vehicleJourney.footnotes, (f, j) => {
+ if(lf.id === f.id) {
+ bool = false
+ }
+ })
+ return bool
+ })
+ }
+
+ render() {
+ if(this.props.status.isFetching == true) {
+ return false
+ }
+ if(this.props.status.fetchSuccess == true) {
+ return (
+ <li className='st_action'>
+ <button
+ type='button'
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length == 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ data-toggle='modal'
+ data-target='#NotesEditVehicleJourneyModal'
+ onClick={() => this.props.onOpenNotesEditModal(actions.getSelected(this.props.vehicleJourneys)[0])}
+ >
+ <span className='fa fa-sticky-note'></span>
+ </button>
+
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'duplicate') ? 'in' : '') } id='NotesEditVehicleJourneyModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>Notes</h4>
+ </div>
+
+ {(this.props.modal.type == 'notes_edit') && (
+ <form>
+ <div className='modal-body'>
+ <h3>Notes associées</h3>
+ {(this.props.modal.modalProps.vehicleJourney.footnotes).map((lf, i) =>
+ <div
+ key={i}
+ className='panel panel-default'
+ >
+ <div className='panel-heading'>
+ <h4 className='panel-title clearfix'>
+ <div className='pull-left' style={{paddingTop: '3px'}}>{lf.code}</div>
+ <div className='pull-right'>{this.renderFootnoteButton(lf, this.props.modal.modalProps.vehicleJourney.footnotes)}</div>
+ </h4>
+ </div>
+ <div className='panel-body'><p>{lf.label}</p></div>
+ </div>
+ )}
+
+ <h3 className='mt-lg'>Sélectionnez les notes à associer à cette course :</h3>
+ {this.filterFN().map((lf, i) =>
+ <div
+ key={i}
+ className='panel panel-default'
+ >
+ <div className='panel-heading'>
+ <h4 className='panel-title clearfix'>
+ <div className='pull-left' style={{paddingTop: '3px'}}>{lf.code}</div>
+ <div className='pull-right'>{this.renderFootnoteButton(lf, this.props.modal.modalProps.vehicleJourney.footnotes)}</div>
+ </h4>
+ </div>
+ <div className='panel-body'><p>{lf.label}</p></div>
+ </div>
+ )}
+ </div>
+
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
+ >
+ Annuler
+ </button>
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
+ >
+ Valider
+ </button>
+ </div>
+ </form>
+ )}
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+NotesEditVehicleJourney.propTypes = {
+ onOpenNotesEditModal: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired,
+ onToggleFootnoteModal: PropTypes.func.isRequired,
+ onNotesEditVehicleJourney: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js
new file mode 100644
index 000000000..c1e2de779
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js
@@ -0,0 +1,114 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../../actions'
+
+export default class ShiftVehicleJourney extends Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ additional_time: 0
+ }
+ }
+
+ handleSubmit() {
+ if(actions.validateFields(this.refs) == true) {
+ this.props.onShiftVehicleJourney(this.state.additional_time)
+ this.props.onModalClose()
+ $('#ShiftVehicleJourneyModal').modal('hide')
+ }
+ }
+
+ handleAdditionalTimeChange() {
+ this.setState((state, props) => {
+ return {
+ additional_time: parseInt(this.refs.additional_time.value)
+ }
+ })
+ }
+
+ render() {
+ if(this.props.status.isFetching == true) {
+ return false
+ }
+ if(this.props.status.fetchSuccess == true) {
+ return (
+ <li className='st_action'>
+ <button
+ type='button'
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length == 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ data-toggle='modal'
+ data-target='#ShiftVehicleJourneyModal'
+ onClick={this.props.onOpenShiftModal}
+ >
+ <span className='sb sb-update-vj'></span>
+ </button>
+
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'shift') ? 'in' : '') } id='ShiftVehicleJourneyModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>Mettre à jour une course</h4>
+ {(this.props.modal.type == 'shift') && (
+ <em>Mettre à jour les horaires de la course {actions.humanOID(actions.getSelected(this.props.vehicleJourneys)[0].objectid)}</em>
+ )}
+ </div>
+
+ {(this.props.modal.type == 'shift') && (
+ <form>
+ <div className='modal-body'>
+ <div className='row'>
+ <div className='col-lg-4 col-lg-offset-4 col-md-4 col-md-offset-4 col-sm-4 col-sm-offset-4 col-xs-12'>
+ <div className='form-group'>
+ <label className='control-label is-required'>Avec un décalage de</label>
+ <input
+ type='number'
+ style={{'width': 104}}
+ ref='additional_time'
+ min='-720'
+ max='720'
+ value={this.state.additional_time}
+ className='form-control'
+ onChange={this.handleAdditionalTimeChange.bind(this)}
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ required
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
+ >
+ Annuler
+ </button>
+ <button
+ className={'btn btn-primary ' + (this.state.additional_time == 0 ? 'disabled' : '')}
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
+ >
+ Valider
+ </button>
+ </div>
+ </form>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+ShiftVehicleJourney.propTypes = {
+ onOpenShiftModal: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js
new file mode 100644
index 000000000..fd2304901
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js
@@ -0,0 +1,131 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../../actions'
+import TimetableSelect2 from './select2s/TimetableSelect2'
+
+export default class TimetablesEditVehicleJourney extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ handleSubmit() {
+ this.props.onTimetablesEditVehicleJourney(this.props.modal.modalProps.vehicleJourneys, this.props.modal.modalProps.timetables)
+ this.props.onModalClose()
+ $('#CalendarsEditVehicleJourneyModal').modal('hide')
+ }
+
+ render() {
+ if(this.props.status.isFetching == true) {
+ return false
+ }
+ if(this.props.status.fetchSuccess == true) {
+ return (
+ <li className='st_action'>
+ <button
+ type='button'
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length > 0 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ data-toggle='modal'
+ data-target='#CalendarsEditVehicleJourneyModal'
+ onClick={() => this.props.onOpenCalendarsEditModal(actions.getSelected(this.props.vehicleJourneys))}
+ >
+ <span className='fa fa-calendar'></span>
+ </button>
+
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'duplicate') ? 'in' : '') } id='CalendarsEditVehicleJourneyModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>Calendriers associés</h4>
+ </div>
+
+ {(this.props.modal.type == 'calendars_edit') && (
+ <form>
+ <div className='modal-body'>
+ <div className='row'>
+ <div className='col-lg-12'>
+ <div className='subform'>
+ <div className='nested-head'>
+ <div className='wrapper'>
+ <div>
+ <div className='form-group'>
+ <label className='control-label'>Calendriers associés</label>
+ </div>
+ </div>
+ <div></div>
+ </div>
+ </div>
+ {this.props.modal.modalProps.timetables.map((tt, i) =>
+ <div className='nested-fields' key={i}>
+ <div className='wrapper'>
+ <div>{tt.comment}</div>
+ <div>
+ <a
+ href='#'
+ title='Supprimer'
+ className='fa fa-trash remove_fields'
+ style={{height: 'auto', lineHeight: 'normal'}}
+ onClick={(e) => {
+ e.preventDefault()
+ this.props.onDeleteCalendarModal(tt)
+ }}
+ ></a>
+ </div>
+ </div>
+ </div>
+ )}
+ <div className='nested-fields'>
+ <div className='wrapper'>
+ <div>
+ <TimetableSelect2
+ onSelect2Timetable={this.props.onSelect2Timetable}
+ chunkURL={'/autocomplete_time_tables.json'}
+ isFilter={false}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
+ >
+ Annuler
+ </button>
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
+ >
+ Valider
+ </button>
+ </div>
+ </form>
+ )}
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+TimetablesEditVehicleJourney.propTypes = {
+ onOpenCalendarsEditModal: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired,
+ onTimetablesEditVehicleJourney: PropTypes.func.isRequired,
+ onDeleteCalendarModal: PropTypes.func.isRequired,
+ onSelect2Timetable: PropTypes.func.isRequired,
+ filters: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js
new file mode 100644
index 000000000..03272e8b4
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js
@@ -0,0 +1,64 @@
+import _ from 'lodash'
+import React, { PropTypes, Component } from 'react'
+import Select2 from 'react-select2-wrapper'
+
+// get JSON full path
+let origin = window.location.origin
+let path = window.location.pathname.split('/', 3).join('/')
+let line = window.location.pathname.split('/')[4]
+
+
+export default class BSelect4 extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ return (
+ <Select2
+ data={(this.props.company) ? [this.props.company.name] : undefined}
+ value={(this.props.company) ? this.props.company.name : undefined}
+ onSelect={(e) => this.props.onSelect2Company(e) }
+ onUnselect={() => this.props.onUnselect2Company()}
+ multiple={false}
+ ref='company_id'
+ options={{
+ allowClear: true,
+ theme: 'bootstrap',
+ width: '100%',
+ placeholder: 'Filtrer par transporteur...',
+ language: require('./fr'),
+ ajax: {
+ url: origin + path + '/companies.json' + '?line_id=' + line,
+ dataType: 'json',
+ delay: '500',
+ data: function(params) {
+ return {
+ q: {name_cont: params.term},
+ };
+ },
+ processResults: function(data, params) {
+
+ return {
+ results: data.map(
+ item => _.assign(
+ {},
+ item,
+ {text: item.name}
+ )
+ )
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 1,
+ templateResult: formatRepo
+ }}
+ />
+ )
+ }
+}
+
+const formatRepo = (props) => {
+ if(props.text) return props.text
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js
new file mode 100644
index 000000000..bf00a9d96
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js
@@ -0,0 +1,63 @@
+import _ from 'lodash'
+import React, { PropTypes, Component } from 'react'
+import Select2 from 'react-select2-wrapper'
+import actions from '../../../actions'
+
+// get JSON full path
+let origin = window.location.origin
+let path = window.location.pathname.split('/', 7).join('/')
+
+
+export default class BSelect4 extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ return (
+ <Select2
+ data={(this.props.isFilter) ? [this.props.filters.query.journeyPattern.published_name] : ((this.props.selection.selectedJPModal) ? [this.props.selection.selectedJPModal.published_name] : undefined)}
+ value={(this.props.isFilter) ? this.props.filters.query.journeyPattern.published_name : ((this.props.selection.selectedJPModal) ? this.props.selection.selectedJPModal.published_name : undefined) }
+ onSelect={(e) => this.props.onSelect2JourneyPattern(e)}
+ multiple={false}
+ ref='journey_pattern_id'
+ options={{
+ allowClear: false,
+ theme: 'bootstrap',
+ placeholder: 'Filtrer par code, nom ou OID de mission...',
+ language: require('./fr'),
+ width: '100%',
+ ajax: {
+ url: origin + path + '/journey_patterns_collection.json',
+ dataType: 'json',
+ delay: '500',
+ data: function(params) {
+ return {
+ q: {published_name_or_objectid_or_registration_number_cont: params.term},
+ };
+ },
+ processResults: function(data, params) {
+ return {
+ results: data.map(
+ item => _.assign(
+ {},
+ item,
+ { text: "<strong>" + item.published_name + " - " + actions.humanOID(item.object_id) + "</strong><br/><small>" + item.registration_number + "</small>" }
+ )
+ )
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 1,
+ escapeMarkup: function (markup) { return markup; },
+ templateResult: formatRepo
+ }}
+ />
+ )
+ }
+}
+
+const formatRepo = (props) => {
+ if(props.text) return props.text
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js
new file mode 100644
index 000000000..8463965b9
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js
@@ -0,0 +1,68 @@
+import _ from 'lodash'
+import React, { PropTypes, Component } from 'react'
+import Select2 from 'react-select2-wrapper'
+import actions from '../../../actions'
+
+// get JSON full path
+var origin = window.location.origin
+var path = window.location.pathname.split('/', 3).join('/')
+
+
+export default class BSelect4 extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ return (
+ <Select2
+ data={(this.props.isFilter) ? [this.props.filters.query.timetable.comment] : undefined}
+ value={(this.props.isFilter) ? this.props.filters.query.timetable.comment : undefined}
+ onSelect={(e) => this.props.onSelect2Timetable(e) }
+ multiple={false}
+ ref='timetable_id'
+ options={{
+ allowClear: false,
+ theme: 'bootstrap',
+ width: '100%',
+ placeholder: 'Filtrer par calendrier...',
+ language: require('./fr'),
+ ajax: {
+ url: origin + path + this.props.chunkURL,
+ dataType: 'json',
+ delay: '500',
+ data: function(params) {
+ let newParmas = params.term.split(" ")
+ return {
+ q: {
+ objectid_cont_any: newParmas,
+ comment_cont_any: newParmas,
+ m: 'or'
+ }
+ };
+ },
+ processResults: function(data, params) {
+ return {
+ results: data.map(
+ item => _.assign(
+ {},
+ item,
+ {text: '<strong>' + "<span class='fa fa-circle' style='color:" + (item.color ? item.color : '#4B4B4B') + "'></span> " + item.comment + ' - ' + actions.humanOID(item.objectid) + '</strong><br/><small>' + (item.day_types ? item.day_types.match(/[A-Z]?[a-z]+/g).join(', ') : "") + '</small>'}
+ )
+ )
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 1,
+ escapeMarkup: function (markup) { return markup; },
+ templateResult: formatRepo
+ }}
+ />
+ )
+ }
+}
+
+const formatRepo = (props) => {
+ if(props.text) return props.text
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js
new file mode 100644
index 000000000..34273fcf6
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js
@@ -0,0 +1,62 @@
+import _ from 'lodash'
+import React, { PropTypes, Component } from 'react'
+import Select2 from 'react-select2-wrapper'
+import actions from '../../../actions'
+
+// get JSON full path
+let origin = window.location.origin
+let path = window.location.pathname.split('/', 7).join('/')
+
+
+export default class BSelect4b extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ return (
+ <Select2
+ data={(this.props.isFilter) ? [this.props.filters.query.vehicleJourney.objectid] : undefined}
+ value={(this.props.isFilter) ? this.props.filters.query.vehicleJourney.objectid : undefined}
+ onSelect={(e) => this.props.onSelect2VehicleJourney(e)}
+ multiple={false}
+ ref='vehicle_journey_objectid'
+ options={{
+ allowClear: false,
+ theme: 'bootstrap',
+ placeholder: 'Filtrer par ID course...',
+ width: '100%',
+ language: require('./fr'),
+ ajax: {
+ url: origin + path + '/vehicle_journeys.json',
+ dataType: 'json',
+ delay: '500',
+ data: function(params) {
+ return {
+ q: {objectid_cont: params.term},
+ };
+ },
+ processResults: function(data, params) {
+ return {
+ results: data.vehicle_journeys.map(
+ item => _.assign(
+ {},
+ item,
+ { id: item.objectid, text: actions.humanOID(item.objectid) }
+ )
+ )
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 1,
+ templateResult: formatRepo
+ }}
+ />
+ )
+ }
+}
+
+const formatRepo = (props) => {
+ if(props.text) return props.text
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/fr.js b/app/javascript/vehicle_journeys/components/tools/select2s/fr.js
new file mode 100644
index 000000000..20154d412
--- /dev/null
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/fr.js
@@ -0,0 +1,9 @@
+module.exports = {
+ errorLoading:function(){return"Les résultats ne peuvent pas être chargés."},
+ inputTooLong:function(e){var t=e.input.length-e.maximum,n="Supprimez "+t+" caractère";return t!==1&&(n+="s"),n},
+ inputTooShort:function(e){var t=e.minimum-e.input.length,n="Saisissez "+t+" caractère";return t!==1&&(n+="s"),n},
+ loadingMore:function(){return"Chargement de résultats supplémentaires…"},
+ maximumSelected:function(e){var t="Vous pouvez seulement sélectionner "+e.maximum+" élément";return e.maximum!==1&&(t+="s"),t},
+ noResults:function(){return"Aucun résultat trouvé"},
+ searching:function(){return"Recherche en cours…"}
+}
diff --git a/app/javascript/vehicle_journeys/containers/ConfirmModal.js b/app/javascript/vehicle_journeys/containers/ConfirmModal.js
new file mode 100644
index 000000000..e751a70f6
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/ConfirmModal.js
@@ -0,0 +1,30 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import ConfirmModal from '../components/ConfirmModal'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ vehicleJourneys: state.vehicleJourneys
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalAccept: (next, state) =>{
+ dispatch(actions.fetchingApi())
+ actions.submitVehicleJourneys(dispatch, state, next)
+ },
+ onModalCancel: (next) =>{
+ dispatch(actions.fetchingApi())
+ dispatch(next)
+ },
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ }
+ }
+}
+
+const ConfirmModalContainer = connect(mapStateToProps, mapDispatchToProps)(ConfirmModal)
+
+export default ConfirmModalContainer
diff --git a/app/javascript/vehicle_journeys/containers/Filters.js b/app/javascript/vehicle_journeys/containers/Filters.js
new file mode 100644
index 000000000..bec3527f4
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/Filters.js
@@ -0,0 +1,48 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import Filters from '../components/Filters'
+
+const mapStateToProps = (state) => {
+ return {
+ filters: state.filters,
+ pagination: state.pagination
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onUpdateStartTimeFilter: (e, unit) =>{
+ e.preventDefault()
+ dispatch(actions.updateStartTimeFilter(e.target.value, unit))
+ },
+ onUpdateEndTimeFilter: (e, unit) =>{
+ e.preventDefault()
+ dispatch(actions.updateEndTimeFilter(e.target.value, unit))
+ },
+ onToggleWithoutSchedule: () =>{
+ dispatch(actions.toggleWithoutSchedule())
+ },
+ onToggleWithoutTimeTable: () =>{
+ dispatch(actions.toggleWithoutTimeTable())
+ },
+ onResetFilters: (e, pagination) =>{
+ dispatch(actions.checkConfirmModal(e, actions.resetFilters(dispatch), pagination.stateChanged, dispatch))
+ },
+ onFilter: (e, pagination) =>{
+ dispatch(actions.checkConfirmModal(e, actions.filterQuery(dispatch), pagination.stateChanged, dispatch))
+ },
+ onSelect2Timetable: (e) => {
+ dispatch(actions.filterSelect2Timetable(e.params.data))
+ },
+ onSelect2JourneyPattern: (e) => {
+ dispatch(actions.filterSelect2JourneyPattern(e.params.data))
+ },
+ onSelect2VehicleJourney: (e) => {
+ dispatch(actions.filterSelect2VehicleJourney(e.params.data))
+ }
+ }
+}
+
+const FiltersList = connect(mapStateToProps, mapDispatchToProps)(Filters)
+
+export default FiltersList
diff --git a/app/javascript/vehicle_journeys/containers/Navigate.js b/app/javascript/vehicle_journeys/containers/Navigate.js
new file mode 100644
index 000000000..f6cd4e2a1
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/Navigate.js
@@ -0,0 +1,17 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import NavigateComponent from '../components/Navigate'
+
+const mapStateToProps = (state) => {
+ return {
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ pagination: state.pagination,
+ filters: state.filters
+ }
+}
+
+
+const Navigate = connect(mapStateToProps)(NavigateComponent)
+
+export default Navigate
diff --git a/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js b/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js
new file mode 100644
index 000000000..18f9e994e
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js
@@ -0,0 +1,28 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import SaveVehicleJourneysComponent from '../components/SaveVehicleJourneys'
+
+const mapStateToProps = (state) => {
+ return {
+ editMode: state.editMode,
+ vehicleJourneys: state.vehicleJourneys,
+ page: state.pagination.page,
+ status: state.status,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onEnterEditMode: () => {
+ dispatch(actions.enterEditMode())
+ },
+ onSubmitVehicleJourneys: (next, state) => {
+ actions.submitVehicleJourneys(dispatch, state, next)
+ }
+ }
+}
+
+const SaveVehicleJourneys = connect(mapStateToProps, mapDispatchToProps)(SaveVehicleJourneysComponent)
+
+export default SaveVehicleJourneys
diff --git a/app/javascript/vehicle_journeys/containers/ToggleArrivals.js b/app/javascript/vehicle_journeys/containers/ToggleArrivals.js
new file mode 100644
index 000000000..f6e180f80
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/ToggleArrivals.js
@@ -0,0 +1,21 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import ToggleArrivals from '../components/ToggleArrivals'
+
+const mapStateToProps = (state) => {
+ return {
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onToggleArrivals: () =>{
+ dispatch(actions.toggleArrivals())
+ }
+ }
+}
+
+const ToggleArrivalsList = connect(mapStateToProps, mapDispatchToProps)(ToggleArrivals)
+
+export default ToggleArrivalsList
diff --git a/app/javascript/vehicle_journeys/containers/Tools.js b/app/javascript/vehicle_journeys/containers/Tools.js
new file mode 100644
index 000000000..b760b52d6
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/Tools.js
@@ -0,0 +1,23 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import ToolsComponent from '../components/Tools'
+
+const mapStateToProps = (state) => {
+ return {
+ vehicleJourneys: state.vehicleJourneys,
+ editMode: state.editMode,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onCancelSelection: () => {
+ dispatch(actions.cancelSelection())
+ }
+ }
+}
+
+const Tools = connect(mapStateToProps, mapDispatchToProps)(ToolsComponent)
+
+export default Tools
diff --git a/app/javascript/vehicle_journeys/containers/VehicleJourneysList.js b/app/javascript/vehicle_journeys/containers/VehicleJourneysList.js
new file mode 100644
index 000000000..38ab9f6d3
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/VehicleJourneysList.js
@@ -0,0 +1,32 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import VehicleJourneys from '../components/VehicleJourneys'
+
+const mapStateToProps = (state) => {
+ return {
+ editMode: state.editMode,
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ filters: state.filters,
+ stopPointsList: state.stopPointsList
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onLoadFirstPage: (filters) =>{
+ dispatch(actions.fetchingApi())
+ actions.fetchVehicleJourneys(dispatch, undefined, undefined, filters.queryString)
+ },
+ onUpdateTime: (e, subIndex, index, timeUnit, isDeparture, isArrivalsToggled) => {
+ dispatch(actions.updateTime(e.target.value, subIndex, index, timeUnit, isDeparture, isArrivalsToggled))
+ },
+ onSelectVehicleJourney: (index) => {
+ dispatch(actions.selectVehicleJourney(index))
+ }
+ }
+}
+
+const VehicleJourneysList = connect(mapStateToProps, mapDispatchToProps)(VehicleJourneys)
+
+export default VehicleJourneysList
diff --git a/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js
new file mode 100644
index 000000000..b3f777448
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js
@@ -0,0 +1,37 @@
+import actions from '../../actions'
+import { connect } from 'react-redux'
+import CreateModal from '../../components/tools/CreateModal'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ stopPointsList: state.stopPointsList,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ onAddVehicleJourney: (data, selectedJourneyPattern, stopPointsList, selectedCompany) =>{
+ dispatch(actions.addVehicleJourney(data, selectedJourneyPattern, stopPointsList, selectedCompany))
+ },
+ onOpenCreateModal: () =>{
+ dispatch(actions.openCreateModal())
+ },
+ onSelect2JourneyPattern: (e) =>{
+ dispatch(actions.selectJPCreateModal(e.params.data))
+ },
+ onSelect2Company: (e) => {
+ dispatch(actions.select2Company(e.params.data))
+ }
+ }
+}
+
+const AddVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(CreateModal)
+
+export default AddVehicleJourney
diff --git a/app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js b/app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js
new file mode 100644
index 000000000..d7d315da4
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js
@@ -0,0 +1,22 @@
+import actions from '../../actions'
+import { connect } from 'react-redux'
+import DeleteVJComponent from '../../components/tools/DeleteVehicleJourneys'
+
+const mapStateToProps = (state) => {
+ return {
+ vehicleJourneys: state.vehicleJourneys,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onDeleteVehicleJourneys: () =>{
+ dispatch(actions.deleteVehicleJourneys())
+ },
+ }
+}
+
+const DeleteVehicleJourneys = connect(mapStateToProps, mapDispatchToProps)(DeleteVJComponent)
+
+export default DeleteVehicleJourneys
diff --git a/app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js
new file mode 100644
index 000000000..e9ca88040
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js
@@ -0,0 +1,30 @@
+import actions from '../../actions'
+import { connect } from 'react-redux'
+import DuplicateVJComponent from '../../components/tools/DuplicateVehicleJourney'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ onOpenDuplicateModal: () =>{
+ dispatch(actions.openDuplicateModal())
+ },
+ onDuplicateVehicleJourney: (addtionalTime, duplicateNumber, departureDelta) =>{
+ dispatch(actions.duplicateVehicleJourney(addtionalTime, duplicateNumber, departureDelta))
+ }
+ }
+}
+
+const DuplicateVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(DuplicateVJComponent)
+
+export default DuplicateVehicleJourney
diff --git a/app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js
new file mode 100644
index 000000000..2d480aa0c
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js
@@ -0,0 +1,36 @@
+import actions from '../../actions'
+import { connect } from 'react-redux'
+import EditComponent from '../../components/tools/EditVehicleJourney'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ onOpenEditModal: (vj) =>{
+ dispatch(actions.openEditModal(vj))
+ },
+ onEditVehicleJourney: (data, selectedCompany) =>{
+ dispatch(actions.editVehicleJourney(data, selectedCompany))
+ },
+ onSelect2Company: (e) => {
+ dispatch(actions.select2Company(e.params.data))
+ },
+ onUnselect2Company: () => {
+ dispatch(actions.unselect2Company())
+ },
+ }
+}
+
+const EditVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(EditComponent)
+
+export default EditVehicleJourney
diff --git a/app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js
new file mode 100644
index 000000000..5a96ff273
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js
@@ -0,0 +1,33 @@
+import actions from '../../actions'
+import { connect } from 'react-redux'
+import NotesEditComponent from '../../components/tools/NotesEditVehicleJourney'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ onOpenNotesEditModal: (vj) =>{
+ dispatch(actions.openNotesEditModal(vj))
+ },
+ onToggleFootnoteModal: (footnote, isShown) => {
+ dispatch(actions.toggleFootnoteModal(footnote, isShown))
+ },
+ onNotesEditVehicleJourney: (footnotes) =>{
+ dispatch(actions.editVehicleJourneyNotes(footnotes))
+ }
+ }
+}
+
+const NotesEditVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(NotesEditComponent)
+
+export default NotesEditVehicleJourney
diff --git a/app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js
new file mode 100644
index 000000000..a4b4fbe39
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js
@@ -0,0 +1,30 @@
+import actions from '../../actions'
+import { connect } from 'react-redux'
+import ShiftVJComponent from '../../components/tools/ShiftVehicleJourney'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ onOpenShiftModal: () =>{
+ dispatch(actions.openShiftModal())
+ },
+ onShiftVehicleJourney: (data) =>{
+ dispatch(actions.shiftVehicleJourney(data))
+ }
+ }
+}
+
+const ShiftVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(ShiftVJComponent)
+
+export default ShiftVehicleJourney
diff --git a/app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js
new file mode 100644
index 000000000..62150a06e
--- /dev/null
+++ b/app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js
@@ -0,0 +1,37 @@
+import actions from '../../actions'
+import { connect } from 'react-redux'
+import TimetablesEditComponent from '../../components/tools/TimetablesEditVehicleJourney'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ vehicleJourneys: state.vehicleJourneys,
+ status: state.status,
+ filters: state.filters
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ onOpenCalendarsEditModal: (vehicleJourneys) =>{
+ dispatch(actions.openCalendarsEditModal(vehicleJourneys))
+ },
+ onDeleteCalendarModal: (timetable) => {
+ dispatch(actions.deleteCalendarModal(timetable))
+ },
+ onTimetablesEditVehicleJourney: (vehicleJourneys, timetables) =>{
+ dispatch(actions.editVehicleJourneyTimetables(vehicleJourneys, timetables))
+ },
+ onSelect2Timetable: (e) =>{
+ dispatch(actions.selectTTCalendarsModal(e.params.data))
+ dispatch(actions.addSelectedTimetable())
+ }
+ }
+}
+
+const TimetablesEditVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(TimetablesEditComponent)
+
+export default TimetablesEditVehicleJourney
diff --git a/app/javascript/vehicle_journeys/reducers/editMode.js b/app/javascript/vehicle_journeys/reducers/editMode.js
new file mode 100644
index 000000000..bff976804
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/editMode.js
@@ -0,0 +1,10 @@
+export default function editMode(state = {}, action ) {
+ switch (action.type) {
+ case "ENTER_EDIT_MODE":
+ return true
+ case "EXIT_EDIT_MODE":
+ return false
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/reducers/filters.js b/app/javascript/vehicle_journeys/reducers/filters.js
new file mode 100644
index 000000000..76fc98cc5
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/filters.js
@@ -0,0 +1,73 @@
+import _ from 'lodash'
+import actions from '../actions'
+let newQuery, newInterval
+
+export default function filters(state = {}, action) {
+ switch (action.type) {
+ case 'RESET_FILTERS':
+ let interval = {
+ start:{
+ hour: '00',
+ minute: '00'
+ },
+ end:{
+ hour: '23',
+ minute: '59'
+ }
+ }
+ newQuery = _.assign({}, state.query, {interval: interval, journeyPattern: {}, vehicleJourney: {}, timetable: {}, withoutSchedule: true, withoutTimeTable: true })
+ return _.assign({}, state, {query: newQuery, queryString: ''})
+ case 'TOGGLE_WITHOUT_SCHEDULE':
+ newQuery = _.assign({}, state.query, {withoutSchedule: !state.query.withoutSchedule})
+ return _.assign({}, state, {query: newQuery})
+ case 'TOGGLE_WITHOUT_TIMETABLE':
+ newQuery = _.assign({}, state.query, {withoutTimeTable: !state.query.withoutTimeTable})
+ return _.assign({}, state, {query: newQuery})
+ case 'UPDATE_END_TIME_FILTER':
+ newInterval = JSON.parse(JSON.stringify(state.query.interval))
+ newInterval.end[action.unit] = actions.pad(action.val, action.unit)
+ if(parseInt(newInterval.start.hour + newInterval.start.minute) < parseInt(newInterval.end.hour + newInterval.end.minute)){
+ newQuery = _.assign({}, state.query, {interval: newInterval})
+ return _.assign({}, state, {query: newQuery})
+ }else{
+ return state
+ }
+ case 'UPDATE_START_TIME_FILTER':
+ newInterval = JSON.parse(JSON.stringify(state.query.interval))
+ newInterval.start[action.unit] = actions.pad(action.val, action.unit)
+ if(parseInt(newInterval.start.hour + newInterval.start.minute) < parseInt(newInterval.end.hour + newInterval.end.minute)){
+ newQuery = _.assign({}, state.query, {interval: newInterval})
+ return _.assign({}, state, {query: newQuery})
+ }else{
+ return state
+ }
+ case 'SELECT_TT_FILTER':
+ newQuery = _.assign({}, state.query, {timetable : action.selectedItem})
+ return _.assign({}, state, {query: newQuery})
+ case 'SELECT_JP_FILTER':
+ newQuery = _.assign({}, state.query, {journeyPattern : action.selectedItem})
+ return _.assign({}, state, {query: newQuery})
+ case 'SELECT_VJ_FILTER':
+ newQuery = _.assign({}, state.query, {vehicleJourney : action.selectedItem})
+ return _.assign({}, state, {query: newQuery})
+ case 'TOGGLE_ARRIVALS':
+ return _.assign({}, state, {toggleArrivals: !state.toggleArrivals})
+ case 'QUERY_FILTER_VEHICLEJOURNEYS':
+ actions.fetchVehicleJourneys(action.dispatch, undefined, undefined, state.queryString)
+ return state
+ case 'CREATE_QUERY_STRING':
+ let params = {
+ 'q[journey_pattern_id_eq]': state.query.journeyPattern.id || undefined,
+ 'q[objectid_cont]': state.query.vehicleJourney.objectid || undefined,
+ 'q[time_tables_id_eq]': state.query.timetable.id || undefined,
+ 'q[vehicle_journey_at_stops_departure_time_gteq]': (state.query.interval.start.hour + ':' + state.query.interval.start.minute),
+ 'q[vehicle_journey_at_stops_departure_time_lteq]': (state.query.interval.end.hour + ':' + state.query.interval.end.minute),
+ 'q[vehicle_journey_without_departure_time]': state.query.withoutSchedule,
+ 'q[vehicle_journey_without_time_table]': state.query.withoutTimeTable
+ }
+ let queryString = actions.encodeParams(params)
+ return _.assign({}, state, {queryString: queryString})
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/reducers/index.js b/app/javascript/vehicle_journeys/reducers/index.js
new file mode 100644
index 000000000..bb24aa185
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/index.js
@@ -0,0 +1,20 @@
+import { combineReducers } from 'redux'
+import vehicleJourneys from './vehicleJourneys'
+import pagination from './pagination'
+import modal from './modal'
+import status from './status'
+import filters from './filters'
+import editMode from './editMode'
+import stopPointsList from './stopPointsList'
+
+const vehicleJourneysApp = combineReducers({
+ vehicleJourneys,
+ pagination,
+ modal,
+ status,
+ filters,
+ editMode,
+ stopPointsList
+})
+
+export default vehicleJourneysApp
diff --git a/app/javascript/vehicle_journeys/reducers/modal.js b/app/javascript/vehicle_journeys/reducers/modal.js
new file mode 100644
index 000000000..57f54a144
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/modal.js
@@ -0,0 +1,138 @@
+import _ from 'lodash'
+
+let vehicleJourneysModal, newModalProps
+
+export default function modal(state = {}, action) {
+ switch (action.type) {
+ case 'OPEN_CONFIRM_MODAL':
+ $('#ConfirmModal').modal('show')
+ return _.assign({}, state, {
+ type: 'confirm',
+ confirmModal: {
+ callback: action.callback,
+ }
+ })
+ case 'EDIT_NOTES_VEHICLEJOURNEY_MODAL':
+ let vehicleJourneyModal = _.assign({}, action.vehicleJourney)
+ return {
+ type: 'notes_edit',
+ modalProps: {
+ vehicleJourney: vehicleJourneyModal
+ },
+ confirmModal: {}
+ }
+ case 'TOGGLE_FOOTNOTE_MODAL':
+ newModalProps = JSON.parse(JSON.stringify(state.modalProps))
+ if (action.isShown){
+ newModalProps.vehicleJourney.footnotes.push(action.footnote)
+ }else{
+ newModalProps.vehicleJourney.footnotes = newModalProps.vehicleJourney.footnotes.filter((f) => {return f.id != action.footnote.id })
+ }
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'EDIT_VEHICLEJOURNEY_MODAL':
+ return {
+ type: 'edit',
+ modalProps: {
+ vehicleJourney: action.vehicleJourney
+ },
+ confirmModal: {}
+ }
+ case 'EDIT_CALENDARS_VEHICLEJOURNEY_MODAL':
+ vehicleJourneysModal = JSON.parse(JSON.stringify(action.vehicleJourneys))
+ let uniqTimetables = []
+ let timetable = {}
+ vehicleJourneysModal.map((vj, i) => {
+ vj.time_tables.map((tt, j) =>{
+ if(!(_.find(uniqTimetables, tt))){
+ uniqTimetables.push(tt)
+ }
+ })
+ })
+ return {
+ type: 'calendars_edit',
+ modalProps: {
+ vehicleJourneys: vehicleJourneysModal,
+ timetables: uniqTimetables
+ },
+ confirmModal: {}
+ }
+ case 'SELECT_CP_EDIT_MODAL':
+ newModalProps = _.assign({}, state.modalProps, {selectedCompany : action.selectedItem})
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'UNSELECT_CP_EDIT_MODAL':
+ newModalProps = _.assign({}, state.modalProps, {selectedCompany : undefined})
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'SELECT_TT_CALENDAR_MODAL':
+ newModalProps = _.assign({}, state.modalProps, {selectedTimetable : action.selectedItem})
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'ADD_SELECTED_TIMETABLE':
+ if(state.modalProps.selectedTimetable){
+ newModalProps = JSON.parse(JSON.stringify(state.modalProps))
+ if (!_.find(newModalProps.timetables, newModalProps.selectedTimetable)){
+ newModalProps.timetables.push(newModalProps.selectedTimetable)
+ }
+ return _.assign({}, state, {modalProps: newModalProps})
+ }
+ case 'DELETE_CALENDAR_MODAL':
+ newModalProps = JSON.parse(JSON.stringify(state.modalProps))
+ let timetablesModal = state.modalProps.timetables.slice(0)
+ timetablesModal.map((tt, i) =>{
+ if(tt == action.timetable){
+ timetablesModal.splice(i, 1)
+ }
+ })
+ vehicleJourneysModal = state.modalProps.vehicleJourneys.slice(0)
+ vehicleJourneysModal.map((vj) =>{
+ vj.time_tables.map((tt, i) =>{
+ if (_.isEqual(tt, action.timetable)){
+ vj.time_tables.splice(i, 1)
+ }
+ })
+ })
+ newModalProps.vehicleJourneys = vehicleJourneysModal
+ newModalProps.timetables = timetablesModal
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'CREATE_VEHICLEJOURNEY_MODAL':
+ let selectedJP = {}
+ if (window.jpOrigin){
+ let stopAreas = _.map(window.jpOriginStopPoints, (sa, i) =>{
+ return _.assign({}, {stop_area_short_description : {id : sa.stop_area_id}})
+ })
+ selectedJP = {
+ id: window.jpOrigin.id,
+ name: window.jpOrigin.name,
+ published_name: window.jpOrigin.published_name,
+ objectid: window.jpOrigin.objectid,
+ stop_areas: stopAreas
+ }
+ }
+ return {
+ type: 'create',
+ modalProps: window.jpOrigin ? {selectedJPModal: selectedJP} : {},
+ confirmModal: {}
+ }
+ case 'SELECT_JP_CREATE_MODAL':
+ newModalProps = _.assign({}, state.modalProps, {selectedJPModal : action.selectedItem})
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'SHIFT_VEHICLEJOURNEY_MODAL':
+ return {
+ type: 'shift',
+ modalProps: {},
+ confirmModal: {}
+ }
+ case 'DUPLICATE_VEHICLEJOURNEY_MODAL':
+ return {
+ type: 'duplicate',
+ modalProps: {},
+ confirmModal: {}
+ }
+ case 'CLOSE_MODAL':
+ return {
+ type: '',
+ modalProps: {},
+ confirmModal: {}
+ }
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/reducers/pagination.js b/app/javascript/vehicle_journeys/reducers/pagination.js
new file mode 100644
index 000000000..45c40c4c4
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/pagination.js
@@ -0,0 +1,37 @@
+import _ from 'lodash'
+
+export default function pagination(state = {}, action) {
+ switch (action.type) {
+ case 'RECEIVE_JOURNEY_PATTERNS':
+ case 'RECEIVE_VEHICLE_JOURNEYS':
+ return _.assign({}, state, {stateChanged: false})
+ case 'GO_TO_PREVIOUS_PAGE':
+ if (action.pagination.page > 1){
+ return _.assign({}, state, {page : action.pagination.page - 1, stateChanged: false})
+ }
+ return state
+ case 'GO_TO_NEXT_PAGE':
+ if (state.totalCount - (action.pagination.page * action.pagination.perPage) > 0){
+ return _.assign({}, state, {page : action.pagination.page + 1, stateChanged: false})
+ }
+ return state
+ case 'ADD_VEHICLEJOURNEY':
+ case 'UPDATE_TIME':
+ toggleOnConfirmModal('modal')
+ return _.assign({}, state, {stateChanged: true})
+ case 'RESET_PAGINATION':
+ return _.assign({}, state, {page: 1, stateChanged: false})
+ case 'RECEIVE_TOTAL_COUNT':
+ return _.assign({}, state, {totalCount: action.total})
+ case 'UPDATE_TOTAL_COUNT':
+ return _.assign({}, state, {totalCount : state.totalCount - action.diff })
+ default:
+ return state
+ }
+}
+
+const toggleOnConfirmModal = (arg = '') =>{
+ $('.confirm').each(function(){
+ $(this).data('toggle','')
+ })
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/reducers/status.js b/app/javascript/vehicle_journeys/reducers/status.js
new file mode 100644
index 000000000..0bbb05124
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/status.js
@@ -0,0 +1,17 @@
+import _ from 'lodash'
+import actions from '../actions'
+
+export default function status(state = {}, action) {
+ switch (action.type) {
+ case 'UNAVAILABLE_SERVER':
+ return _.assign({}, state, {fetchSuccess: false})
+ case 'FETCH_API':
+ return _.assign({}, state, {isFetching: true})
+ case 'RECEIVE_VEHICLE_JOURNEYS':
+ return _.assign({}, state, {fetchSuccess: true, isFetching: false})
+ case 'RECEIVE_ERRORS':
+ return _.assign({}, state, {fetchSuccess: true, isFetching: false})
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/reducers/stopPointsList.js b/app/javascript/vehicle_journeys/reducers/stopPointsList.js
new file mode 100644
index 000000000..9b22e08f2
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/stopPointsList.js
@@ -0,0 +1,6 @@
+export default function stopPointsList(state = [], action) {
+ switch (action.type) {
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js
new file mode 100644
index 000000000..775fefdca
--- /dev/null
+++ b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js
@@ -0,0 +1,227 @@
+import _ from 'lodash'
+import actions from '../actions'
+
+const vehicleJourney= (state = {}, action, keep) => {
+ switch (action.type) {
+ case 'SELECT_VEHICLEJOURNEY':
+ return _.assign({}, state, {selected: !state.selected})
+ case 'CANCEL_SELECTION':
+ return _.assign({}, state, {selected: false})
+ case 'ADD_VEHICLEJOURNEY':
+ let pristineVjasList = []
+ _.each(action.stopPointsList, (sp) =>{
+ let newVjas = {
+ delta: 0,
+ departure_time:{
+ hour: '00',
+ minute: '00'
+ },
+ arrival_time:{
+ hour: '00',
+ minute: '00'
+ },
+ stop_point_objectid: sp.object_id,
+ stop_area_cityname: sp.city_name,
+ dummy: true
+ }
+ _.each(action.selectedJourneyPattern.stop_areas, (jp) =>{
+ if (jp.stop_area_short_description.id == sp.id){
+ newVjas.dummy = false
+ return
+ }
+ })
+ pristineVjasList.push(newVjas)
+ })
+ return {
+ company: action.selectedCompany,
+ journey_pattern: action.selectedJourneyPattern,
+ published_journey_name: action.data.published_journey_name.value,
+ published_journey_identifier: action.data.published_journey_identifier.value,
+ objectid: '',
+ footnotes: [],
+ time_tables: [],
+ vehicle_journey_at_stops: pristineVjasList,
+ selected: false,
+ deletable: false,
+ transport_mode: window.transportMode ? window.transportMode : 'undefined',
+ transport_submode: window.transportSubmode ? window.transportSubmode : 'undefined'
+ }
+ case 'DUPLICATE_VEHICLEJOURNEY':
+ case 'SHIFT_VEHICLEJOURNEY':
+ let shiftedArray, shiftedSchedule, shiftedVjas
+ shiftedArray = state.vehicle_journey_at_stops.map((vjas, i) => {
+ if (!vjas.dummy){
+ shiftedSchedule = actions.getShiftedSchedule(vjas, action.addtionalTime)
+
+ shiftedVjas = _.assign({}, state.vehicle_journey_at_stops[i], shiftedSchedule)
+ vjas = _.assign({}, state.vehicle_journey_at_stops[i], shiftedVjas)
+ if(!keep){
+ delete vjas['id']
+ }
+ return vjas
+ }else {
+ if(!keep){
+ delete vjas['id']
+ }
+ return vjas
+ }
+ })
+ return _.assign({}, state, {vehicle_journey_at_stops: shiftedArray})
+ case 'UPDATE_TIME':
+ let vj, vjas, vjasArray, newSchedule
+ vjasArray = state.vehicle_journey_at_stops.map((vjas, i) =>{
+ if(i == action.subIndex){
+ newSchedule = {
+ departure_time: _.assign({}, vjas.departure_time),
+ arrival_time: _.assign({}, vjas.arrival_time)
+ }
+ if (action.isDeparture){
+ newSchedule.departure_time[action.timeUnit] = actions.pad(action.val, action.timeUnit)
+ if(!action.isArrivalsToggled)
+ newSchedule.arrival_time[action.timeUnit] = actions.pad(action.val, action.timeUnit)
+ newSchedule = actions.getDelta(newSchedule)
+ if(newSchedule.delta < 0){
+ return vjas
+ }
+ return _.assign({}, state.vehicle_journey_at_stops[action.subIndex], {arrival_time: newSchedule.arrival_time, departure_time: newSchedule.departure_time, delta: newSchedule.delta})
+ }else{
+ newSchedule.arrival_time[action.timeUnit] = actions.pad(action.val, action.timeUnit)
+ newSchedule = actions.getDelta(newSchedule)
+ if(newSchedule.delta < 0){
+ return vjas
+ }
+ return _.assign({}, state.vehicle_journey_at_stops[action.subIndex], {arrival_time: newSchedule.arrival_time, departure_time: newSchedule.departure_time, delta: newSchedule.delta})
+ }
+ }else{
+ return vjas
+ }
+ })
+ return _.assign({}, state, {vehicle_journey_at_stops: vjasArray})
+ default:
+ return state
+ }
+}
+
+export default function vehicleJourneys(state = [], action) {
+ switch (action.type) {
+ case 'RECEIVE_VEHICLE_JOURNEYS':
+ return [...action.json]
+ case 'RECEIVE_ERRORS':
+ return [...action.json]
+ case 'GO_TO_PREVIOUS_PAGE':
+ if(action.pagination.page > 1){
+ actions.fetchVehicleJourneys(action.dispatch, action.pagination.page, action.nextPage, action.queryString)
+ }
+ return state
+ case 'GO_TO_NEXT_PAGE':
+ if (action.pagination.totalCount - (action.pagination.page * action.pagination.perPage) > 0){
+ actions.fetchVehicleJourneys(action.dispatch, action.pagination.page, action.nextPage, action.queryString)
+ }
+ return state
+ case 'ADD_VEHICLEJOURNEY':
+ return [
+ vehicleJourney(state, action),
+ ...state
+ ]
+ case 'EDIT_VEHICLEJOURNEY':
+ return state.map((vj, i) => {
+ if (vj.selected){
+ return _.assign({}, vj, {
+ company: action.selectedCompany,
+ published_journey_name: action.data.published_journey_name.value,
+ published_journey_identifier: action.data.published_journey_identifier.value,
+ })
+ }else{
+ return vj
+ }
+ })
+ case 'EDIT_VEHICLEJOURNEY_NOTES':
+ return state.map((vj, i) => {
+ if (vj.selected){
+ return _.assign({}, vj, {
+ footnotes: action.footnotes
+ })
+ }else{
+ return vj
+ }
+ })
+ case 'EDIT_VEHICLEJOURNEYS_TIMETABLES':
+ let newTimetables = JSON.parse(JSON.stringify(action.timetables))
+ return state.map((vj,i) =>{
+ if(vj.selected){
+ let updatedVJ = _.assign({}, vj)
+ action.vehicleJourneys.map((vjm, j) =>{
+ if(vj.objectid == vjm.objectid){
+ updatedVJ.time_tables = newTimetables
+ }
+ })
+ return updatedVJ
+ }else{
+ return vj
+ }
+ })
+ case 'SHIFT_VEHICLEJOURNEY':
+ return state.map((vj, i) => {
+ if (vj.selected){
+ return vehicleJourney(vj, action, true)
+ }else{
+ return vj
+ }
+ })
+ case 'DUPLICATE_VEHICLEJOURNEY':
+ let dupeVj
+ let dupes = []
+ let selectedIndex
+ let val = action.addtionalTime
+ let departureDelta = action.departureDelta
+ state.map((vj, i) => {
+ if(vj.selected){
+ selectedIndex = i
+ for (i = 0; i< action.duplicateNumber; i++){
+ // We check if the departureDelta is != 0 to create the first VJ on the updated deparure time if it is the case
+ // let delta = departureDelta == 0 ? 1 : 0
+ // action.addtionalTime = (val * (i + delta)) + departureDelta
+ action.addtionalTime = (val * (i + 1)) + departureDelta
+ dupeVj = vehicleJourney(vj, action, false)
+ dupeVj.published_journey_name = dupeVj.published_journey_name + '-' + i
+ dupeVj.selected = false
+ delete dupeVj['objectid']
+ dupes.push(dupeVj)
+ }
+ }
+ })
+ let concatArray = state.slice(0, selectedIndex + 1).concat(dupes)
+ concatArray = concatArray.concat(state.slice(selectedIndex + 1))
+ return concatArray
+ case 'DELETE_VEHICLEJOURNEYS':
+ return state.map((vj, i) =>{
+ if (vj.selected){
+ return _.assign({}, vj, {deletable: true, selected: false})
+ } else {
+ return vj
+ }
+ })
+ case 'SELECT_VEHICLEJOURNEY':
+ return state.map((vj, i) =>{
+ if (i == action.index){
+ return vehicleJourney(vj, action)
+ } else {
+ return vj
+ }
+ })
+ case 'CANCEL_SELECTION':
+ return state.map((vj) => {
+ return vehicleJourney(vj, action)
+ })
+ case 'UPDATE_TIME':
+ return state.map((vj, i) =>{
+ if (i == action.index){
+ return vehicleJourney(vj, action)
+ } else {
+ return vj
+ }
+ })
+ default:
+ return state
+ }
+} \ No newline at end of file