diff options
| author | cedricnjanga | 2017-10-06 11:50:05 +0200 |
|---|---|---|
| committer | cedricnjanga | 2017-10-06 11:50:05 +0200 |
| commit | c4362d355d6f3b2f407e8c51bb6c0bee43f39df3 (patch) | |
| tree | f6998aa0c07043f2b4b7aaf14d719831d05f1fd8 /app/javascript | |
| parent | b6f08e58fae35d5dd8a610af31c2950b37746695 (diff) | |
| download | chouette-core-c4362d355d6f3b2f407e8c51bb6c0bee43f39df3.tar.bz2 | |
Add vehicle journey app
Diffstat (limited to 'app/javascript')
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 |
