aboutsummaryrefslogtreecommitdiffstats
path: root/app/javascript
diff options
context:
space:
mode:
authorcedricnjanga2017-10-06 10:17:17 +0200
committercedricnjanga2017-10-06 10:17:17 +0200
commitb6f08e58fae35d5dd8a610af31c2950b37746695 (patch)
tree989843dd674c41ff73eb75bd630ce4cc91fff91b /app/javascript
parent08517c27551a2dd8b227f6662f4c41574a36d81e (diff)
downloadchouette-core-b6f08e58fae35d5dd8a610af31c2950b37746695.tar.bz2
Add webpacker gem and migrate the React apps
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/date_filters/calendar.js6
-rw-r--r--app/javascript/date_filters/compliance_control_set.js5
-rw-r--r--app/javascript/date_filters/import.js5
-rw-r--r--app/javascript/date_filters/index.js13
-rw-r--r--app/javascript/date_filters/time_table.js5
-rw-r--r--app/javascript/date_filters/workbench.js3
-rw-r--r--app/javascript/helpers/clone.js12
-rw-r--r--app/javascript/helpers/date_filters.js35
-rw-r--r--app/javascript/journey_patterns/actions/index.js220
-rw-r--r--app/javascript/journey_patterns/components/App.js21
-rw-r--r--app/javascript/journey_patterns/components/ConfirmModal.js46
-rw-r--r--app/javascript/journey_patterns/components/CreateModal.js122
-rw-r--r--app/javascript/journey_patterns/components/EditModal.js110
-rw-r--r--app/javascript/journey_patterns/components/JourneyPattern.js131
-rw-r--r--app/javascript/journey_patterns/components/JourneyPatterns.js155
-rw-r--r--app/javascript/journey_patterns/components/Navigate.js62
-rw-r--r--app/javascript/journey_patterns/components/SaveJourneyPattern.js39
-rw-r--r--app/javascript/journey_patterns/containers/AddJourneyPattern.js30
-rw-r--r--app/javascript/journey_patterns/containers/ConfirmModal.js30
-rw-r--r--app/javascript/journey_patterns/containers/JourneyPatternList.js34
-rw-r--r--app/javascript/journey_patterns/containers/Modal.js26
-rw-r--r--app/javascript/journey_patterns/containers/Navigate.js15
-rw-r--r--app/javascript/journey_patterns/containers/SaveJourneyPattern.js27
-rw-r--r--app/javascript/journey_patterns/reducers/editMode.js10
-rw-r--r--app/javascript/journey_patterns/reducers/index.js18
-rw-r--r--app/javascript/journey_patterns/reducers/journeyPatterns.js90
-rw-r--r--app/javascript/journey_patterns/reducers/modal.js41
-rw-r--r--app/javascript/journey_patterns/reducers/pagination.js35
-rw-r--r--app/javascript/journey_patterns/reducers/status.js21
-rw-r--r--app/javascript/journey_patterns/reducers/stopPointsList.js6
-rw-r--r--app/javascript/packs/application.js10
-rw-r--r--app/javascript/packs/date_filters.js1
-rw-r--r--app/javascript/packs/journey_patterns/index.js49
-rw-r--r--app/javascript/packs/routes/edit.js82
-rw-r--r--app/javascript/packs/routes/show.js102
-rw-r--r--app/javascript/packs/time_tables/edit.js77
-rw-r--r--app/javascript/routes/actions/index.js62
-rw-r--r--app/javascript/routes/components/App.js25
-rw-r--r--app/javascript/routes/components/BSelect2.js125
-rw-r--r--app/javascript/routes/components/OlMap.js169
-rw-r--r--app/javascript/routes/components/StopPoint.js94
-rw-r--r--app/javascript/routes/components/StopPointList.js69
-rw-r--r--app/javascript/routes/containers/AddStopPoint.js20
-rw-r--r--app/javascript/routes/containers/VisibleStopPoints.js58
-rw-r--r--app/javascript/routes/form_helper.js55
-rw-r--r--app/javascript/routes/index.js80
-rw-r--r--app/javascript/routes/reducers/index.js8
-rw-r--r--app/javascript/routes/reducers/stopPoints.js144
-rw-r--r--app/javascript/routes/show.js102
-rw-r--r--app/javascript/time_tables/actions/index.js324
-rw-r--r--app/javascript/time_tables/components/ConfirmModal.js50
-rw-r--r--app/javascript/time_tables/components/ErrorModal.js42
-rw-r--r--app/javascript/time_tables/components/ExceptionsInDay.js71
-rw-r--r--app/javascript/time_tables/components/Metas.js139
-rw-r--r--app/javascript/time_tables/components/Navigate.js88
-rw-r--r--app/javascript/time_tables/components/PeriodForm.js148
-rw-r--r--app/javascript/time_tables/components/PeriodManager.js85
-rw-r--r--app/javascript/time_tables/components/PeriodsInDay.js75
-rw-r--r--app/javascript/time_tables/components/SaveTimetable.js42
-rw-r--r--app/javascript/time_tables/components/TagsSelect2.js77
-rw-r--r--app/javascript/time_tables/components/TimeTableDay.js31
-rw-r--r--app/javascript/time_tables/components/Timetable.js115
-rw-r--r--app/javascript/time_tables/containers/App.js55
-rw-r--r--app/javascript/time_tables/containers/ConfirmModal.js31
-rw-r--r--app/javascript/time_tables/containers/ErrorModal.js22
-rw-r--r--app/javascript/time_tables/containers/Metas.js38
-rw-r--r--app/javascript/time_tables/containers/Navigate.js18
-rw-r--r--app/javascript/time_tables/containers/PeriodForm.js46
-rw-r--r--app/javascript/time_tables/containers/SaveTimetable.js25
-rw-r--r--app/javascript/time_tables/containers/Timetable.js44
-rw-r--r--app/javascript/time_tables/reducers/index.js16
-rw-r--r--app/javascript/time_tables/reducers/metas.js40
-rw-r--r--app/javascript/time_tables/reducers/modal.js64
-rw-r--r--app/javascript/time_tables/reducers/pagination.js44
-rw-r--r--app/javascript/time_tables/reducers/status.js15
-rw-r--r--app/javascript/time_tables/reducers/timetable.js117
76 files changed, 4567 insertions, 0 deletions
diff --git a/app/javascript/date_filters/calendar.js b/app/javascript/date_filters/calendar.js
new file mode 100644
index 000000000..266fcd16a
--- /dev/null
+++ b/app/javascript/date_filters/calendar.js
@@ -0,0 +1,6 @@
+const DateFilter = require('../helpers/date_filters')
+
+const calendarDF = new DateFilter("calendar_filter_btn", "Tous les champs du filtre de date doivent être remplis", "#q_contains_date_NUMi")
+
+module.exports = calendarDF
+
diff --git a/app/javascript/date_filters/compliance_control_set.js b/app/javascript/date_filters/compliance_control_set.js
new file mode 100644
index 000000000..8ac90f54d
--- /dev/null
+++ b/app/javascript/date_filters/compliance_control_set.js
@@ -0,0 +1,5 @@
+const DateFilter = require('../helpers/date_filters')
+
+const complianceControlSetDF = new DateFilter("compliance_control_set_filter_btn", "Tous les champs du filtre de date doivent être remplis", "#q_updated_at_start_date_NUMi", "#q_updated_at_end_date_NUMi")
+
+module.exports = complianceControlSetDF \ No newline at end of file
diff --git a/app/javascript/date_filters/import.js b/app/javascript/date_filters/import.js
new file mode 100644
index 000000000..997dbd3f6
--- /dev/null
+++ b/app/javascript/date_filters/import.js
@@ -0,0 +1,5 @@
+const DateFilter = require('../helpers/date_filters')
+
+const importDF = new DateFilter("import_filter_btn", "Tous les champs du filtre de date doivent être remplis", "#q_started_at_start_date_NUMi", "#q_started_at_end_date_NUMi")
+
+module.exports = importDF \ No newline at end of file
diff --git a/app/javascript/date_filters/index.js b/app/javascript/date_filters/index.js
new file mode 100644
index 000000000..1e09c8cf6
--- /dev/null
+++ b/app/javascript/date_filters/index.js
@@ -0,0 +1,13 @@
+const calendarDF = require('./calendar')
+const complianceControlSetDF = require('./compliance_control_set')
+const timetableDF = require('./time_table')
+const importDF = require('./import')
+const workbenchDF = require('./workbench')
+
+module.exports = {
+ calendarDF: () => calendarDF,
+ complianceControlSetDF: () => complianceControlSetDF,
+ timetableDF: () => timetableDF,
+ importDF: () => importDF,
+ workbenchDF: () => workbenchDF
+} \ No newline at end of file
diff --git a/app/javascript/date_filters/time_table.js b/app/javascript/date_filters/time_table.js
new file mode 100644
index 000000000..e211bc12e
--- /dev/null
+++ b/app/javascript/date_filters/time_table.js
@@ -0,0 +1,5 @@
+const DateFilter = require('../helpers/date_filters')
+
+const timetableDF = new DateFilter("time_table_filter_btn", "Tous les champs du filtre de date doivent être remplis", "#q_bounding_dates_start_date_NUMi", "#q_bounding_dates_end_date_NUMi")
+
+module.exports = timetableDF
diff --git a/app/javascript/date_filters/workbench.js b/app/javascript/date_filters/workbench.js
new file mode 100644
index 000000000..e5fd58e53
--- /dev/null
+++ b/app/javascript/date_filters/workbench.js
@@ -0,0 +1,3 @@
+const DateFilter = require('../helpers/date_filters')
+
+const workbenchDF = new DateFilter("referential_filter_btn", "Tous les champs du filtre de date doivent être remplis", "#q_validity_period_start_date_NUMi", "#q_validity_period_end_date_NUMi")
diff --git a/app/javascript/helpers/clone.js b/app/javascript/helpers/clone.js
new file mode 100644
index 000000000..00127e2b1
--- /dev/null
+++ b/app/javascript/helpers/clone.js
@@ -0,0 +1,12 @@
+import _ from 'lodash'
+
+/* This function helps having a bit more security when we pass data from the backend to the React parts
+ It clones the obj (window variable) and then conditionnaly delete the window variable
+*/
+
+export default function clone(window, key, deletable = false) {
+ let obj = _.cloneDeep(window[key])
+
+ if (deletable) delete window[key]
+ return obj
+} \ No newline at end of file
diff --git a/app/javascript/helpers/date_filters.js b/app/javascript/helpers/date_filters.js
new file mode 100644
index 000000000..621e163ad
--- /dev/null
+++ b/app/javascript/helpers/date_filters.js
@@ -0,0 +1,35 @@
+export default function DateFilter(buttonId, message, ...inputIds) {
+ this.buttonId = buttonId
+ this.inputIds = inputIds
+ this.message = message
+
+ const getVal = (str, key) => {
+ let newStr = str.replace(/NUM/, key)
+ return $(newStr).val()
+ }
+
+ const getDates = () => {
+ return this.inputIds.reduce((arr, id) => {
+ let newIds = [1, 2, 3].map(key => getVal(id, key))
+ arr.push(...newIds)
+ return arr
+ },[])
+ }
+
+ const allInputFilled = () => {
+ return getDates().every(date => !!date)
+ }
+
+ const noInputFilled = () => {
+ return getDates().every(date => !date)
+ }
+
+ const button = document.getElementById(this.buttonId)
+
+ button && button.addEventListener('click', (event) => {
+ if (!allInputFilled() && !noInputFilled()) {
+ event.preventDefault()
+ alert(this.message)
+ }
+ })
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/actions/index.js b/app/javascript/journey_patterns/actions/index.js
new file mode 100644
index 000000000..0c1cb5f5c
--- /dev/null
+++ b/app/javascript/journey_patterns/actions/index.js
@@ -0,0 +1,220 @@
+import Promise from 'promise-polyfill'
+
+// To add to window
+if (!window.Promise) {
+ window.Promise = Promise;
+}
+
+const actions = {
+ enterEditMode: () => ({
+ type: "ENTER_EDIT_MODE"
+ }),
+ exitEditMode: () => ({
+ type: "EXIT_EDIT_MODE"
+ }),
+ receiveJourneyPatterns : (json) => ({
+ type: "RECEIVE_JOURNEY_PATTERNS",
+ json
+ }),
+ receiveErrors : (json) => ({
+ type: "RECEIVE_ERRORS",
+ json
+ }),
+ unavailableServer : () => ({
+ type: 'UNAVAILABLE_SERVER'
+ }),
+ goToPreviousPage : (dispatch, pagination) => ({
+ type: 'GO_TO_PREVIOUS_PAGE',
+ dispatch,
+ pagination,
+ nextPage : false
+ }),
+ goToNextPage : (dispatch, pagination) => ({
+ type: 'GO_TO_NEXT_PAGE',
+ dispatch,
+ pagination,
+ nextPage : true
+ }),
+ updateCheckboxValue : (e, index) => ({
+ type : 'UPDATE_CHECKBOX_VALUE',
+ id : e.currentTarget.id,
+ index
+ }),
+ checkConfirmModal : (event, callback, stateChanged,dispatch) => {
+ if(stateChanged === true){
+ return actions.openConfirmModal(callback)
+ }else{
+ dispatch(actions.fetchingApi())
+ return callback
+ }
+ },
+ openConfirmModal : (callback) => ({
+ type : 'OPEN_CONFIRM_MODAL',
+ callback
+ }),
+ openEditModal : (index, journeyPattern) => ({
+ type : 'EDIT_JOURNEYPATTERN_MODAL',
+ index,
+ journeyPattern
+ }),
+ openCreateModal : () => ({
+ type : 'CREATE_JOURNEYPATTERN_MODAL'
+ }),
+ deleteJourneyPattern : (index) => ({
+ type : 'DELETE_JOURNEYPATTERN',
+ index,
+ }),
+ closeModal : () => ({
+ type : 'CLOSE_MODAL'
+ }),
+ saveModal : (index, data) => ({
+ type: 'SAVE_MODAL',
+ data,
+ index
+ }),
+ addJourneyPattern : (data) => ({
+ type: 'ADD_JOURNEYPATTERN',
+ data,
+ }),
+ savePage : (dispatch, currentPage) => ({
+ type: 'SAVE_PAGE',
+ dispatch
+ }),
+ updateTotalCount: (diff) => ({
+ type: 'UPDATE_TOTAL_COUNT',
+ diff
+ }),
+ fetchingApi: () =>({
+ type: 'FETCH_API'
+ }),
+ resetValidation: (target) => {
+ $(target).parent().removeClass('has-error').children('.help-block').remove()
+ },
+ humanOID : (oid) => oid.split(':')[2].split("-").pop(),
+ 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
+ }
+ },
+ submitJourneyPattern : (dispatch, state, next) => {
+ dispatch(actions.fetchingApi())
+ let urlJSON = window.location.pathname + ".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.receiveJourneyPatterns(json))
+ }
+ }
+ })
+ },
+ fetchJourneyPatterns : (dispatch, currentPage, nextPage) => {
+ if(currentPage == undefined){
+ currentPage = 1
+ }
+ let journeyPatterns = []
+ 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"
+ if(page > 1){
+ str = '.json?page=' + page.toString()
+ }
+ let urlJSON = window.location.pathname + str
+ 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 {
+ if(json.length != 0){
+ let val
+ for (val of json){
+ for (let stop_point of val.route_short_description.stop_points){
+ stop_point.checked = false
+ val.stop_area_short_descriptions.map((element) => {
+ if(element.stop_area_short_description.id === stop_point.id){
+ stop_point.checked = true
+ }
+ })
+ }
+ journeyPatterns.push({
+ name: val.name,
+ object_id: val.object_id,
+ published_name: val.published_name,
+ registration_number: val.registration_number,
+ stop_points: val.route_short_description.stop_points,
+ deletable: false
+ })
+ }
+ }
+ window.currentItemsLength = journeyPatterns.length
+ dispatch(actions.receiveJourneyPatterns(journeyPatterns))
+ }
+ })
+ },
+ getChecked : (jp) => {
+ return jp.filter((obj) => {
+ return obj.checked
+ })
+ }
+}
+
+export default actions \ No newline at end of file
diff --git a/app/javascript/journey_patterns/components/App.js b/app/javascript/journey_patterns/components/App.js
new file mode 100644
index 000000000..ac6214cc1
--- /dev/null
+++ b/app/javascript/journey_patterns/components/App.js
@@ -0,0 +1,21 @@
+import React from 'react'
+import AddJourneyPattern from '../containers/AddJourneyPattern'
+import Navigate from '../containers/Navigate'
+import Modal from '../containers/Modal'
+import ConfirmModal from '../containers/ConfirmModal'
+import SaveJourneyPattern from '../containers/SaveJourneyPattern'
+import JourneyPatternList from '../containers/JourneyPatternList'
+
+const App = () => (
+ <div>
+ <Navigate />
+ <JourneyPatternList />
+ <Navigate />
+ <AddJourneyPattern />
+ <SaveJourneyPattern />
+ <ConfirmModal />
+ <Modal/>
+ </div>
+)
+
+export default App
diff --git a/app/javascript/journey_patterns/components/ConfirmModal.js b/app/javascript/journey_patterns/components/ConfirmModal.js
new file mode 100644
index 000000000..2cc1bef44
--- /dev/null
+++ b/app/javascript/journey_patterns/components/ConfirmModal.js
@@ -0,0 +1,46 @@
+import React, { PropTypes } from 'react'
+
+export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCancel, journeyPatterns}) {
+ return (
+ <div className={'modal fade ' + ((modal.type == 'confirm') ? 'in' : '')} id='ConfirmModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>Confirmation</h4>
+ </div>
+ <div className='modal-body'>
+ <div className='mt-md mb-md'>
+ <p>Vous vous apprêtez à changer de page. Voulez-vous valider vos modifications avant cela ?</p>
+ </div>
+ </div>
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={() => { onModalCancel(modal.confirmModal.callback) }}
+ >
+ Ne pas valider
+ </button>
+ <button
+ className='btn btn-primary'
+ data-dismiss='modal'
+ type='button'
+ onClick={() => { onModalAccept(modal.confirmModal.callback, journeyPatterns) }}
+ >
+ Valider
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ConfirmModal.propTypes = {
+ modal: PropTypes.object.isRequired,
+ onModalAccept: PropTypes.func.isRequired,
+ onModalCancel: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/components/CreateModal.js b/app/javascript/journey_patterns/components/CreateModal.js
new file mode 100644
index 000000000..d0eff6e57
--- /dev/null
+++ b/app/javascript/journey_patterns/components/CreateModal.js
@@ -0,0 +1,122 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class CreateModal extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ handleSubmit() {
+ if(actions.validateFields(this.refs) == true) {
+ this.props.onAddJourneyPattern(this.refs)
+ this.props.onModalClose()
+ $('#NewJourneyPatternModal').modal('hide')
+ }
+ }
+
+ render() {
+ if(this.props.status.isFetching == true || this.props.status.policy['journey_patterns.create'] == false || this.props.editMode == false) {
+ return false
+ }
+ if(this.props.status.fetchSuccess == true) {
+ return (
+ <div className="select_toolbox">
+ <ul>
+ <li className='st_action'>
+ <button
+ type='button'
+ data-toggle='modal'
+ data-target='#NewJourneyPatternModal'
+ onClick={this.props.onOpenCreateModal}
+ >
+ <span className="fa fa-plus"></span>
+ </button>
+
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'create') ? 'in' : '') } id='NewJourneyPatternModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>Ajouter une mission</h4>
+ </div>
+
+ {(this.props.modal.type == 'create') && (
+ <form>
+ <div className='modal-body'>
+ <div className='form-group'>
+ <label className='control-label is-required'>Nom</label>
+ <input
+ type='text'
+ ref='name'
+ className='form-control'
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ required
+ />
+ </div>
+ <div className='row'>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'>
+ <div className='form-group'>
+ <label className='control-label is-required'>Nom public</label>
+ <input
+ type='text'
+ ref='published_name'
+ className='form-control'
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ required
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'>
+ <div className='form-group'>
+ <label className='control-label'>Code mission</label>
+ <input
+ type='text'
+ ref='registration_number'
+ 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>
+ </ul>
+ </div>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+CreateModal.propTypes = {
+ index: PropTypes.number,
+ modal: PropTypes.object.isRequired,
+ status: PropTypes.object.isRequired,
+ onOpenCreateModal: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired,
+ onAddJourneyPattern: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/components/EditModal.js b/app/javascript/journey_patterns/components/EditModal.js
new file mode 100644
index 000000000..699f89b85
--- /dev/null
+++ b/app/javascript/journey_patterns/components/EditModal.js
@@ -0,0 +1,110 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class EditModal extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ handleSubmit() {
+ if(actions.validateFields(this.refs) == true) {
+ this.props.saveModal(this.props.modal.modalProps.index, this.refs)
+ $('#JourneyPatternModal').modal('hide')
+ }
+ }
+
+ render() {
+ return (
+ <div className={ 'modal fade ' + ((this.props.modal.type == 'edit') ? 'in' : '') } id='JourneyPatternModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>
+ Editer la mission
+ {(this.props.modal.type == 'edit') && (
+ <em> "{this.props.modal.modalProps.journeyPattern.name}"</em>
+ )}
+ </h4>
+ </div>
+
+ {(this.props.modal.type == 'edit') && (
+ <form>
+ <div className='modal-body'>
+ <div className='form-group'>
+ <label className='control-label is-required'>Nom</label>
+ <input
+ type='text'
+ ref='name'
+ className='form-control'
+ id={this.props.modal.modalProps.index}
+ defaultValue={this.props.modal.modalProps.journeyPattern.name}
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ required
+ />
+ </div>
+
+ <div className='row'>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'>
+ <div className='form-group'>
+ <label className='control-label is-required'>Nom public</label>
+ <input
+ type='text'
+ ref='published_name'
+ className='form-control'
+ id={this.props.modal.modalProps.index}
+ defaultValue={this.props.modal.modalProps.journeyPattern.published_name}
+ onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
+ required
+ />
+ </div>
+ </div>
+ <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'>
+ <div className='form-group'>
+ <label className='control-label'>Code mission</label>
+ <input
+ type='text'
+ ref='registration_number'
+ className='form-control'
+ id={this.props.modal.modalProps.index}
+ defaultValue={this.props.modal.modalProps.journeyPattern.registration_number}
+ 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>
+ )
+ }
+}
+
+EditModal.propTypes = {
+ index: PropTypes.number,
+ modal: PropTypes.object,
+ onModalClose: PropTypes.func.isRequired,
+ saveModal: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/components/JourneyPattern.js b/app/javascript/journey_patterns/components/JourneyPattern.js
new file mode 100644
index 000000000..dde73a957
--- /dev/null
+++ b/app/javascript/journey_patterns/components/JourneyPattern.js
@@ -0,0 +1,131 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class JourneyPattern extends Component{
+ constructor(props){
+ super(props)
+ this.previousCity = undefined
+ }
+
+ vehicleJourneyURL(jpOid) {
+ let routeURL = window.location.pathname.split('/', 7).join('/')
+ let vjURL = routeURL + '/vehicle_journeys?jp=' + jpOid
+
+ return (
+ <a href={vjURL}>Horaires des courses</a>
+ )
+ }
+
+ cityNameChecker(sp) {
+ let bool = false
+ if(sp.city_name != this.previousCity){
+ bool = true
+ this.previousCity = sp.city_name
+ }
+ return (
+ <div
+ className={(bool) ? 'headlined' : ''}
+ >
+ <span className='has_radio'>
+ <input
+ onChange = {(e) => this.props.onCheckboxChange(e)}
+ type='checkbox'
+ id={sp.id}
+ checked={sp.checked}
+ disabled={(this.props.value.deletable || this.props.status.policy['journey_patterns.update'] == false || this.props.editMode == false) ? 'disabled' : ''}
+ >
+ </input>
+ <span className='radio-label'></span>
+ </span>
+ </div>
+ )
+ }
+
+ getErrors(errors) {
+ let err = Object.keys(errors).map((key, index) => {
+ return (
+ <li key={index} style={{listStyleType: 'disc'}}>
+ <strong>{key}</strong> { errors[key] }
+ </li>
+ )
+ })
+
+ return (
+ <ul className="alert alert-danger">{err}</ul>
+ )
+ }
+
+ isDisabled(action) {
+ return !this.props.status.policy[`journey_patterns.${action}`] && !this.props.editMode
+ }
+
+ render() {
+ this.previousCity = undefined
+
+ return (
+ <div className={'t2e-item' + (this.props.value.deletable ? ' disabled' : '') + (this.props.value.object_id ? '' : ' to_record') + (this.props.value.errors ? ' has-error': '')}>
+ {/* Errors */}
+ {/* this.props.value.errors ? this.getErrors(this.props.value.errors) : '' */}
+
+ <div className='th'>
+ <div className='strong mb-xs'>{this.props.value.object_id ? actions.humanOID(this.props.value.object_id) : '-'}</div>
+ <div>{this.props.value.registration_number}</div>
+ <div>{actions.getChecked(this.props.value.stop_points).length} arrêt(s)</div>
+
+ <div className={this.props.value.deletable ? 'btn-group disabled' : 'btn-group'}>
+ <div
+ className={this.props.value.deletable ? 'btn dropdown-toggle disabled' : 'btn dropdown-toggle'}
+ data-toggle='dropdown'
+ >
+ <span className='fa fa-cog'></span>
+ </div>
+ <ul className='dropdown-menu'>
+ <li className={this.isDisabled('update') ? 'disabled' : ''}>
+ <button
+ type='button'
+ disabled={this.isDisabled('update')}
+ onClick={this.props.onOpenEditModal}
+ data-toggle='modal'
+ data-target='#JourneyPatternModal'
+ >
+ Editer
+ </button>
+ </li>
+ <li className={this.props.value.object_id ? '' : 'disabled'}>
+ {this.vehicleJourneyURL(this.props.value.object_id)}
+ </li>
+ <li className={'delete-action' + (this.isDisabled('destroy') ? ' disabled' : '')}>
+ <button
+ type='button'
+ disabled={this.isDisabled('destroy') ? 'disabled' : ''}
+ onClick={(e) => {
+ e.preventDefault()
+ this.props.onDeleteJourneyPattern(this.props.index)}
+ }
+ >
+ <span className='fa fa-trash'></span>Supprimer
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ {this.props.value.stop_points.map((stopPoint, i) =>{
+ return (
+ <div key={i} className='td'>
+ {this.cityNameChecker(stopPoint)}
+ </div>
+ )
+ })}
+ </div>
+ )
+ }
+}
+
+JourneyPattern.propTypes = {
+ value: PropTypes.object,
+ index: PropTypes.number,
+ onCheckboxChange: PropTypes.func.isRequired,
+ onOpenEditModal: PropTypes.func.isRequired,
+ onDeleteJourneyPattern: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/components/JourneyPatterns.js b/app/javascript/journey_patterns/components/JourneyPatterns.js
new file mode 100644
index 000000000..4b2badabb
--- /dev/null
+++ b/app/javascript/journey_patterns/components/JourneyPatterns.js
@@ -0,0 +1,155 @@
+import React, { PropTypes, Component } from 'react'
+import _ from 'lodash'
+import JourneyPattern from './JourneyPattern'
+
+
+export default class JourneyPatterns extends Component {
+ constructor(props){
+ super(props)
+ this.previousCity = undefined
+ }
+ componentDidMount() {
+ this.props.onLoadFirstPage()
+ }
+ 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.journeyPatterns, 'errors') && (
+ <div className="alert alert-danger mt-sm">
+ <strong>Erreur : </strong>
+ {this.props.journeyPatterns.map((jp, index) =>
+ jp.errors && jp.errors.map((err, i) => {
+ return (
+ <ul key={i}>
+ <li>{err}</li>
+ </ul>
+ )
+ })
+ )}
+ </div>
+ )}
+
+ <div className={'table table-2entries mt-sm mb-sm' + ((this.props.journeyPatterns.length > 0) ? '' : ' no_result')}>
+ <div className='t2e-head w20'>
+ <div className='th'>
+ <div className='strong mb-xs'>ID Mission</div>
+ <div>Code mission</div>
+ <div>Nb arrêts</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.journeyPatterns.map((journeyPattern, index) =>
+ <JourneyPattern
+ value={ journeyPattern }
+ key={ index }
+ onCheckboxChange= {(e) => this.props.onCheckboxChange(e, index)}
+ onOpenEditModal= {() => this.props.onOpenEditModal(index, journeyPattern)}
+ onDeleteJourneyPattern={() => this.props.onDeleteJourneyPattern(index)}
+ status= {this.props.status}
+ editMode= {this.props.editMode}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+ }
+}
+
+JourneyPatterns.propTypes = {
+ journeyPatterns: PropTypes.array.isRequired,
+ stopPointsList: PropTypes.array.isRequired,
+ status: PropTypes.object.isRequired,
+ onCheckboxChange: PropTypes.func.isRequired,
+ onLoadFirstPage: PropTypes.func.isRequired,
+ onOpenEditModal: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/components/Navigate.js b/app/javascript/journey_patterns/components/Navigate.js
new file mode 100644
index 000000000..f2fdd668f
--- /dev/null
+++ b/app/javascript/journey_patterns/components/Navigate.js
@@ -0,0 +1,62 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default function Navigate({ dispatch, journeyPatterns, pagination, status }) {
+ let firstPage = 1
+ let lastPage = Math.ceil(pagination.totalCount / window.journeyPatternsPerPage)
+
+ let firstItemOnPage = firstPage + (pagination.perPage * (pagination.page - firstPage))
+ let lastItemOnPage = firstItemOnPage + (pagination.perPage - firstPage)
+
+ if(status.isFetching == true) {
+ return false
+ }
+ if(status.fetchSuccess == true) {
+ return (
+ <div className='row'>
+ <div className='col-lg-12 text-right'>
+ <div className='pagination'>
+ Liste des missions {firstItemOnPage} à {(lastItemOnPage < pagination.totalCount) ? lastItemOnPage : pagination.totalCount} sur {pagination.totalCount}
+ <form className='page_links' onSubmit={e => {
+ e.preventDefault()
+ }}>
+ <button
+ onClick={e => {
+ e.preventDefault()
+ dispatch(actions.checkConfirmModal(e, actions.goToPreviousPage(dispatch, pagination), pagination.stateChanged, dispatch))
+ }}
+ type='button'
+ data-toggle=''
+ data-target='#ConfirmModal'
+ className={'previous_page' + (pagination.page == firstPage ? ' disabled' : '')}
+ disabled={(pagination.page == firstPage ? ' disabled' : '')}
+ >
+ </button>
+ <button
+ onClick={e => {
+ e.preventDefault()
+ dispatch(actions.checkConfirmModal(e, actions.goToNextPage(dispatch, pagination), pagination.stateChanged, dispatch))
+ }}
+ type='button'
+ data-toggle=''
+ data-target='#ConfirmModal'
+ className={'next_page' + (pagination.page == lastPage ? ' disabled' : '')}
+ disabled={(pagination.page == lastPage ? 'disabled' : '')}
+ >
+ </button>
+ </form>
+ </div>
+ </div>
+ </div>
+ )
+ } else {
+ return false
+ }
+}
+
+Navigate.propTypes = {
+ journeyPatterns: 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/journey_patterns/components/SaveJourneyPattern.js b/app/javascript/journey_patterns/components/SaveJourneyPattern.js
new file mode 100644
index 000000000..d071fa542
--- /dev/null
+++ b/app/javascript/journey_patterns/components/SaveJourneyPattern.js
@@ -0,0 +1,39 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class SaveJourneyPattern extends Component {
+ constructor(props){
+ super(props)
+ }
+
+ render() {
+ if(this.props.status.policy['journey_patterns.update'] == false) {
+ return false
+ }else{
+ return (
+ <div className='row mt-md'>
+ <div className='col-lg-12 text-right'>
+ <form className='jp_collection formSubmitr ml-xs' onSubmit={e => {e.preventDefault()}}>
+ <button
+ className='btn btn-default'
+ type='button'
+ onClick={e => {
+ e.preventDefault()
+ this.props.editMode ? this.props.onSubmitJourneyPattern(this.props.dispatch, this.props.journeyPatterns) : this.props.onEnterEditMode()
+ }}
+ >
+ {this.props.editMode ? "Valider" : "Editer"}
+ </button>
+ </form>
+ </div>
+ </div>
+ )
+ }
+ }
+}
+
+SaveJourneyPattern.propTypes = {
+ journeyPatterns: PropTypes.array.isRequired,
+ status: PropTypes.object.isRequired,
+ page: PropTypes.number.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/containers/AddJourneyPattern.js b/app/javascript/journey_patterns/containers/AddJourneyPattern.js
new file mode 100644
index 000000000..b093fd111
--- /dev/null
+++ b/app/javascript/journey_patterns/containers/AddJourneyPattern.js
@@ -0,0 +1,30 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import CreateModal from '../components/CreateModal'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ journeyPatterns: state.journeyPatterns,
+ editMode: state.editMode,
+ status: state.status
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ onAddJourneyPattern: (data) =>{
+ dispatch(actions.addJourneyPattern(data))
+ },
+ onOpenCreateModal: () =>{
+ dispatch(actions.openCreateModal())
+ }
+ }
+}
+
+const AddJourneyPattern = connect(mapStateToProps, mapDispatchToProps)(CreateModal)
+
+export default AddJourneyPattern
diff --git a/app/javascript/journey_patterns/containers/ConfirmModal.js b/app/javascript/journey_patterns/containers/ConfirmModal.js
new file mode 100644
index 000000000..92ce09f33
--- /dev/null
+++ b/app/javascript/journey_patterns/containers/ConfirmModal.js
@@ -0,0 +1,30 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import ConfirmModal from '../components/ConfirmModal'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ journeyPatterns: state.journeyPatterns
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalAccept: (next, state) =>{
+ dispatch(actions.fetchingApi())
+ actions.submitJourneyPattern(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/journey_patterns/containers/JourneyPatternList.js b/app/javascript/journey_patterns/containers/JourneyPatternList.js
new file mode 100644
index 000000000..d98734407
--- /dev/null
+++ b/app/javascript/journey_patterns/containers/JourneyPatternList.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import JourneyPatterns from '../components/JourneyPatterns'
+
+const mapStateToProps = (state) => {
+ return {
+ journeyPatterns: state.journeyPatterns,
+ status: state.status,
+ editMode: state.editMode,
+ stopPointsList: state.stopPointsList
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onLoadFirstPage: () =>{
+ dispatch(actions.fetchingApi())
+ actions.fetchJourneyPatterns(dispatch)
+ },
+ onCheckboxChange: (e, index) =>{
+ dispatch(actions.updateCheckboxValue(e, index))
+ },
+ onOpenEditModal: (index, journeyPattern) =>{
+ dispatch(actions.openEditModal(index, journeyPattern))
+ },
+ onDeleteJourneyPattern: (index) =>{
+ dispatch(actions.deleteJourneyPattern(index))
+ }
+ }
+}
+
+const JourneyPatternList = connect(mapStateToProps, mapDispatchToProps)(JourneyPatterns)
+
+export default JourneyPatternList
diff --git a/app/javascript/journey_patterns/containers/Modal.js b/app/javascript/journey_patterns/containers/Modal.js
new file mode 100644
index 000000000..ace71a857
--- /dev/null
+++ b/app/javascript/journey_patterns/containers/Modal.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import EditModal from '../components/EditModal'
+import CreateModal from '../components/CreateModal'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ journeyPattern: state.journeyPattern
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ },
+ saveModal: (index, data) =>{
+ dispatch(actions.saveModal(index, data))
+ }
+ }
+}
+
+const ModalContainer = connect(mapStateToProps, mapDispatchToProps)(EditModal, CreateModal)
+
+export default ModalContainer
diff --git a/app/javascript/journey_patterns/containers/Navigate.js b/app/javascript/journey_patterns/containers/Navigate.js
new file mode 100644
index 000000000..d34e0b4c5
--- /dev/null
+++ b/app/javascript/journey_patterns/containers/Navigate.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import NavigateComponent from '../components/Navigate'
+
+const mapStateToProps = (state) => {
+ return {
+ journeyPatterns: state.journeyPatterns,
+ status: state.status,
+ pagination: state.pagination
+ }
+}
+
+const Navigate = connect(mapStateToProps)(NavigateComponent)
+
+export default Navigate \ No newline at end of file
diff --git a/app/javascript/journey_patterns/containers/SaveJourneyPattern.js b/app/javascript/journey_patterns/containers/SaveJourneyPattern.js
new file mode 100644
index 000000000..b630c121c
--- /dev/null
+++ b/app/javascript/journey_patterns/containers/SaveJourneyPattern.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import SaveJourneyPatternComponent from '../components/SaveJourneyPattern'
+
+const mapStateToProps = (state) => {
+ return {
+ journeyPatterns: state.journeyPatterns,
+ editMode: state.editMode,
+ page: state.pagination.page,
+ status: state.status
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onEnterEditMode: () => {
+ dispatch(actions.enterEditMode())
+ },
+ onSubmitJourneyPattern: (next, state) => {
+ actions.submitJourneyPattern(dispatch, state, next)
+ }
+ }
+}
+
+const SaveJourneyPattern = connect(mapStateToProps, mapDispatchToProps)(SaveJourneyPatternComponent)
+
+export default SaveJourneyPattern
diff --git a/app/javascript/journey_patterns/reducers/editMode.js b/app/javascript/journey_patterns/reducers/editMode.js
new file mode 100644
index 000000000..bff976804
--- /dev/null
+++ b/app/javascript/journey_patterns/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/journey_patterns/reducers/index.js b/app/javascript/journey_patterns/reducers/index.js
new file mode 100644
index 000000000..2ffaf86d4
--- /dev/null
+++ b/app/javascript/journey_patterns/reducers/index.js
@@ -0,0 +1,18 @@
+import { combineReducers } from 'redux'
+import editMode from './editMode'
+import status from './status'
+import journeyPatterns from './journeyPatterns'
+import pagination from './pagination'
+import modal from './modal'
+import stopPointsList from './stopPointsList'
+
+const journeyPatternsApp = combineReducers({
+ editMode,
+ status,
+ journeyPatterns,
+ pagination,
+ stopPointsList,
+ modal
+})
+
+export default journeyPatternsApp
diff --git a/app/javascript/journey_patterns/reducers/journeyPatterns.js b/app/javascript/journey_patterns/reducers/journeyPatterns.js
new file mode 100644
index 000000000..7702e21bc
--- /dev/null
+++ b/app/javascript/journey_patterns/reducers/journeyPatterns.js
@@ -0,0 +1,90 @@
+import _ from 'lodash'
+import actions from "../actions"
+
+export default function journeyPattern(state = {}, action) {
+ switch (action.type) {
+ case 'ADD_JOURNEYPATTERN':
+ let stopPoints = window.stopPoints
+
+ if(stopPoints != undefined) {
+ stopPoints.map((s)=>{
+ s.checked = false
+ return s
+ })
+ }
+ return {
+ name: action.data.name.value,
+ published_name: action.data.published_name.value,
+ registration_number: action.data.registration_number.value,
+ stop_points: stopPoints,
+ deletable: false
+ }
+ case 'UPDATE_CHECKBOX_VALUE':
+ var updatedStopPoints = state.stop_points.map((s) => {
+ if (String(s.id) == action.id) {
+ return _.assign({}, s, {checked : !s.checked})
+ }else {
+ return s
+ }
+ })
+ return _.assign({}, state, {stop_points: updatedStopPoints})
+ default:
+ return state
+ }
+}
+
+const journeyPatterns = (state = [], action) => {
+ switch (action.type) {
+ case 'RECEIVE_JOURNEY_PATTERNS':
+ return [...action.json]
+ case 'RECEIVE_ERRORS':
+ return [...action.json]
+ case 'GO_TO_PREVIOUS_PAGE':
+ $('#ConfirmModal').modal('hide')
+ if(action.pagination.page > 1){
+ actions.fetchJourneyPatterns(action.dispatch, action.pagination.page, action.nextPage)
+ }
+ return state
+ case 'GO_TO_NEXT_PAGE':
+ $('#ConfirmModal').modal('hide')
+ if (action.pagination.totalCount - (action.pagination.page * action.pagination.perPage) > 0){
+ actions.fetchJourneyPatterns(action.dispatch, action.pagination.page, action.nextPage)
+ }
+ return state
+ case 'UPDATE_CHECKBOX_VALUE':
+ return state.map((j, i) =>{
+ if(i == action.index) {
+ return journeyPattern(j, action)
+ } else {
+ return j
+ }
+ })
+ case 'DELETE_JOURNEYPATTERN':
+ return state.map((j, i) =>{
+ if(i == action.index) {
+ return _.assign({}, j, {deletable: true})
+ } else {
+ return j
+ }
+ })
+ case 'ADD_JOURNEYPATTERN':
+ return [
+ journeyPattern(state, action),
+ ...state
+ ]
+ case 'SAVE_MODAL':
+ return state.map((j, i) =>{
+ if(i == action.index) {
+ return _.assign({}, j, {
+ name: action.data.name.value,
+ published_name: action.data.published_name.value,
+ registration_number: action.data.registration_number.value
+ })
+ } else {
+ return j
+ }
+ })
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/reducers/modal.js b/app/javascript/journey_patterns/reducers/modal.js
new file mode 100644
index 000000000..0a96f1679
--- /dev/null
+++ b/app/javascript/journey_patterns/reducers/modal.js
@@ -0,0 +1,41 @@
+import _ from 'lodash'
+
+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_JOURNEYPATTERN_MODAL':
+ return {
+ type: 'edit',
+ modalProps: {
+ index: action.index,
+ journeyPattern: action.journeyPattern
+ },
+ confirmModal: {}
+ }
+ case 'CREATE_JOURNEYPATTERN_MODAL':
+ return {
+ type: 'create',
+ modalProps: {},
+ confirmModal: {}
+ }
+ case 'DELETE_JOURNEYPATTERN':
+ return _.assign({}, state, { type: '' })
+ case 'SAVE_MODAL':
+ return _.assign({}, state, { type: '' })
+ case 'CLOSE_MODAL':
+ return {
+ type: '',
+ modalProps: {},
+ confirmModal: {}
+ }
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/reducers/pagination.js b/app/javascript/journey_patterns/reducers/pagination.js
new file mode 100644
index 000000000..01fdf21d4
--- /dev/null
+++ b/app/javascript/journey_patterns/reducers/pagination.js
@@ -0,0 +1,35 @@
+import _ from 'lodash'
+
+export default function pagination (state = {}, action) {
+ switch (action.type) {
+ case 'RECEIVE_JOURNEY_PATTERNS':
+ return _.assign({}, state, {stateChanged: false})
+ case 'GO_TO_PREVIOUS_PAGE':
+ if (action.pagination.page > 1){
+ toggleOnConfirmModal()
+ 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){
+ toggleOnConfirmModal()
+ return _.assign({}, state, {page : action.pagination.page + 1, stateChanged: false})
+ }
+ return state
+ case 'UPDATE_CHECKBOX_VALUE':
+ case 'ADD_JOURNEYPATTERN':
+ case 'SAVE_MODAL':
+ toggleOnConfirmModal('modal')
+ return _.assign({}, state, {stateChanged: true})
+ 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/journey_patterns/reducers/status.js b/app/javascript/journey_patterns/reducers/status.js
new file mode 100644
index 000000000..88c75966d
--- /dev/null
+++ b/app/javascript/journey_patterns/reducers/status.js
@@ -0,0 +1,21 @@
+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_JOURNEY_PATTERNS':
+ return _.assign({}, state, {fetchSuccess: true, isFetching: false})
+ case 'RECEIVE_ERRORS':
+ return _.assign({}, state, {isFetching: false})
+ case 'ENTER_EDIT_MODE':
+ return _.assign({}, state, {editMode: true})
+ case 'EXIT_EDIT_MODE':
+ return _.assign({}, state, {editMode: false})
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/journey_patterns/reducers/stopPointsList.js b/app/javascript/journey_patterns/reducers/stopPointsList.js
new file mode 100644
index 000000000..ee5eb1a80
--- /dev/null
+++ b/app/javascript/journey_patterns/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/packs/application.js b/app/javascript/packs/application.js
new file mode 100644
index 000000000..54b106ee0
--- /dev/null
+++ b/app/javascript/packs/application.js
@@ -0,0 +1,10 @@
+/* eslint no-console:0 */
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+//
+// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
+// layout file, like app/views/layouts/application.html.erb
+
+console.log('Hello World from Webpacker')
diff --git a/app/javascript/packs/date_filters.js b/app/javascript/packs/date_filters.js
new file mode 100644
index 000000000..db58bd679
--- /dev/null
+++ b/app/javascript/packs/date_filters.js
@@ -0,0 +1 @@
+const DateFilter = require('../date_filters/index') \ No newline at end of file
diff --git a/app/javascript/packs/journey_patterns/index.js b/app/javascript/packs/journey_patterns/index.js
new file mode 100644
index 000000000..fde28b45d
--- /dev/null
+++ b/app/javascript/packs/journey_patterns/index.js
@@ -0,0 +1,49 @@
+import React from 'react'
+import { render } from 'react-dom'
+import { Provider } from 'react-redux'
+import { createStore } from 'redux'
+import journeyPatternsApp from '../../journey_patterns/reducers'
+import App from '../../journey_patterns/components/App'
+import clone from '../../helpers/clone'
+
+// 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 initialState = {
+ editMode: false,
+ status: {
+ policy: window.perms,
+ fetchSuccess: true,
+ isFetching: false
+ },
+ journeyPatterns: [],
+ stopPointsList: window.stopPoints,
+ pagination: {
+ page : 1,
+ totalCount: window.journeyPatternLength,
+ perPage: window.journeyPatternsPerPage,
+ stateChanged: false
+ },
+ modal: {
+ type: '',
+ modalProps: {},
+ confirmModal: {}
+ }
+}
+// const loggerMiddleware = createLogger()
+
+let store = createStore(
+ journeyPatternsApp,
+ initialState,
+ // applyMiddleware(thunkMiddleware, promise, loggerMiddleware)
+)
+
+render(
+ <Provider store={store}>
+ <App />
+ </Provider>,
+ document.getElementById('journey_patterns')
+)
diff --git a/app/javascript/packs/routes/edit.js b/app/javascript/packs/routes/edit.js
new file mode 100644
index 000000000..810489353
--- /dev/null
+++ b/app/javascript/packs/routes/edit.js
@@ -0,0 +1,82 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { render } from 'react-dom'
+import { Provider } from 'react-redux'
+import { createStore } from 'redux'
+
+import reducers from '../../routes/reducers'
+import App from '../../routes/components/App'
+import { handleForm, handleStopPoints } from '../../routes/form_helper'
+import clone from '../../helpers/clone'
+let datas = clone(window, "itinerary_stop", true)
+datas = JSON.parse(decodeURIComponent(datas))
+
+// 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')
+
+const getInitialState = () => {
+ let state = []
+
+ datas.map(function (v, i) {
+ let fancyText = v.name.replace("&#39;", "\'")
+ if (v.zip_code && v.city_name)
+ fancyText += ", " + v.zip_code + " " + v.city_name.replace("&#39;", "\'")
+
+ state.push({
+ stoppoint_id: v.stoppoint_id,
+ stoparea_id: v.stoparea_id,
+ user_objectid: v.user_objectid,
+ short_name: v.short_name ? v.short_name.replace("&#39;", "\'") : '',
+ area_type: v.area_type,
+ index: i,
+ edit: false,
+ city_name: v.city_name ? v.city_name.replace("&#39;", "\'") : '',
+ zip_code: v.zip_code,
+ name: v.name ? v.name.replace("&#39;", "\'") : '',
+ registration_number: v.registration_number,
+ text: fancyText,
+ for_boarding: v.for_boarding || "normal",
+ for_alighting: v.for_alighting || "normal",
+ longitude: v.longitude || 0,
+ latitude: v.latitude || 0,
+ comment: v.comment ? v.comment.replace("&#39;", "\'") : '',
+ olMap: {
+ isOpened: false,
+ json: {}
+ }
+ })
+ })
+
+ return state
+}
+
+var initialState = { stopPoints: getInitialState() }
+// const loggerMiddleware = createLogger()
+let store = createStore(
+ reducers,
+ initialState
+ // applyMiddleware(thunkMiddleware, promise, loggerMiddleware)
+)
+
+render(
+ <Provider store={store}>
+ <App />
+ </Provider>,
+ document.getElementById('stop_points')
+)
+
+document.querySelector('input[name=commit]').addEventListener('click', (event) => {
+ let state = store.getState()
+
+ let name = $("#route_name").val()
+ let publicName = $("#route_published_name").val()
+ if (name == "" || publicName == "") {
+ event.preventDefault()
+ handleForm("#route_name", "#route_published_name")
+ }
+
+ handleStopPoints(event, state)
+})
diff --git a/app/javascript/packs/routes/show.js b/app/javascript/packs/routes/show.js
new file mode 100644
index 000000000..7f14a6f11
--- /dev/null
+++ b/app/javascript/packs/routes/show.js
@@ -0,0 +1,102 @@
+import clone from '../../helpers/clone'
+let route = clone(window, "route", true)
+route = JSON.parse(decodeURIComponent(route))
+
+const geoColPts = []
+const geoColLns = []
+const geoColEdges = [
+ new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)]))
+ }),
+ new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[route.length - 1].longitude), parseFloat(route[route.length - 1].latitude)]))
+ })
+]
+route.forEach(function (stop, i) {
+ if (i < route.length - 1) {
+ geoColLns.push(new ol.Feature({
+ geometry: new ol.geom.LineString([
+ ol.proj.fromLonLat([parseFloat(route[i].longitude), parseFloat(route[i].latitude)]),
+ ol.proj.fromLonLat([parseFloat(route[i + 1].longitude), parseFloat(route[i + 1].latitude)])
+ ])
+ }))
+ }
+ geoColPts.push(new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(stop.longitude), parseFloat(stop.latitude)]))
+ })
+ )
+})
+var edgeStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 5,
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 2
+ }),
+ fill: new ol.style.Fill({
+ color: '#007fbb',
+ width: 2
+ })
+ }))
+})
+var defaultStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 4,
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 2
+ }),
+ fill: new ol.style.Fill({
+ color: '#ffffff',
+ width: 2
+ })
+ }))
+})
+var lineStyle = new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 3
+ })
+})
+
+var vectorPtsLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColPts
+ }),
+ style: defaultStyles,
+ zIndex: 2
+})
+var vectorEdgesLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColEdges
+ }),
+ style: edgeStyles,
+ zIndex: 3
+})
+var vectorLnsLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColLns
+ }),
+ style: [lineStyle],
+ zIndex: 1
+})
+
+var map = new ol.Map({
+ target: 'route_map',
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ }),
+ vectorPtsLayer,
+ vectorEdgesLayer,
+ vectorLnsLayer
+ ],
+ controls: [new ol.control.ScaleLine(), new ol.control.Zoom(), new ol.control.ZoomSlider()],
+ interactions: ol.interaction.defaults({
+ zoom: true
+ }),
+ view: new ol.View({
+ center: ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)]),
+ zoom: 13
+ })
+});
diff --git a/app/javascript/packs/time_tables/edit.js b/app/javascript/packs/time_tables/edit.js
new file mode 100644
index 000000000..cf058d501
--- /dev/null
+++ b/app/javascript/packs/time_tables/edit.js
@@ -0,0 +1,77 @@
+import React from 'react'
+import { render } from 'react-dom'
+import { Provider } from 'react-redux'
+import { createStore } from 'redux'
+import timeTablesApp from '../../time_tables/reducers'
+import App from '../../time_tables/containers/App'
+import clone from '../../helpers/clone'
+
+const actionType = clone(window, "actionType", true)
+
+// 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')
+
+let initialState = {
+ status: {
+ actionType: actionType,
+ policy: window.perms,
+ fetchSuccess: true,
+ isFetching: false
+ },
+ timetable: {
+ current_month: [],
+ current_periode_range: '',
+ periode_range: [],
+ time_table_periods: [],
+ time_table_dates: []
+ },
+ metas: {
+ comment: '',
+ day_types: [],
+ tags: [],
+ initial_tags: [],
+ color: '',
+ calendar: null
+ },
+ pagination: {
+ stateChanged: false,
+ currentPage: '',
+ periode_range: []
+ },
+ modal: {
+ type: '',
+ modalProps: {
+ active: false,
+ begin: {
+ day: '01',
+ month: '01',
+ year: String(new Date().getFullYear())
+ },
+ end: {
+ day: '01',
+ month: '01',
+ year: String(new Date().getFullYear())
+ },
+ index: false,
+ error: ''
+ },
+ confirmModal: {}
+ }
+}
+// const loggerMiddleware = createLogger()
+
+let store = createStore(
+ timeTablesApp,
+ initialState,
+ // applyMiddleware(thunkMiddleware, promise, loggerMiddleware)
+)
+
+render(
+ <Provider store={store}>
+ <App />
+ </Provider>,
+ document.getElementById('periods')
+)
diff --git a/app/javascript/routes/actions/index.js b/app/javascript/routes/actions/index.js
new file mode 100644
index 000000000..13b2d60b2
--- /dev/null
+++ b/app/javascript/routes/actions/index.js
@@ -0,0 +1,62 @@
+const actions = {
+ addStop : () => {
+ return {
+ type: 'ADD_STOP'
+ }
+ },
+ moveStopUp : (index) => {
+ return {
+ type: 'MOVE_STOP_UP',
+ index
+ }
+ },
+ moveStopDown : (index) => {
+ return {
+ type: 'MOVE_STOP_DOWN',
+ index
+ }
+ },
+ deleteStop : (index) => {
+ return {
+ type: 'DELETE_STOP',
+ index
+ }
+ },
+ updateInputValue : (index, text) => {
+ return {
+ type : 'UPDATE_INPUT_VALUE',
+ index,
+ text
+ }
+ },
+ updateSelectValue: (e, index) => {
+ return {
+ type :'UPDATE_SELECT_VALUE',
+ select_id: e.currentTarget.id,
+ select_value: e.currentTarget.value,
+ index
+ }
+ },
+ toggleMap: (index) =>({
+ type: 'TOGGLE_MAP',
+ index
+ }),
+ toggleEdit: (index) =>({
+ type: 'TOGGLE_EDIT',
+ index
+ }),
+ closeMaps: () => ({
+ type : 'CLOSE_MAP'
+ }),
+ selectMarker: (index, data) =>({
+ type: 'SELECT_MARKER',
+ index,
+ data
+ }),
+ unselectMarker: (index) => ({
+ type: 'UNSELECT_MARKER',
+ index
+ })
+}
+
+module.exports = actions
diff --git a/app/javascript/routes/components/App.js b/app/javascript/routes/components/App.js
new file mode 100644
index 000000000..0f5786407
--- /dev/null
+++ b/app/javascript/routes/components/App.js
@@ -0,0 +1,25 @@
+import React, { Component, PropTypes } from 'react'
+import AddStopPoint from '../containers/AddStopPoint'
+import VisibleStopPoints from'../containers/VisibleStopPoints'
+import clone from '../../helpers/clone'
+const I18n = clone(window , "I18n", true)
+
+export default class App extends Component {
+
+ getChildContext() {
+ return { I18n }
+ }
+
+ render() {
+ return (
+ <div>
+ <VisibleStopPoints />
+ <AddStopPoint />
+ </div>
+ )
+ }
+}
+
+App.childContextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/routes/components/BSelect2.js b/app/javascript/routes/components/BSelect2.js
new file mode 100644
index 000000000..5cc9561e9
--- /dev/null
+++ b/app/javascript/routes/components/BSelect2.js
@@ -0,0 +1,125 @@
+import _ from'lodash'
+import React, { Component, PropTypes } from 'react'
+import Select2 from 'react-select2-wrapper'
+
+
+// get JSON full path
+var origin = window.location.origin
+var path = window.location.pathname.split('/', 3).join('/')
+
+
+class BSelect3 extends Component{
+ constructor(props, context) {
+ super(props, context)
+ }
+ onChange(e) {
+ this.props.onChange(this.props.index, {
+ text: e.currentTarget.textContent,
+ stoparea_id: e.currentTarget.value,
+ user_objectid: e.params.data.user_objectid,
+ longitude: e.params.data.longitude,
+ latitude: e.params.data.latitude,
+ name: e.params.data.name,
+ short_name: e.params.data.short_name,
+ city_name: e.params.data.city_name,
+ area_type: e.params.data.area_type,
+ zip_code: e.params.data.zip_code,
+ comment: e.params.data.comment
+ })
+ }
+
+ parsedText(data) {
+ let a = data.replace('</em></small>', '')
+ let b = a.split('<small><em>')
+ if (b.length > 1) {
+ return (
+ <span>
+ {b[0]}
+ <small><em>{b[1]}</em></small>
+ </span>
+ )
+ } else {
+ return (
+ <span>{data}</span>
+ )
+ }
+ }
+
+ render() {
+ if(this.props.value.edit)
+ return (
+ <div className='select2-bootstrap-append'>
+ <BSelect2 {...this.props} onSelect={ this.onChange.bind(this) }/>
+ </div>
+ )
+ else
+ if(!this.props.value.stoparea_id)
+ return (
+ <div>
+ <BSelect2 {...this.props} onSelect={ this.onChange.bind(this) }/>
+ </div>
+ )
+ else
+ return (
+ <a
+ className='navlink'
+ href={origin + path + '/stop_areas/' + this.props.value.stoparea_id}
+ title="Voir l'arrêt"
+ >
+ {this.parsedText(this.props.value.text)}
+ </a>
+ )
+ }
+}
+
+export default class BSelect2 extends Component{
+ componentDidMount() {
+ this.refs.newSelect.el.select2('open')
+ }
+
+ render() {
+ return (
+ <Select2
+ value={ this.props.value.stoparea_id }
+ onSelect={ this.props.onSelect }
+ ref='newSelect'
+ options={{
+ placeholder: this.context.I18n.routes.edit.select2.placeholder,
+ allowClear: true,
+ language: 'fr', /* Doesn't seem to work... :( */
+ theme: 'bootstrap',
+ width: '100%',
+ ajax: {
+ url: origin + path + '/autocomplete_stop_areas.json',
+ dataType: 'json',
+ delay: '500',
+ data: function(params) {
+ return {
+ q: params.term,
+ target_type: 'zdep'
+ };
+ },
+ processResults: function(data, params) {
+ return {
+ results: data.map(
+ item => _.assign(
+ {},
+ item,
+ { text: item.name + ", " + item.zip_code + " " + item.short_city_name + " <small><em>(" + item.user_objectid + ")</em></small>" }
+ )
+ )
+ };
+ },
+ cache: true
+ },
+ escapeMarkup: function (markup) { return markup; },
+ minimumInputLength: 3
+ }}
+ />
+ )
+ }
+}
+
+BSelect2.contextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/routes/components/OlMap.js b/app/javascript/routes/components/OlMap.js
new file mode 100644
index 000000000..2c01dfa7f
--- /dev/null
+++ b/app/javascript/routes/components/OlMap.js
@@ -0,0 +1,169 @@
+import _ from 'lodash'
+import React, { Component, PropTypes } from 'react'
+
+export default class OlMap extends Component{
+ constructor(props, context){
+ super(props, context)
+ }
+
+ fetchApiURL(id){
+ const origin = window.location.origin
+ const path = window.location.pathname.split('/', 3).join('/')
+ return origin + path + "/autocomplete_stop_areas/" + id + "/around?target_type=zdep"
+ }
+
+ componentDidUpdate(prevProps, prevState){
+ if(prevProps.value.olMap.isOpened == false && this.props.value.olMap.isOpened == true){
+ var source = new ol.source.Vector({
+ format: new ol.format.GeoJSON(),
+ url: this.fetchApiURL(this.props.value.stoparea_id)
+ })
+ var feature = new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(this.props.value.longitude), parseFloat(this.props.value.latitude)]))
+ })
+
+ var defaultStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 4,
+ fill: new ol.style.Fill({
+ color: '#004d87'
+ })
+ }))
+ })
+ var selectedStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 6,
+ fill: new ol.style.Fill({
+ color: '#da2f36'
+ })
+ }))
+ })
+
+ var centerLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: [feature]
+ }),
+ style: selectedStyles,
+ zIndex: 2
+ })
+ var vectorLayer = new ol.layer.Vector({
+ source: source,
+ style: defaultStyles,
+ zIndex: 1
+ });
+
+ var map = new ol.Map({
+ target: 'stoppoint_map' + this.props.index,
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ }),
+ vectorLayer,
+ centerLayer
+ ],
+ controls: [ new ol.control.ScaleLine() ],
+ interactions: ol.interaction.defaults({
+ dragPan: false,
+ doubleClickZoom: false,
+ shiftDragZoom: false,
+ mouseWheelZoom: false
+ }),
+ view: new ol.View({
+ center: ol.proj.fromLonLat([parseFloat(this.props.value.longitude), parseFloat(this.props.value.latitude)]),
+ zoom: 18
+ })
+ });
+
+ // Selectable marker
+ var select = new ol.interaction.Select({
+ style: selectedStyles
+ });
+
+ map.addInteraction(select);
+
+ select.on('select', function(e) {
+ feature.setStyle(defaultStyles);
+ centerLayer.setZIndex(0);
+
+ if(e.selected.length != 0) {
+
+ if(e.selected[0].getGeometry() == feature.getGeometry()) {
+ if(e.selected[0].style_.image_.fill_.color_ != '#da2f36'){
+ feature.setStyle(selectedStyles);
+ centerLayer.setZIndex(2);
+ e.preventDefault()
+ return false
+ }
+ }
+ let data = _.assign({}, e.selected[0].getProperties(), {geometry: undefined});
+
+ this.props.onSelectMarker(this.props.index, data)
+ } else {
+ this.props.onUnselectMarker(this.props.index)
+ }
+ }, this);
+ }
+ }
+
+ render() {
+ if (this.props.value.olMap.isOpened) {
+ return (
+ <div className='map_container'>
+ <div className='map_metas'>
+ <p>
+ <strong>{this.props.value.olMap.json.name}</strong>
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.stop_point_type} : </strong>
+ {this.props.value.olMap.json.area_type}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.short_name} : </strong>
+ {this.props.value.olMap.json.short_name}
+ </p>
+ <p>
+ <strong>{this.context.I18n.id_reflex} : </strong>
+ {this.props.value.olMap.json.user_objectid}
+ </p>
+
+ <p><strong>{this.context.I18n.routes.edit.map.coordinates} : </strong></p>
+ <p style={{paddingLeft: 10, marginTop: 0}}>
+ <em>{this.context.I18n.routes.edit.map.proj}.: </em>WSG84<br/>
+ <em>{this.context.I18n.routes.edit.map.lat}.: </em>{this.props.value.olMap.json.latitude} <br/>
+ <em>{this.context.I18n.routes.edit.map.lon}.: </em>{this.props.value.olMap.json.longitude}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.map.postal_code} : </strong>
+ {this.props.value.olMap.json.zip_code}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.map.city} : </strong>
+ {this.props.value.olMap.json.city_name}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.map.comment} : </strong>
+ {this.props.value.olMap.json.comment}
+ </p>
+ {(this.props.value.stoparea_id != this.props.value.olMap.json.stoparea_id) &&(
+ <div className='btn btn-outline-primary btn-sm'
+ onClick= {() => {this.props.onUpdateViaOlMap(this.props.index, this.props.value.olMap.json)}}
+ >{this.context.I18n.actions.select}</div>
+ )}
+ </div>
+ <div className='map_content'>
+ <div id={"stoppoint_map" + this.props.index} className='map'></div>
+ </div>
+ </div>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+OlMap.PropTypes = {
+}
+
+OlMap.contextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/routes/components/StopPoint.js b/app/javascript/routes/components/StopPoint.js
new file mode 100644
index 000000000..606121f99
--- /dev/null
+++ b/app/javascript/routes/components/StopPoint.js
@@ -0,0 +1,94 @@
+import React, { PropTypes } from 'react'
+import BSelect2 from './BSelect2'
+import OlMap from './OlMap'
+
+export default function StopPoint(props, {I18n}) {
+ return (
+ <div className='nested-fields'>
+ <div className='wrapper'>
+ <div style={{width: 90}}>
+ <span>{props.value.user_objectid}</span>
+ </div>
+
+ <div>
+ <BSelect2 id={'route_stop_points_' + props.id} value={props.value} onChange={props.onChange} index={props.index} />
+ </div>
+
+ <div>
+ <select className='form-control' value={props.value.for_boarding} id="for_boarding" onChange={props.onSelectChange}>
+ <option value="normal">{I18n.routes.edit.stop_point.boarding.normal}</option>
+ <option value="forbidden">{I18n.routes.edit.stop_point.boarding.forbidden}</option>
+ </select>
+ </div>
+
+ <div>
+ <select className='form-control' value={props.value.for_alighting} id="for_alighting" onChange={props.onSelectChange}>
+ <option value="normal">{I18n.routes.edit.stop_point.alighting.normal}</option>
+ <option value="forbidden">{I18n.routes.edit.stop_point.alighting.forbidden}</option>
+ </select>
+ </div>
+
+ <div className='actions-5'>
+ <div
+ className={'btn btn-link' + (props.value.stoparea_id ? '' : ' disabled')}
+ onClick={props.onToggleMap}
+ >
+ <span className='fa fa-map-marker'></span>
+ </div>
+
+ <div
+ className={'btn btn-link' + (props.first ? ' disabled' : '')}
+ onClick={props.onMoveUpClick}
+ >
+ <span className='fa fa-arrow-up'></span>
+ </div>
+ <div
+ className={'btn btn-link' + (props.last ? ' disabled' : '')}
+ onClick={props.onMoveDownClick}
+ >
+ <span className='fa fa-arrow-down'></span>
+ </div>
+
+ <div
+ className='btn btn-link'
+ onClick={props.onToggleEdit}
+ >
+ <span className={'fa' + (props.value.edit ? ' fa-check' : ' fa-pencil')}></span>
+ </div>
+ <div
+ className='btn btn-link'
+ onClick={props.onDeleteClick}
+ >
+ <span className='fa fa-trash text-danger'></span>
+ </div>
+ </div>
+ </div>
+
+ <OlMap
+ value = {props.value}
+ index = {props.index}
+ onSelectMarker = {props.onSelectMarker}
+ onUnselectMarker = {props.onUnselectMarker}
+ onUpdateViaOlMap = {props.onUpdateViaOlMap}
+ />
+ </div>
+ )
+}
+
+StopPoint.PropTypes = {
+ onToggleMap: PropTypes.func.isRequired,
+ onToggleEdit: PropTypes.func.isRequired,
+ onDeleteClick: PropTypes.func.isRequired,
+ onMoveUpClick: PropTypes.func.isRequired,
+ onMoveDownClick: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSelectChange: PropTypes.func.isRequired,
+ first: PropTypes.bool,
+ last: PropTypes.bool,
+ index: PropTypes.number,
+ value: PropTypes.object
+}
+
+StopPoint.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/routes/components/StopPointList.js b/app/javascript/routes/components/StopPointList.js
new file mode 100644
index 000000000..68af16f57
--- /dev/null
+++ b/app/javascript/routes/components/StopPointList.js
@@ -0,0 +1,69 @@
+import React, { PropTypes } from 'react'
+import StopPoint from './StopPoint'
+
+export default function StopPointList({ stopPoints, onDeleteClick, onMoveUpClick, onMoveDownClick, onChange, onSelectChange, onToggleMap, onToggleEdit, onSelectMarker, onUnselectMarker, onUpdateViaOlMap }, {I18n}) {
+ return (
+ <div className='subform'>
+ <div className='nested-head'>
+ <div className="wrapper">
+ <div style={{width: 100}}>
+ <div className="form-group">
+ <label className="control-label">{I18n.reflex_id}</label>
+ </div>
+ </div>
+ <div>
+ <div className="form-group">
+ <label className="control-label">{I18n.simple_form.labels.stop_point.name}</label>
+ </div>
+ </div>
+ <div>
+ <div className="form-group">
+ <label className="control-label">{I18n.simple_form.labels.stop_point.for_boarding}</label>
+ </div>
+ </div>
+ <div>
+ <div className="form-group">
+ <label className="control-label">{I18n.simple_form.labels.stop_point.for_alighting}</label>
+ </div>
+ </div>
+ <div className='actions-5'></div>
+ </div>
+ </div>
+ {stopPoints.map((stopPoint, index) =>
+ <StopPoint
+ key={'item-' + index}
+ onDeleteClick={() => onDeleteClick(index)}
+ onMoveUpClick={() => {
+ onMoveUpClick(index)
+ }}
+ onMoveDownClick={() => onMoveDownClick(index)}
+ onChange={ onChange }
+ onSelectChange={ (e) => onSelectChange(e, index) }
+ onToggleMap={() => onToggleMap(index)}
+ onToggleEdit={() => onToggleEdit(index)}
+ onSelectMarker={onSelectMarker}
+ onUnselectMarker={onUnselectMarker}
+ onUpdateViaOlMap={onUpdateViaOlMap}
+ first={ index === 0 }
+ last={ index === (stopPoints.length - 1) }
+ index={ index }
+ value={ stopPoint }
+ />
+ )}
+ </div>
+ )
+}
+
+StopPointList.PropTypes = {
+ stopPoints: PropTypes.array.isRequired,
+ onDeleteClick: PropTypes.func.isRequired,
+ onMoveUpClick: PropTypes.func.isRequired,
+ onMoveDownClick: PropTypes.func.isRequired,
+ onSelectChange: PropTypes.func.isRequired,
+ onSelectMarker: PropTypes.func.isRequired,
+ onUnselectMarker : PropTypes.func.isRequired
+}
+
+StopPointList.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/routes/containers/AddStopPoint.js b/app/javascript/routes/containers/AddStopPoint.js
new file mode 100644
index 000000000..fd9227ff3
--- /dev/null
+++ b/app/javascript/routes/containers/AddStopPoint.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import actions from '../actions'
+
+let AddStopPoint = ({ dispatch }) => {
+ return (
+ <div className="nested-linker">
+ <form onSubmit={e => {
+ e.preventDefault()
+ dispatch(actions.closeMaps())
+ dispatch(actions.addStop())
+ }}>
+ <button type="submit" className="btn btn-outline-primary">
+ Ajouter un arrêt
+ </button>
+ </form>
+ </div>
+ )
+}
+export default AddStopPoint = connect()(AddStopPoint)
diff --git a/app/javascript/routes/containers/VisibleStopPoints.js b/app/javascript/routes/containers/VisibleStopPoints.js
new file mode 100644
index 000000000..67d77af50
--- /dev/null
+++ b/app/javascript/routes/containers/VisibleStopPoints.js
@@ -0,0 +1,58 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import StopPointList from '../components/StopPointList'
+
+const mapStateToProps = (state) => {
+ return {
+ stopPoints: state.stopPoints
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onDeleteClick: (index) =>{
+ dispatch(actions.deleteStop(index))
+ dispatch(actions.closeMaps())
+ },
+ onMoveUpClick: (index) =>{
+ dispatch(actions.moveStopUp(index))
+ dispatch(actions.closeMaps())
+ },
+ onMoveDownClick: (index) =>{
+ dispatch(actions.moveStopDown(index))
+ dispatch(actions.closeMaps())
+ },
+ onChange: (index, text) =>{
+ dispatch(actions.updateInputValue(index, text))
+ dispatch(actions.closeMaps())
+ dispatch(actions.toggleEdit(index))
+ },
+ onSelectChange: (e, index) =>{
+ dispatch(actions.updateSelectValue(e, index))
+ dispatch(actions.closeMaps())
+ },
+ onToggleMap: (index) =>{
+ dispatch(actions.toggleMap(index))
+ },
+ onToggleEdit: (index) =>{
+ dispatch(actions.toggleEdit(index))
+ },
+ onSelectMarker: (index, data) =>{
+ dispatch(actions.selectMarker(index, data))
+ },
+ onUnselectMarker: (index) =>{
+ dispatch(actions.unselectMarker(index))
+ },
+ onUpdateViaOlMap: (index, data) =>{
+ dispatch(actions.updateInputValue(index, data))
+ dispatch(actions.toggleMap(index))
+ }
+ }
+}
+
+const VisibleStopPoints = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(StopPointList)
+
+export default VisibleStopPoints
diff --git a/app/javascript/routes/form_helper.js b/app/javascript/routes/form_helper.js
new file mode 100644
index 000000000..8a3277234
--- /dev/null
+++ b/app/javascript/routes/form_helper.js
@@ -0,0 +1,55 @@
+const formHelper = {
+ addInput: (name, value, index) => {
+ let form = document.querySelector('form')
+ let input = document.createElement('input')
+ let formatedName = `route[stop_points_attributes][${index.toString()}][${name}]`
+ input.setAttribute('type', 'hidden')
+ input.setAttribute('name', formatedName)
+ input.setAttribute('value', value)
+ form.appendChild(input)
+ },
+ addError: (ids) => {
+ ids.forEach((id) => {
+ if (!$(id).parents('.form-group').hasClass('has-error')) {
+ $(id).parents('.form-group').addClass('has-error')
+ $(id).parent().append(`<span class='help-block small'>${'doit être rempli(e)'}</span>`)
+ }
+ })
+ },
+ cleanInputs: (ids) => {
+ ids.forEach((id) =>{
+ $(id).parents('.form-group').removeClass('has-error')
+ $(id).siblings('span').remove()
+ })
+ },
+ handleForm: (...ids) => {
+ let filledInputs = []
+ let blankInputs = []
+ ids.forEach(id => {
+ $(id).val() == "" ? blankInputs.push(id) : filledInputs.push(id)
+ })
+
+ if (filledInputs.length > 0) formHelper.cleanInputs(filledInputs)
+ if (blankInputs.length > 0) formHelper.addError(blankInputs)
+ },
+ handleStopPoints: (event, state) => {
+ if (state.stopPoints.length >= 2) {
+ state.stopPoints.map((stopPoint, i) => {
+ formHelper.addInput('id', stopPoint.stoppoint_id ? stopPoint.stoppoint_id : '', i)
+ formHelper.addInput('stop_area_id', stopPoint.stoparea_id, i)
+ formHelper.addInput('position', i, i)
+ formHelper.addInput('for_boarding', stopPoint.for_boarding, i)
+ formHelper.addInput('for_alighting', stopPoint.for_alighting, i)
+ })
+ if ($('.alert.alert-danger').length > 0) $('.alert.alert-danger').remove()
+ } else {
+ event.preventDefault()
+ let msg = "L'itinéraire doit comporter au moins deux arrêts"
+ if ($('.alert.alert-danger').length == 0) {
+ $('#stop_points').find('.subform').after(`<div class='alert alert-danger'><span class='fa fa-lg fa-exclamation-circle'></span><span>" ${msg} "</span></div>`)
+ }
+ }
+ }
+}
+
+export default formHelper \ No newline at end of file
diff --git a/app/javascript/routes/index.js b/app/javascript/routes/index.js
new file mode 100644
index 000000000..febae7d54
--- /dev/null
+++ b/app/javascript/routes/index.js
@@ -0,0 +1,80 @@
+import React from 'react'
+import { render } from 'react-dom'
+import { Provider } from 'react-redux'
+import { createStore } from 'redux'
+import reducers from './reducers'
+import App from './components/App'
+import { handleForm, handleStopPoints } from './form_helper'
+import clone from '../helpers/clone'
+let datas = clone(window, "itinerary_stop", true)
+datas = JSON.parse(decodeURIComponent(datas))
+
+// 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')
+
+const getInitialState = () => {
+ let state = []
+
+ datas.map(function(v, i) {
+ let fancyText = v.name.replace("&#39;", "\'")
+ if(v.zip_code && v.city_name)
+ fancyText += ", " + v.zip_code + " " + v.city_name.replace("&#39;", "\'")
+
+ state.push({
+ stoppoint_id: v.stoppoint_id,
+ stoparea_id: v.stoparea_id,
+ user_objectid: v.user_objectid,
+ short_name: v.short_name ? v.short_name.replace("&#39;", "\'") : '',
+ area_type: v.area_type,
+ index: i,
+ edit: false,
+ city_name: v.city_name ? v.city_name.replace("&#39;", "\'") : '',
+ zip_code: v.zip_code,
+ name: v.name ? v.name.replace("&#39;", "\'") : '',
+ registration_number: v.registration_number,
+ text: fancyText,
+ for_boarding: v.for_boarding || "normal",
+ for_alighting: v.for_alighting || "normal",
+ longitude: v.longitude || 0,
+ latitude: v.latitude || 0,
+ comment: v.comment ? v.comment.replace("&#39;", "\'") : '',
+ olMap: {
+ isOpened: false,
+ json: {}
+ }
+ })
+ })
+
+ return state
+}
+
+var initialState = {stopPoints: getInitialState()}
+// const loggerMiddleware = createLogger()
+let store = createStore(
+ reducers,
+ initialState
+ // applyMiddleware(thunkMiddleware, promise, loggerMiddleware)
+)
+
+render(
+ <Provider store={store}>
+ <App />
+ </Provider>,
+ document.getElementById('stop_points')
+)
+
+document.querySelector('input[name=commit]').addEventListener('click', (event)=>{
+ let state = store.getState()
+
+ let name = $("#route_name").val()
+ let publicName = $("#route_published_name").val()
+ if (name == "" || publicName == "") {
+ event.preventDefault()
+ handleForm("#route_name", "#route_published_name")
+ }
+
+ handleStopPoints(event, state)
+})
diff --git a/app/javascript/routes/reducers/index.js b/app/javascript/routes/reducers/index.js
new file mode 100644
index 000000000..eb01ea9f7
--- /dev/null
+++ b/app/javascript/routes/reducers/index.js
@@ -0,0 +1,8 @@
+import { combineReducers } from 'redux'
+import stopPoints from './stopPoints'
+
+const stopPointsApp = combineReducers({
+ stopPoints
+})
+
+export default stopPointsApp
diff --git a/app/javascript/routes/reducers/stopPoints.js b/app/javascript/routes/reducers/stopPoints.js
new file mode 100644
index 000000000..25679e747
--- /dev/null
+++ b/app/javascript/routes/reducers/stopPoints.js
@@ -0,0 +1,144 @@
+import _ from 'lodash'
+import { addInput } from '../form_helper'
+
+const stopPoint = (state = {}, action, length) => {
+ switch (action.type) {
+ case 'ADD_STOP':
+ return {
+ text: '',
+ index: length,
+ edit: true,
+ for_boarding: 'normal',
+ for_alighting: 'normal',
+ olMap: {
+ isOpened: false,
+ json: {}
+ }
+ }
+ default:
+ return state
+ }
+}
+
+const updateFormForDeletion = (stop) =>{
+ if (stop.stoppoint_id !== undefined){
+ let now = Date.now()
+ addInput('id', stop.stoppoint_id, now)
+ addInput('_destroy', 'true', now)
+ }
+}
+
+const stopPoints = (state = [], action) => {
+ switch (action.type) {
+ case 'ADD_STOP':
+ return [
+ ...state,
+ stopPoint(undefined, action, state.length)
+ ]
+ case 'MOVE_STOP_UP':
+ return [
+ ...state.slice(0, action.index - 1),
+ _.assign({}, state[action.index], { stoppoint_id: state[action.index - 1].stoppoint_id }),
+ _.assign({}, state[action.index - 1], { stoppoint_id: state[action.index].stoppoint_id }),
+ ...state.slice(action.index + 1)
+ ]
+ case 'MOVE_STOP_DOWN':
+ return [
+ ...state.slice(0, action.index),
+ _.assign({}, state[action.index + 1], { stoppoint_id: state[action.index].stoppoint_id }),
+ _.assign({}, state[action.index], { stoppoint_id: state[action.index + 1].stoppoint_id }),
+ ...state.slice(action.index + 2)
+ ]
+ case 'DELETE_STOP':
+ updateFormForDeletion(state[action.index])
+ return [
+ ...state.slice(0, action.index),
+ ...state.slice(action.index + 1).map((stopPoint)=>{
+ stopPoint.index--
+ return stopPoint
+ })
+ ]
+ case 'UPDATE_INPUT_VALUE':
+ return state.map( (t, i) => {
+ if (i === action.index) {
+ return _.assign(
+ {},
+ t,
+ {
+ stoppoint_id: t.stoppoint_id,
+ text: action.text.text,
+ stoparea_id: action.text.stoparea_id,
+ user_objectid: action.text.user_objectid,
+ latitude: action.text.latitude,
+ longitude: action.text.longitude,
+ name: action.text.name,
+ short_name: action.text.short_name,
+ area_type: action.text.area_type,
+ city_name: action.text.city_name,
+ comment: action.text.comment,
+ registration_number: action.text.registration_number
+ }
+ )
+ } else {
+ return t
+ }
+ })
+ case 'UPDATE_SELECT_VALUE':
+ return state.map( (t, i) => {
+ if (i === action.index) {
+ let stopState = _.assign({}, t)
+ stopState[action.select_id] = action.select_value
+ return stopState
+ } else {
+ return t
+ }
+ })
+ case 'TOGGLE_EDIT':
+ return state.map((t, i) => {
+ if (i === action.index){
+ return _.assign({}, t, {edit: !t.edit})
+ } else {
+ return t
+ }
+ })
+ case 'TOGGLE_MAP':
+ return state.map( (t, i) => {
+ if (i === action.index){
+ let val = !t.olMap.isOpened
+ let jsonData = val ? _.assign({}, t, {olMap: undefined}) : {}
+ let stateMap = _.assign({}, t.olMap, {isOpened: val, json: jsonData})
+ return _.assign({}, t, {olMap: stateMap})
+ }else {
+ let emptyMap = _.assign({}, t.olMap, {isOpened: false, json : {}})
+ return _.assign({}, t, {olMap: emptyMap})
+ }
+ })
+ case 'SELECT_MARKER':
+ return state.map((t, i) => {
+ if (i === action.index){
+ let stateMap = _.assign({}, t.olMap, {json: action.data})
+ return _.assign({}, t, {olMap: stateMap})
+ } else {
+ return t
+ }
+ })
+ case 'UNSELECT_MARKER':
+ return state.map((t, i) => {
+ if (i === action.index){
+ let stateMap = _.assign({}, t.olMap, {json: {}})
+ return _.assign({}, t, {olMap: stateMap})
+ } else {
+ return t
+ }
+ })
+ case 'CLOSE_MAP':
+ return state.map( (t, i) => {
+ let emptyMap = _.assign({}, t.olMap, {isOpened: false, json: {}})
+ return _.assign({}, t, {olMap: emptyMap})
+ })
+ default:
+ return state
+ }
+}
+
+export default stopPoints \ No newline at end of file
diff --git a/app/javascript/routes/show.js b/app/javascript/routes/show.js
new file mode 100644
index 000000000..e88469900
--- /dev/null
+++ b/app/javascript/routes/show.js
@@ -0,0 +1,102 @@
+const clone = require('../helpers/clone')
+let route = clone(window, "route", true)
+route = JSON.parse(decodeURIComponent(route))
+
+const geoColPts = []
+const geoColLns= []
+const geoColEdges = [
+ new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)]))
+ }),
+ new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[route.length - 1].longitude), parseFloat(route[route.length - 1].latitude)]))
+ })
+]
+route.forEach(function(stop, i){
+ if (i < route.length - 1){
+ geoColLns.push(new ol.Feature({
+ geometry: new ol.geom.LineString([
+ ol.proj.fromLonLat([parseFloat(route[i].longitude), parseFloat(route[i].latitude)]),
+ ol.proj.fromLonLat([parseFloat(route[i+1].longitude), parseFloat(route[i+1].latitude)])
+ ])
+ }))
+ }
+ geoColPts.push(new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(stop.longitude), parseFloat(stop.latitude)]))
+ })
+ )
+})
+var edgeStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 5,
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 2
+ }),
+ fill: new ol.style.Fill({
+ color: '#007fbb',
+ width: 2
+ })
+ }))
+})
+var defaultStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 4,
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 2
+ }),
+ fill: new ol.style.Fill({
+ color: '#ffffff',
+ width: 2
+ })
+ }))
+})
+var lineStyle = new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 3
+ })
+})
+
+var vectorPtsLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColPts
+ }),
+ style: defaultStyles,
+ zIndex: 2
+})
+var vectorEdgesLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColEdges
+ }),
+ style: edgeStyles,
+ zIndex: 3
+})
+var vectorLnsLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColLns
+ }),
+ style: [lineStyle],
+ zIndex: 1
+})
+
+var map = new ol.Map({
+ target: 'route_map',
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ }),
+ vectorPtsLayer,
+ vectorEdgesLayer,
+ vectorLnsLayer
+ ],
+ controls: [ new ol.control.ScaleLine(), new ol.control.Zoom(), new ol.control.ZoomSlider() ],
+ interactions: ol.interaction.defaults({
+ zoom: true
+ }),
+ view: new ol.View({
+ center: ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)]),
+ zoom: 13
+ })
+});
diff --git a/app/javascript/time_tables/actions/index.js b/app/javascript/time_tables/actions/index.js
new file mode 100644
index 000000000..5a02e8523
--- /dev/null
+++ b/app/javascript/time_tables/actions/index.js
@@ -0,0 +1,324 @@
+import _ from 'lodash'
+import clone from '../../helpers/clone'
+const I18n = clone(window, "I18n")
+
+const actions = {
+ weekDays: (index) => {
+ return _.range(1, 8).map(n => I18n.time_tables.edit.metas.days[n])
+ },
+ strToArrayDayTypes: (str) =>{
+ return actions.weekDays().map(day => str.indexOf(day) !== -1)
+ },
+ arrayToStrDayTypes: (dayTypes) => {
+ let newDayTypes = dayTypes.reduce((arr, dayActive, i) => {
+ if (dayActive) arr.push(actions.weekDays()[i])
+ return arr
+ }, [])
+
+ return newDayTypes.join(',')
+ },
+ fetchingApi: () =>({
+ type: 'FETCH_API'
+ }),
+ receiveErrors : (json) => ({
+ type: "RECEIVE_ERRORS",
+ json
+ }),
+ unavailableServer: () => ({
+ type: 'UNAVAILABLE_SERVER'
+ }),
+ receiveMonth: (json) => ({
+ type: 'RECEIVE_MONTH',
+ json
+ }),
+ receiveTimeTables: (json) => ({
+ type: 'RECEIVE_TIME_TABLES',
+ json
+ }),
+ goToPreviousPage : (dispatch, pagination) => ({
+ type: 'GO_TO_PREVIOUS_PAGE',
+ dispatch,
+ pagination,
+ nextPage : false
+ }),
+ goToNextPage : (dispatch, pagination) => ({
+ type: 'GO_TO_NEXT_PAGE',
+ dispatch,
+ pagination,
+ nextPage : true
+ }),
+ changePage : (dispatch, val) => ({
+ type: 'CHANGE_PAGE',
+ dispatch,
+ page: val
+ }),
+ updateDayTypes: (dayTypes) => ({
+ type: 'UPDATE_DAY_TYPES',
+ dayTypes
+ }),
+ updateCurrentMonthFromDaytypes: (dayTypes) => ({
+ type: 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES',
+ dayTypes
+ }),
+ updateComment: (comment) => ({
+ type: 'UPDATE_COMMENT',
+ comment
+ }),
+ updateColor: (color) => ({
+ type: 'UPDATE_COLOR',
+ color
+ }),
+ select2Tags: (selectedTag) => ({
+ type: 'UPDATE_SELECT_TAG',
+ selectedItem: {
+ id: selectedTag.id,
+ name: selectedTag.name
+ }
+ }),
+ unselect2Tags: (selectedTag) => ({
+ type: 'UPDATE_UNSELECT_TAG',
+ selectedItem: {
+ id: selectedTag.id,
+ name: selectedTag.name
+ }
+ }),
+ deletePeriod: (index, dayTypes) => ({
+ type: 'DELETE_PERIOD',
+ index,
+ dayTypes
+ }),
+ openAddPeriodForm: () => ({
+ type: 'OPEN_ADD_PERIOD_FORM'
+ }),
+ openEditPeriodForm: (period, index) => ({
+ type: 'OPEN_EDIT_PERIOD_FORM',
+ period,
+ index
+ }),
+ closePeriodForm: () => ({
+ type: 'CLOSE_PERIOD_FORM'
+ }),
+ resetModalErrors: () => ({
+ type: 'RESET_MODAL_ERRORS'
+ }),
+ updatePeriodForm: (val, group, selectType) => ({
+ type: 'UPDATE_PERIOD_FORM',
+ val,
+ group,
+ selectType
+ }),
+ validatePeriodForm: (modalProps, timeTablePeriods, metas, timetableInDates, error) => ({
+ type: 'VALIDATE_PERIOD_FORM',
+ modalProps,
+ timeTablePeriods,
+ metas,
+ timetableInDates,
+ error
+ }),
+ addIncludedDate: (index, dayTypes, date) => ({
+ type: 'ADD_INCLUDED_DATE',
+ index,
+ dayTypes,
+ date
+ }),
+ removeIncludedDate: (index, dayTypes, date) => ({
+ type: 'REMOVE_INCLUDED_DATE',
+ index,
+ dayTypes,
+ date
+ }),
+ addExcludedDate: (index, dayTypes, date) => ({
+ type: 'ADD_EXCLUDED_DATE',
+ index,
+ dayTypes,
+ date
+ }),
+ removeExcludedDate: (index, dayTypes, date) => ({
+ type: 'REMOVE_EXCLUDED_DATE',
+ index,
+ dayTypes,
+ date
+ }),
+ openConfirmModal : (callback) => ({
+ type : 'OPEN_CONFIRM_MODAL',
+ callback
+ }),
+ showErrorModal: (error) => ({
+ type: 'OPEN_ERROR_MODAL',
+ error
+ }),
+ closeModal : () => ({
+ type : 'CLOSE_MODAL'
+ }),
+ monthName(strDate) {
+ let monthList = _.range(1,13).map(n => I18n.calendars.months[n])
+ let date = new Date(strDate)
+ return monthList[date.getMonth()]
+ },
+ getHumanDate(strDate, mLimit) {
+ let origin = strDate.split('-')
+ let D = origin[2]
+ let M = actions.monthName(strDate).toLowerCase()
+ let Y = origin[0]
+
+ if(mLimit && M.length > mLimit) {
+ M = M.substr(0, mLimit) + '.'
+ }
+
+ return (D + ' ' + M + ' ' + Y)
+ },
+ getLocaleDate(strDate) {
+ let date = new Date(strDate)
+ return date.toLocaleDateString()
+ },
+ updateSynthesis: ({current_month, time_table_dates: dates, time_table_periods: periods}) => {
+ let newPeriods = _.reject(periods, 'deleted')
+ let improvedCM = current_month.map((d, i) => {
+ let isInPeriod = actions.isInPeriod(newPeriods, d.date)
+ let isIncluded = _.some(dates, {'date': d.date, 'in_out': true})
+
+ return _.assign({}, current_month[i], {
+ in_periods: isInPeriod,
+ include_date: isIncluded,
+ excluded_date: !isInPeriod ? false : current_month[i].excluded_date
+ })
+ })
+ return improvedCM
+ },
+ isInPeriod: (periods, date) => {
+ date = new Date(date)
+
+ for (let period of periods) {
+ let begin = new Date(period.period_start)
+ let end = new Date(period.period_end)
+ if (date >= begin && date <= end) return true
+ }
+
+ return false
+ },
+ checkConfirmModal: (event, callback, stateChanged, dispatch, metas, timetable) => {
+ if(stateChanged){
+ const error = actions.errorModalKey(timetable.time_table_periods, metas.day_types)
+ if(error){
+ return actions.showErrorModal(error)
+ }else{
+ return actions.openConfirmModal(callback)
+ }
+ }else{
+ dispatch(actions.fetchingApi())
+ return callback
+ }
+ },
+ formatDate: (props) => {
+ return props.year + '-' + props.month + '-' + props.day
+ },
+ checkErrorsInPeriods: (start, end, index, periods) => {
+ let error = ''
+ start = new Date(start)
+ end = new Date(end)
+
+ for (let i = 0; i < periods.length; i++) {
+ let period = periods[i]
+ if (index !== i && !period.deleted) {
+ if (new Date(period.period_start) <= end && new Date(period.period_end) >= start) {
+ error = I18n.time_tables.edit.error_submit.periods_overlaps
+ break
+ }
+ }
+ }
+ return error
+ },
+ checkErrorsInDates: (start, end, in_days) => {
+ let error = ''
+ start = new Date(start)
+ end = new Date(end)
+
+ for (let day of in_days) {
+ if (start <= new Date(day.date) && end >= new Date(day.date)) {
+ error = I18n.time_tables.edit.error_submit.dates_overlaps
+ break
+ }
+ }
+ return error
+ },
+ fetchTimeTables: (dispatch, nextPage) => {
+ let urlJSON = window.location.pathname.split('/', 5).join('/')
+ if(nextPage) {
+ urlJSON += "/month.json?date=" + nextPage
+ }else{
+ urlJSON += ".json"
+ }
+ 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 {
+ if(nextPage){
+ dispatch(actions.receiveMonth(json))
+ }else{
+ dispatch(actions.receiveTimeTables(json))
+ }
+ }
+ })
+ },
+ submitTimetable: (dispatch, timetable, metas, next) => {
+ dispatch(actions.fetchingApi())
+ let strDayTypes = actions.arrayToStrDayTypes(metas.day_types)
+ metas.day_types= strDayTypes
+ let sentState = _.assign({}, timetable, metas)
+ let urlJSON = window.location.pathname.split('/', 5).join('/')
+ let hasError = false
+ fetch(urlJSON + '.json', {
+ credentials: 'same-origin',
+ method: 'PATCH',
+ contentType: 'application/json; charset=utf-8',
+ Accept: 'application/json',
+ body: JSON.stringify(sentState),
+ 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 {
+ dispatch(actions.receiveTimeTables(json))
+ }
+ }
+ })
+ },
+ errorModalKey: (periods, dayTypes) => {
+ const withoutPeriodsWithDaysTypes = _.reject(periods, 'deleted').length == 0 && _.some(dayTypes) && "withoutPeriodsWithDaysTypes"
+ const withPeriodsWithoutDayTypes = _.reject(periods, 'deleted').length > 0 && _.every(dayTypes, dt => dt == false) && "withPeriodsWithoutDayTypes"
+
+ return (withoutPeriodsWithDaysTypes || withPeriodsWithoutDayTypes) && (withoutPeriodsWithDaysTypes ? "withoutPeriodsWithDaysTypes" : "withPeriodsWithoutDayTypes")
+
+ },
+ errorModalMessage: (errorKey) => {
+ switch (errorKey) {
+ case "withoutPeriodsWithDaysTypes":
+ return I18n.time_tables.edit.error_modal.withoutPeriodsWithDaysTypes
+ case "withPeriodsWithoutDayTypes":
+ return I18n.time_tables.edit.error_modal.withPeriodsWithoutDayTypes
+ default:
+ return errorKey
+
+ }
+ }
+}
+
+export default actions \ No newline at end of file
diff --git a/app/javascript/time_tables/components/ConfirmModal.js b/app/javascript/time_tables/components/ConfirmModal.js
new file mode 100644
index 000000000..d89170ee7
--- /dev/null
+++ b/app/javascript/time_tables/components/ConfirmModal.js
@@ -0,0 +1,50 @@
+import React, { PropTypes } from 'react'
+
+export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCancel, timetable, metas}, {I18n}) {
+ return (
+ <div className={'modal fade ' + ((modal.type == 'confirm') ? 'in' : '')} id='ConfirmModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>{I18n.time_tables.edit.confirm_modal.title}</h4>
+ </div>
+ <div className='modal-body'>
+ <div className='mt-md mb-md'>
+ <p>{I18n.time_tables.edit.confirm_modal.message}</p>
+ </div>
+ </div>
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={() => { onModalCancel(modal.confirmModal.callback) }}
+ >
+ {I18n.cancel}
+ </button>
+ <button
+ className='btn btn-primary'
+ data-dismiss='modal'
+ type='button'
+ onClick={() => { onModalAccept(modal.confirmModal.callback, timetable, metas) }}
+ >
+ {I18n.actions.submit}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ConfirmModal.propTypes = {
+ modal: PropTypes.object.isRequired,
+ onModalAccept: PropTypes.func.isRequired,
+ onModalCancel: PropTypes.func.isRequired
+}
+
+ConfirmModal.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/components/ErrorModal.js b/app/javascript/time_tables/components/ErrorModal.js
new file mode 100644
index 000000000..e810f49ab
--- /dev/null
+++ b/app/javascript/time_tables/components/ErrorModal.js
@@ -0,0 +1,42 @@
+import React, { PropTypes } from 'react'
+import actions from '../actions'
+
+export default function ErrorModal({dispatch, modal, onModalClose}, {I18n}) {
+ return (
+ <div className={'modal fade ' + ((modal.type == 'error') ? 'in' : '')} id='ErrorModal'>
+ <div className='modal-container'>
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <div className='modal-header'>
+ <h4 className='modal-title'>{I18n.time_tables.edit.error_modal.title}</h4>
+ </div>
+ <div className='modal-body'>
+ <div className='mt-md mb-md'>
+ <p>{actions.errorModalMessage(modal.modalProps.error)}</p>
+ </div>
+ </div>
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={() => { onModalClose() }}
+ >
+ {I18n.back}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ErrorModal.propTypes = {
+ modal: PropTypes.object.isRequired,
+ onModalClose: PropTypes.func.isRequired
+}
+
+ErrorModal.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/components/ExceptionsInDay.js b/app/javascript/time_tables/components/ExceptionsInDay.js
new file mode 100644
index 000000000..3335ee89d
--- /dev/null
+++ b/app/javascript/time_tables/components/ExceptionsInDay.js
@@ -0,0 +1,71 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class ExceptionsInDay extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ handleClick() {
+ const {index, day, metas: {day_types} } = this.props
+ if (day.in_periods && day_types[day.wday]) {
+ day.excluded_date ? this.props.onRemoveExcludedDate(index, day_types, day.date) : this.props.onAddExcludedDate(index, day_types, day.date)
+ } else {
+ day.include_date ? this.props.onRemoveIncludedDate(index, day_types, day.date) : this.props.onAddIncludedDate(index, day_types, day.date)
+ }
+ }
+
+ render() {
+ {/* display add or remove link, only if true in daytypes */}
+ {/* display add or remove link, according to context (presence in period, or not) */}
+ if(this.props.value.current_month[this.props.index].in_periods == true && this.props.blueDaytype == true) {
+ return (
+ <div className='td'>
+ <button
+ type='button'
+ className={'btn btn-circle' + (this.props.value.current_month[this.props.index].excluded_date ? ' active' : '')}
+ data-actiontype='remove'
+ onClick={(e) => {
+ $(e.currentTarget).toggleClass('active')
+ this.handleClick()
+ }}
+ >
+ <span className='fa fa-times'></span>
+ </button>
+ </div>
+ )
+ } else {
+ return (
+ <div className='td'>
+ <button
+ type='button'
+ className={'btn btn-circle' + (this.props.value.current_month[this.props.index].include_date ? ' active' : '')}
+ data-actiontype='add'
+ onClick={(e) => {
+ $(e.currentTarget).toggleClass('active')
+ this.handleClick()
+ }}
+ >
+ <span className='fa fa-plus'></span>
+ </button>
+ </div>
+ )
+ // } else if(this.props.value.current_month[this.props.index].in_periods == true && this.props.blueDaytype == false){
+ // return (
+ // <div className='td'></div>
+ // )
+ // } else{
+ // return false
+ // }
+ }
+ }
+}
+
+ExceptionsInDay.propTypes = {
+ value: PropTypes.object.isRequired,
+ metas: PropTypes.object.isRequired,
+ blueDaytype: PropTypes.bool.isRequired,
+ onExcludeDateFromPeriod: PropTypes.func.isRequired,
+ onIncludeDateInPeriod: PropTypes.func.isRequired,
+ index: PropTypes.number.isRequired
+}
diff --git a/app/javascript/time_tables/components/Metas.js b/app/javascript/time_tables/components/Metas.js
new file mode 100644
index 000000000..7098d2b82
--- /dev/null
+++ b/app/javascript/time_tables/components/Metas.js
@@ -0,0 +1,139 @@
+import React, { PropTypes } from 'react'
+import actions from '../actions'
+import TagsSelect2 from './TagsSelect2'
+
+export default function Metas({metas, onUpdateDayTypes, onUpdateComment, onUpdateColor, onSelect2Tags, onUnselect2Tags}, {I18n}) {
+ let colorList = ["", "#9B9B9B", "#FFA070", "#C67300", "#7F551B", "#41CCE3", "#09B09C", "#3655D7", "#6321A0", "#E796C6", "#DD2DAA"]
+ return (
+ <div className='form-horizontal'>
+ <div className="row">
+ <div className="col-lg-10 col-lg-offset-1">
+ {/* comment (name) */}
+ <div className="form-group">
+ <label htmlFor="" className="control-label col-sm-4 required">
+ {I18n.time_tables.edit.metas.name} <abbr title="">*</abbr>
+ </label>
+ <div className="col-sm-8">
+ <input
+ type='text'
+ className='form-control'
+ value={metas.comment}
+ required='required'
+ onChange={(e) => (onUpdateComment(e.currentTarget.value))}
+ />
+ </div>
+ </div>
+
+ {/* color */}
+ <div className="form-group">
+ <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.color}</label>
+ <div className="col-sm-8">
+ <div className="dropdown color_selector">
+ <button
+ type='button'
+ className="btn btn-default dropdown-toggle"
+ id='dpdwn_color'
+ data-toggle='dropdown'
+ aria-haspopup='true'
+ aria-expanded='true'
+ >
+ <span
+ className='fa fa-circle mr-xs'
+ style={{color: (metas.color == '') ? 'transparent' : metas.color}}
+ ></span>
+ <span className='caret'></span>
+ </button>
+
+ <div className="form-group dropdown-menu" aria-labelledby='dpdwn_color'>
+ {colorList.map((c, i) =>
+ <span
+ className="radio"
+ key={i}
+ onClick={() => {onUpdateColor(c)}}
+ >
+ <label htmlFor="">
+ <input
+ type='radio'
+ className='color_selector'
+ value={c}
+ />
+ <span
+ className='fa fa-circle'
+ style={{color: ((c == '') ? 'transparent' : c)}}
+ ></span>
+ </label>
+ </span>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* tags */}
+ <div className="form-group">
+ <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.tag_list}</label>
+ <div className="col-sm-8">
+ <TagsSelect2
+ initialTags={metas.initial_tags}
+ tags={metas.tags}
+ onSelect2Tags={(e) => onSelect2Tags(e)}
+ onUnselect2Tags={(e) => onUnselect2Tags(e)}
+ />
+ </div>
+ </div>
+
+ {/* calendar */}
+ <div className="form-group">
+ <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.calendar}</label>
+ <div className="col-sm-8">
+ <span>{metas.calendar ? metas.calendar.name : I18n.time_tables.edit.metas.no_calendar}</span>
+ </div>
+ </div>
+
+ {/* day_types */}
+ <div className="form-group">
+ <label htmlFor="" className="control-label col-sm-4">
+ {I18n.time_tables.edit.metas.day_types}
+ </label>
+ <div className="col-sm-8">
+ <div className="form-group labelled-checkbox-group">
+ {metas.day_types.map((day, i) =>
+ <div
+ className='lcbx-group-item'
+ data-wday={'day_' + i}
+ key={i}
+ >
+ <div className="checkbox">
+ <label>
+ <input
+ onChange={(e) => {onUpdateDayTypes(i, metas.day_types)}}
+ id={i}
+ type="checkbox"
+ checked={day ? 'checked' : ''}
+ />
+ <span className='lcbx-group-item-label'>{actions.weekDays()[i]}</span>
+ </label>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+Metas.propTypes = {
+ metas: PropTypes.object.isRequired,
+ onUpdateDayTypes: PropTypes.func.isRequired,
+ onUpdateColor: PropTypes.func.isRequired,
+ onUpdateColor: PropTypes.func.isRequired,
+ onSelect2Tags: PropTypes.func.isRequired,
+ onUnselect2Tags: PropTypes.func.isRequired
+}
+
+Metas.contextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/time_tables/components/Navigate.js b/app/javascript/time_tables/components/Navigate.js
new file mode 100644
index 000000000..6ae80bce0
--- /dev/null
+++ b/app/javascript/time_tables/components/Navigate.js
@@ -0,0 +1,88 @@
+import React, { PropTypes, Component } from 'react'
+import _ from 'lodash'
+import actions from '../actions'
+
+export default function Navigate({ dispatch, metas, timetable, pagination, status, filters}) {
+ if(status.isFetching == true) {
+ return false
+ }
+ if(status.fetchSuccess == true) {
+ let pageIndex = pagination.periode_range.indexOf(pagination.currentPage)
+ let firstPage = pageIndex == 0
+ let lastPage = pageIndex == pagination.periode_range.length - 1
+ return (
+ <div className="pagination pull-right">
+ <form className='form-inline' onSubmit={e => {e.preventDefault()}}>
+ {/* date selector */}
+ <div className="form-group">
+ <div className="dropdown month_selector" style={{display: 'inline-block'}}>
+ <div
+ className='btn btn-default dropdown-toggle'
+ id='date_selector'
+ data-toggle='dropdown'
+ aria-haspopup='true'
+ aria-expanded='true'
+ >
+ {pagination.currentPage ? (actions.monthName(pagination.currentPage) + ' ' + new Date(pagination.currentPage).getFullYear()) : ''}
+ <span className='caret'></span>
+ </div>
+ <ul
+ className='dropdown-menu'
+ aria-labelledby='date_selector'
+ >
+ {_.map(pagination.periode_range, (month, i) => (
+ <li key={i}>
+ <button
+ type='button'
+ value={month}
+ onClick={e => {
+ e.preventDefault()
+ dispatch(actions.checkConfirmModal(e, actions.changePage(dispatch, e.currentTarget.value), pagination.stateChanged, dispatch, metas, timetable))
+ }}
+ >
+ {actions.monthName(month) + ' ' + new Date(month).getFullYear()}
+ </button>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+
+ {/* prev/next */}
+ <div className="form-group">
+ <div className="page_links">
+ <button
+ onClick={e => {
+ e.preventDefault()
+ dispatch(actions.checkConfirmModal(e, actions.goToPreviousPage(dispatch, pagination), pagination.stateChanged, dispatch, metas, timetable))
+ }}
+ type='button'
+ data-target='#ConfirmModal'
+ className={(firstPage ? 'disabled ' : '') + 'previous_page'}
+ disabled={(firstPage ? 'disabled' : '')}
+ ></button>
+ <button
+ onClick={e => {
+ e.preventDefault()
+ dispatch(actions.checkConfirmModal(e, actions.goToNextPage(dispatch, pagination), pagination.stateChanged, dispatch, metas, timetable))
+ }}
+ type='button'
+ data-target='#ConfirmModal'
+ className={(lastPage ? 'disabled ' : '') + 'next_page'}
+ disabled={(lastPage ? 'disabled' : '')}
+ ></button>
+ </div>
+ </div>
+ </form>
+ </div>
+ )
+ } else {
+ return false
+ }
+}
+
+Navigate.propTypes = {
+ status: PropTypes.object.isRequired,
+ pagination: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/components/PeriodForm.js b/app/javascript/time_tables/components/PeriodForm.js
new file mode 100644
index 000000000..893a1fa6a
--- /dev/null
+++ b/app/javascript/time_tables/components/PeriodForm.js
@@ -0,0 +1,148 @@
+import React, { PropTypes } from 'react'
+import _ from 'lodash'
+let monthsArray = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
+
+const formatNumber = (val) => {
+ return ("0" + val).slice(-2)
+}
+
+const makeDaysOptions = (daySelected) => {
+ let arr = []
+ for(let i = 1; i < 32; i++) {
+ arr.push(<option value={formatNumber(i)} key={i}>{formatNumber(i)}</option>)
+ }
+ return arr
+}
+
+const makeMonthsOptions = (monthSelected) => {
+ let arr = []
+ for(let i = 1; i < 13; i++) {
+ arr.push(<option value={formatNumber(i)} key={i}>{monthsArray[i - 1]}</option>)
+ }
+ return arr
+}
+
+const makeYearsOptions = (yearSelected) => {
+ let arr = []
+ let startYear = new Date().getFullYear() - 3
+ for(let i = startYear; i <= startYear + 6; i++) {
+ arr.push(<option key={i}>{i}</option>)
+ }
+ return arr
+}
+
+export default function PeriodForm({modal, timetable, metas, onOpenAddPeriodForm, onClosePeriodForm, onUpdatePeriodForm, onValidatePeriodForm}, {I18n}) {
+ return (
+ <div className="container-fluid">
+ <div className="row">
+ <div className="col lg-6 col-lg-offset-3">
+ <div className='subform'>
+ {modal.modalProps.active &&
+ <div>
+ <div className="nested-head">
+ <div className="wrapper">
+ <div>
+ <div className="form-group">
+ <label htmlFor="" className="control-label required">
+ {I18n.time_tables.edit.period_form.begin}
+ <abbr title="requis">*</abbr>
+ </label>
+ </div>
+ </div>
+ <div>
+ <div className="form-group">
+ <label htmlFor="" className="control-label required">
+ {I18n.time_tables.edit.period_form.end}
+ <abbr title="requis">*</abbr>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="nested-fields">
+ <div className="wrapper">
+ <div>
+ <div className={'form-group date ' + (modal.modalProps.error ? ' has-error' : '')}>
+ <div className="form-inline">
+ <select value={formatNumber(modal.modalProps.begin.day)} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'day', modal.modalProps)} id="q_validity_period_begin_gteq_3i" className="date required form-control">
+ {makeDaysOptions(modal.modalProps.begin.day)}
+ </select>
+ <select value={formatNumber(modal.modalProps.begin.month)} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'month', modal.modalProps)} id="q_validity_period_begin_gteq_2i" className="date required form-control">
+ {makeMonthsOptions(modal.modalProps.begin.month)}
+ </select>
+ <select value={modal.modalProps.begin.year} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'year', modal.modalProps)} id="q_validity_period_begin_gteq_1i" className="date required form-control">
+ {makeYearsOptions(modal.modalProps.begin.year)}
+ </select>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div className={'form-group date ' + (modal.modalProps.error ? ' has-error' : '')}>
+ <div className="form-inline">
+ <select value={formatNumber(modal.modalProps.end.day)} onChange={(e) => onUpdatePeriodForm(e, 'end', 'day', modal.modalProps)} id="q_validity_period_end_gteq_3i" className="date required form-control">
+ {makeDaysOptions(modal.modalProps.end.day)}
+ </select>
+ <select value={formatNumber(modal.modalProps.end.month)} onChange={(e) => onUpdatePeriodForm(e, 'end', 'month', modal.modalProps)} id="q_validity_period_end_gteq_2i" className="date required form-control">
+ {makeMonthsOptions(modal.modalProps.end.month)}
+ </select>
+ <select value={modal.modalProps.end.year} onChange={(e) => onUpdatePeriodForm(e, 'end', 'year', modal.modalProps)} id="q_validity_period_end_gteq_1i" className="date required form-control">
+ {makeYearsOptions(modal.modalProps.end.year)}
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div className='links nested-linker'>
+ <span className='help-block small text-danger pull-left mt-xs ml-sm'>
+ {modal.modalProps.error}
+ </span>
+ <button
+ type='button'
+ className='btn btn-link'
+ onClick={onClosePeriodForm}
+ >
+ {I18n.cancel}
+ </button>
+ <button
+ type='button'
+ className='btn btn-outline-primary mr-sm'
+ onClick={() => onValidatePeriodForm(modal.modalProps, timetable.time_table_periods, metas, _.filter(timetable.time_table_dates, ['in_out', true]))}
+ >
+ {I18n.actions.submit}
+ </button>
+ </div>
+ </div>
+ }
+ {!modal.modalProps.active &&
+ <div className="text-right">
+ <button
+ type='button'
+ className='btn btn-outline-primary'
+ onClick={onOpenAddPeriodForm}
+ >
+ {I18n.time_tables.actions.add_period}
+ </button>
+ </div>
+ }
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+PeriodForm.propTypes = {
+ modal: PropTypes.object.isRequired,
+ metas: PropTypes.object.isRequired,
+ onOpenAddPeriodForm: PropTypes.func.isRequired,
+ onClosePeriodForm: PropTypes.func.isRequired,
+ onUpdatePeriodForm: PropTypes.func.isRequired,
+ onValidatePeriodForm: PropTypes.func.isRequired,
+ timetable: PropTypes.object.isRequired
+}
+
+PeriodForm.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/components/PeriodManager.js b/app/javascript/time_tables/components/PeriodManager.js
new file mode 100644
index 000000000..9922ce2c4
--- /dev/null
+++ b/app/javascript/time_tables/components/PeriodManager.js
@@ -0,0 +1,85 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+
+export default class PeriodManager extends Component {
+ constructor(props, context) {
+ super(props, context)
+ }
+
+ toEndPeriod(curr, end) {
+ let diff
+
+ let startCurrM = curr.split('-')[1]
+ let endPeriodM = end.split('-')[1]
+
+ let lastDayInM = new Date(curr.split('-')[2], startCurrM + 1, 0)
+ lastDayInM = lastDayInM.toJSON().substr(0, 10).split('-')[2]
+
+ if(startCurrM === endPeriodM) {
+ diff = (end.split('-')[2] - curr.split('-')[2])
+ } else {
+ diff = (lastDayInM - curr.split('-')[2])
+ }
+
+ return diff
+ }
+
+ render() {
+ return (
+ <div
+ className='period_manager'
+ id={this.props.value.id}
+ data-toendperiod={this.toEndPeriod(this.props.currentDate.toJSON().substr(0, 10), this.props.value.period_end)}
+ >
+ <p className='strong'>
+ {actions.getLocaleDate(this.props.value.period_start) + ' > ' + actions.getLocaleDate(this.props.value.period_end)}
+ </p>
+
+ <div className='dropdown'>
+ <div
+ className='btn dropdown-toggle'
+ id='period_actions'
+ data-toggle='dropdown'
+ aria-haspopup='true'
+ aria-expanded='true'
+ >
+ <span className='fa fa-cog'></span>
+ </div>
+ <ul
+ className='dropdown-menu'
+ aria-labelledby='date_selector'
+ >
+ <li>
+ <button
+ type='button'
+ onClick={() => this.props.onOpenEditPeriodForm(this.props.value, this.props.index)}
+ >
+ Modifier
+ </button>
+ </li>
+ <li className='delete-action'>
+ <button
+ type='button'
+ onClick={() => this.props.onDeletePeriod(this.props.index, this.props.metas.day_types)}
+ >
+ <span className='fa fa-trash'></span>
+ Supprimer
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ )
+ }
+}
+
+PeriodManager.propTypes = {
+ value: PropTypes.object.isRequired,
+ currentDate: PropTypes.object.isRequired,
+ onDeletePeriod: PropTypes.func.isRequired,
+ onOpenEditPeriodForm: PropTypes.func.isRequired
+}
+
+PeriodManager.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/components/PeriodsInDay.js b/app/javascript/time_tables/components/PeriodsInDay.js
new file mode 100644
index 000000000..888537579
--- /dev/null
+++ b/app/javascript/time_tables/components/PeriodsInDay.js
@@ -0,0 +1,75 @@
+import React, { PropTypes, Component } from 'react'
+import PeriodManager from './PeriodManager'
+
+export default class PeriodsInDay extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ isIn(date) {
+ let currentDate = date.getTime()
+ let cls = 'td'
+ let periods = this.props.value
+
+ periods.map((p, i) => {
+ if (!p.deleted){
+ let begin = new Date(p.period_start).getTime()
+ let end = new Date(p.period_end).getTime()
+
+ if(currentDate >= begin && currentDate <= end) {
+ if(currentDate == begin) {
+ cls += ' in_periods start_period'
+ } else if(currentDate == end) {
+ cls += ' in_periods end_period'
+ } else {
+ cls += ' in_periods'
+ }
+ }
+ }
+ })
+ return cls
+ }
+
+ render() {
+ return (
+ <div
+ className={this.isIn(this.props.currentDate) + (this.props.metas.day_types[this.props.day.wday] || !this.props.day.in_periods ? '' : ' out_from_daytypes')}
+ >
+ {this.props.value.map((p, i) => {
+ if(!p.deleted){
+ let begin = new Date(p.period_start).getTime()
+ let end = new Date(p.period_end).getTime()
+ let d = this.props.currentDate.getTime()
+
+ if(d >= begin && d <= end) {
+ if(d == begin || (this.props.currentDate.getUTCDate() == 1)) {
+ return (
+ <PeriodManager
+ key={i}
+ index={i}
+ value={p}
+ metas={this.props.metas}
+ currentDate={this.props.currentDate}
+ onDeletePeriod={this.props.onDeletePeriod}
+ onOpenEditPeriodForm={this.props.onOpenEditPeriodForm}
+ />
+ )
+ } else {
+ return false
+ }
+ }
+ }else{
+ return false
+ }
+ })}
+ </div>
+ )
+ }
+}
+
+PeriodsInDay.propTypes = {
+ value: PropTypes.array.isRequired,
+ currentDate: PropTypes.object.isRequired,
+ index: PropTypes.number.isRequired,
+ onDeletePeriod: PropTypes.func.isRequired
+}
diff --git a/app/javascript/time_tables/components/SaveTimetable.js b/app/javascript/time_tables/components/SaveTimetable.js
new file mode 100644
index 000000000..0dffc7936
--- /dev/null
+++ b/app/javascript/time_tables/components/SaveTimetable.js
@@ -0,0 +1,42 @@
+import React, { PropTypes, Component } from 'react'
+import _ from 'lodash'
+import actions from '../actions'
+
+export default class SaveTimetable extends Component{
+ constructor(props){
+ super(props)
+ }
+
+ render() {
+ const error = actions.errorModalKey(this.props.timetable.time_table_periods, this.props.metas.day_types)
+
+ return (
+ <div className='row mt-md'>
+ <div className='col-lg-12 text-right'>
+ <form className='time_tables formSubmitr ml-xs' onSubmit={e => {e.preventDefault()}}>
+ <button
+ className='btn btn-default'
+ type='button'
+ onClick={e => {
+ e.preventDefault()
+ if (error) {
+ this.props.onShowErrorModal(error)
+ } else {
+ actions.submitTimetable(this.props.getDispatch(), this.props.timetable, this.props.metas)
+ }
+ }}
+ >
+ Valider
+ </button>
+ </form>
+ </div>
+ </div>
+ )
+ }
+}
+
+SaveTimetable.propTypes = {
+ timetable: PropTypes.object.isRequired,
+ status: PropTypes.object.isRequired,
+ metas: PropTypes.object.isRequired
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/components/TagsSelect2.js b/app/javascript/time_tables/components/TagsSelect2.js
new file mode 100644
index 000000000..24f473f42
--- /dev/null
+++ b/app/javascript/time_tables/components/TagsSelect2.js
@@ -0,0 +1,77 @@
+import React, { PropTypes, Component } from 'react'
+import _ from 'lodash'
+import Select2 from 'react-select2-wrapper'
+
+// get JSON full path
+let origin = window.location.origin
+let path = window.location.pathname.split('/', 4).join('/')
+
+export default class TagsSelect2 extends Component {
+ constructor(props, context) {
+ super(props, context)
+ }
+
+ mapKeys(array){
+ return array.map((item) =>
+ _.mapKeys(item, (v, k) =>
+ ((k == 'name') ? 'text' : k)
+ )
+ )
+ }
+
+ render() {
+ return (
+ <Select2
+ value={(this.props.tags.length) ? _.map(this.props.tags, 'id') : undefined}
+ data={(this.props.initialTags.length) ? this.mapKeys(this.props.initialTags) : undefined}
+ onSelect={(e) => this.props.onSelect2Tags(e)}
+ onUnselect={(e) => setTimeout( () => this.props.onUnselect2Tags(e, 150))}
+ multiple={true}
+ ref='tags_id'
+ options={{
+ tags:true,
+ createTag: function(params) {
+ return {name: params.term, text: params.term, id: params.term}
+ },
+ allowClear: true,
+ theme: 'bootstrap',
+ width: '100%',
+ placeholder: this.context.I18n.time_tables.edit.select2.tag.placeholder,
+ ajax: {
+ url: origin + path + '/tags.json',
+ dataType: 'json',
+ delay: '500',
+ data: function(params) {
+ return {
+ tag: params.term,
+ };
+ },
+ processResults: function(data, params) {
+ let items = _.filter(data, ({name}) => name.includes(params.term) )
+ return {
+ results: items.map(
+ item => _.assign(
+ {},
+ item,
+ {text: item.name}
+ )
+ )
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 1,
+ templateResult: formatRepo
+ }}
+ />
+ )
+ }
+}
+
+const formatRepo = (props) => {
+ if(props.name) return props.name
+}
+
+TagsSelect2.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/components/TimeTableDay.js b/app/javascript/time_tables/components/TimeTableDay.js
new file mode 100644
index 000000000..165c7b848
--- /dev/null
+++ b/app/javascript/time_tables/components/TimeTableDay.js
@@ -0,0 +1,31 @@
+import React, { PropTypes, Component } from 'react'
+
+export default class TimeTableDay extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ return (
+ <span
+ className={'day' + (this.props.value.wday == 0 ? ' last_wday' : '')}
+ data-wday={'S' + this.props.value.wnumber}
+ >
+ <span className='dayname'>
+ {((this.props.value.day).charAt(0) == 'm') ? (this.props.value.day).substr(0, 2) : (this.props.value.day).charAt(0)}
+ </span>
+ <span
+ className={'daynumber' + (((this.props.value.in_periods && this.props.dayTypeActive && !this.props.value.excluded_date) || (this.props.value.include_date)) ? ' included' : '')}
+ >
+ {this.props.value.mday}
+ </span>
+ </span>
+ )
+ }
+}
+
+TimeTableDay.propTypes = {
+ value: PropTypes.object.isRequired,
+ index: PropTypes.number.isRequired,
+ dayTypeActive: PropTypes.bool.isRequired
+}
diff --git a/app/javascript/time_tables/components/Timetable.js b/app/javascript/time_tables/components/Timetable.js
new file mode 100644
index 000000000..df6e6016b
--- /dev/null
+++ b/app/javascript/time_tables/components/Timetable.js
@@ -0,0 +1,115 @@
+import React, { PropTypes, Component } from 'react'
+import actions from '../actions'
+import TimeTableDay from './TimeTableDay'
+import PeriodsInDay from './PeriodsInDay'
+import ExceptionsInDay from './ExceptionsInDay'
+
+
+export default class Timetable extends Component {
+ constructor(props, context){
+ super(props, context)
+ }
+
+ currentDate(mFirstday, day) {
+ let currentMonth = mFirstday.split('-')
+ let twodigitsDay = day < 10 ? ('0' + day) : day
+ let currentDate = new Date(currentMonth[0] + '-' + currentMonth[1] + '-' + twodigitsDay)
+
+ return currentDate
+ }
+
+ render() {
+ if(this.props.status.isFetching == true) {
+ return (
+ <div className="isLoading" style={{marginTop: 80, marginBottom: 80}}>
+ <div className="loader"></div>
+ </div>
+ )
+ } else {
+ return (
+ <div className="table table-2entries mb-sm">
+ <div className="t2e-head w20">
+ <div className="th">
+ <div className="strong">{this.context.I18n.time_tables.synthesis}</div>
+ </div>
+ <div className="td"><span>{this.context.I18n.time_tables.edit.day_types}</span></div>
+ <div className="td"><span>{this.context.I18n.time_tables.edit.periods}</span></div>
+ <div className="td"><span>{this.context.I18n.time_tables.edit.exceptions}</span></div>
+ </div>
+ <div className="t2e-item-list w80">
+ <div>
+ <div className="t2e-item">
+ <div className="th">
+ <div className="strong monthName">
+ {actions.monthName(this.props.timetable.current_periode_range)}
+ </div>
+
+ <div className='monthDays'>
+ {this.props.timetable.current_month.map((d, i) =>
+ <TimeTableDay
+ key={i}
+ index={i}
+ value={d}
+ dayTypeActive={this.props.metas.day_types[d.wday]}
+ />
+ )}
+ </div>
+ </div>
+
+ {this.props.timetable.current_month.map((d, i) =>
+ <div
+ key={i}
+ className={'td-group'+ (d.wday == 0 ? ' last_wday' : '')}
+ >
+ {/* day_types */}
+ <div className={"td" + (this.props.metas.day_types[d.wday] || !d.in_periods ? '' : ' out_from_daytypes') }></div>
+
+ {/* periods */}
+ <PeriodsInDay
+ day={d}
+ index={i}
+ value={this.props.timetable.time_table_periods}
+ currentDate={this.currentDate(this.props.timetable.current_periode_range, d.mday)}
+ onDeletePeriod={this.props.onDeletePeriod}
+ onOpenEditPeriodForm={this.props.onOpenEditPeriodForm}
+ metas={this.props.metas}
+ />
+
+ {/* exceptions */}
+ <ExceptionsInDay
+ day={d}
+ index={i}
+ value={this.props.timetable}
+ currentDate={d.date}
+ metas={this.props.metas}
+ blueDaytype={this.props.metas.day_types[d.wday]}
+ onAddIncludedDate={this.props.onAddIncludedDate}
+ onRemoveIncludedDate={this.props.onRemoveIncludedDate}
+ onAddExcludedDate={this.props.onAddExcludedDate}
+ onRemoveExcludedDate={this.props.onRemoveExcludedDate}
+ onExcludeDateFromPeriod={this.props.onExcludeDateFromPeriod}
+ onIncludeDateInPeriod={this.props.onIncludeDateInPeriod}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+ }
+}
+
+Timetable.propTypes = {
+ metas: PropTypes.object.isRequired,
+ timetable: PropTypes.object.isRequired,
+ status: PropTypes.object.isRequired,
+ onDeletePeriod: PropTypes.func.isRequired,
+ onExcludeDateFromPeriod: PropTypes.func.isRequired,
+ onIncludeDateInPeriod: PropTypes.func.isRequired
+}
+
+Timetable.contextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/time_tables/containers/App.js b/app/javascript/time_tables/containers/App.js
new file mode 100644
index 000000000..235dccb50
--- /dev/null
+++ b/app/javascript/time_tables/containers/App.js
@@ -0,0 +1,55 @@
+import React, { PropTypes, Component } from 'react'
+import { connect } from'react-redux'
+import actions from '../actions'
+import Metas from './Metas'
+import Timetable from './Timetable'
+import Navigate from './Navigate'
+import PeriodForm from './PeriodForm'
+import SaveTimetable from './SaveTimetable'
+import ConfirmModal from './ConfirmModal'
+import ErrorModal from './ErrorModal'
+import clone from '../../helpers/clone'
+const I18n = clone(window, "I18n", true)
+
+class App extends Component {
+ componentDidMount(){
+ this.props.onLoadFirstPage()
+ }
+
+ getChildContext() {
+ return { I18n }
+ }
+
+ render(){
+ return(
+ <div className='row'>
+ <div className="col-lg-8 col-lg-offset-2 col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
+ <Metas />
+ <Navigate />
+ <Timetable />
+ <PeriodForm />
+ <SaveTimetable />
+ <ConfirmModal />
+ <ErrorModal />
+ </div>
+ </div>
+ )
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onLoadFirstPage: () =>{
+ dispatch(actions.fetchingApi())
+ actions.fetchTimeTables(dispatch)
+ }
+ }
+}
+
+App.childContextTypes = {
+ I18n: PropTypes.object
+}
+
+const timeTableApp = connect(null, mapDispatchToProps)(App)
+
+export default timeTableApp
diff --git a/app/javascript/time_tables/containers/ConfirmModal.js b/app/javascript/time_tables/containers/ConfirmModal.js
new file mode 100644
index 000000000..f3742b038
--- /dev/null
+++ b/app/javascript/time_tables/containers/ConfirmModal.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import ConfirmModal from '../components/ConfirmModal'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ timetable: state.timetable,
+ metas: state.metas
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalAccept: (next, timetable, metas) =>{
+ dispatch(actions.fetchingApi())
+ actions.submitTimetable(dispatch, timetable, metas, 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/time_tables/containers/ErrorModal.js b/app/javascript/time_tables/containers/ErrorModal.js
new file mode 100644
index 000000000..37099073b
--- /dev/null
+++ b/app/javascript/time_tables/containers/ErrorModal.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import ErrorModal from '../components/ErrorModal'
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onModalClose: () =>{
+ dispatch(actions.closeModal())
+ dispatch(actions.resetModalErrors())
+ }
+ }
+}
+
+const ErrorModalContainer = connect(mapStateToProps, mapDispatchToProps)(ErrorModal)
+
+export default ErrorModalContainer
diff --git a/app/javascript/time_tables/containers/Metas.js b/app/javascript/time_tables/containers/Metas.js
new file mode 100644
index 000000000..ebccf556e
--- /dev/null
+++ b/app/javascript/time_tables/containers/Metas.js
@@ -0,0 +1,38 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import MetasComponent from '../components/Metas'
+
+const mapStateToProps = (state) => {
+ return {
+ metas: state.metas
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onUpdateDayTypes: (index, dayTypes) => {
+ let newDayTypes = dayTypes.slice(0)
+ newDayTypes[index] = !newDayTypes[index]
+ dispatch(actions.updateDayTypes(newDayTypes))
+ dispatch(actions.updateCurrentMonthFromDaytypes(newDayTypes))
+ },
+ onUpdateComment: (comment) => {
+ dispatch(actions.updateComment(comment))
+ },
+ onUpdateColor: (color) => {
+ dispatch(actions.updateColor(color))
+ },
+ onSelect2Tags: (e) => {
+ e.preventDefault()
+ dispatch(actions.select2Tags(e.params.data))
+ },
+ onUnselect2Tags: (e) => {
+ e.preventDefault()
+ dispatch(actions.unselect2Tags(e.params.data))
+ }
+ }
+}
+
+const Metas = connect(mapStateToProps, mapDispatchToProps)(MetasComponent)
+
+export default Metas
diff --git a/app/javascript/time_tables/containers/Navigate.js b/app/javascript/time_tables/containers/Navigate.js
new file mode 100644
index 000000000..8d163659c
--- /dev/null
+++ b/app/javascript/time_tables/containers/Navigate.js
@@ -0,0 +1,18 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import actions from '../actions'
+import NavigateComponent from '../components/Navigate'
+
+const mapStateToProps = (state) => {
+ return {
+ metas: state.metas,
+ timetable: state.timetable,
+ status: state.status,
+ pagination: state.pagination
+ }
+}
+
+
+const Navigate = connect(mapStateToProps)(NavigateComponent)
+
+export default Navigate
diff --git a/app/javascript/time_tables/containers/PeriodForm.js b/app/javascript/time_tables/containers/PeriodForm.js
new file mode 100644
index 000000000..49e79f348
--- /dev/null
+++ b/app/javascript/time_tables/containers/PeriodForm.js
@@ -0,0 +1,46 @@
+import { connect } from 'react-redux'
+import _ from 'lodash'
+import actions from '../actions'
+import PeriodFormComponent from '../components/PeriodForm'
+
+
+
+const mapStateToProps = (state) => {
+ return {
+ modal: state.modal,
+ timetable: state.timetable,
+ metas: state.metas,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onOpenAddPeriodForm: () => {
+ dispatch(actions.openAddPeriodForm())
+ },
+ onClosePeriodForm: () => {
+ dispatch(actions.closePeriodForm())
+ },
+ onUpdatePeriodForm: (e, group, selectType, modalProps) => {
+ dispatch(actions.updatePeriodForm(e.currentTarget.value, group, selectType))
+ let mProps = _.assign({}, modalProps)
+ mProps[group][selectType] = e.currentTarget.value
+ let val = window.correctDay([parseInt(mProps[group]['day']), parseInt(mProps[group]['month']), parseInt(mProps[group]['year'])])
+ val = (val < 10) ? '0' + String(val) : String(val)
+ dispatch(actions.updatePeriodForm(val, group, 'day'))
+ },
+ onValidatePeriodForm: (modalProps, timeTablePeriods, metas, timetableInDates) => {
+ let period_start = actions.formatDate(modalProps.begin)
+ let period_end = actions.formatDate(modalProps.end)
+ let error = ''
+ if (new Date(period_end) <= new Date(period_start)) error = 'La date de départ doit être antérieure à la date de fin'
+ if (error == '') error = actions.checkErrorsInPeriods(period_start, period_end, modalProps.index, timeTablePeriods)
+ if (error == '') error = actions.checkErrorsInDates(period_start, period_end, timetableInDates)
+ dispatch(actions.validatePeriodForm(modalProps, timeTablePeriods, metas, timetableInDates, error))
+ }
+ }
+}
+
+const PeriodForm = connect(mapStateToProps, mapDispatchToProps)(PeriodFormComponent)
+
+export default PeriodForm
diff --git a/app/javascript/time_tables/containers/SaveTimetable.js b/app/javascript/time_tables/containers/SaveTimetable.js
new file mode 100644
index 000000000..7574dc5cc
--- /dev/null
+++ b/app/javascript/time_tables/containers/SaveTimetable.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import SaveTimetableComponent from '../components/SaveTimetable'
+
+const mapStateToProps = (state) => {
+ return {
+ timetable: state.timetable,
+ metas: state.metas,
+ status: state.status
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onShowErrorModal: (errorKey) => {
+ dispatch(actions.showErrorModal(errorKey))
+ },
+ getDispatch: () => {
+ return dispatch
+ }
+ }
+}
+const SaveTimetable = connect(mapStateToProps, mapDispatchToProps)(SaveTimetableComponent)
+
+export default SaveTimetable
diff --git a/app/javascript/time_tables/containers/Timetable.js b/app/javascript/time_tables/containers/Timetable.js
new file mode 100644
index 000000000..e78e8840a
--- /dev/null
+++ b/app/javascript/time_tables/containers/Timetable.js
@@ -0,0 +1,44 @@
+import { connect } from 'react-redux'
+import actions from '../actions'
+import TimetableComponent from '../components/Timetable'
+
+const mapStateToProps = (state) => {
+ return {
+ metas: state.metas,
+ timetable: state.timetable,
+ status: state.status
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onDeletePeriod: (index, dayTypes) =>{
+ dispatch(actions.deletePeriod(index, dayTypes))
+ },
+ onAddIncludedDate: (index, dayTypes, date) => {
+ dispatch(actions.addIncludedDate(index, dayTypes, date))
+ },
+ onRemoveIncludedDate: (index, dayTypes, date) => {
+ dispatch(actions.removeIncludedDate(index, dayTypes, date))
+ },
+ onAddExcludedDate: (index, dayTypes, date) => {
+ dispatch(actions.addExcludedDate(index, dayTypes, date))
+ },
+ onRemoveExcludedDate: (index, dayTypes, date) => {
+ dispatch(actions.removeExcludedDate(index, dayTypes, date))
+ },
+ onExcludeDateFromPeriod: (index, dayTypes, date) => {
+ dispatch(actions.excludeDateFromPeriod(index, dayTypes, date))
+ },
+ onIncludeDateInPeriod: (index, dayTypes, date) => {
+ dispatch(actions.includeDateInPeriod(index, dayTypes, date))
+ },
+ onOpenEditPeriodForm: (period, index) => {
+ dispatch(actions.openEditPeriodForm(period, index))
+ }
+ }
+}
+
+const Timetable = connect(mapStateToProps, mapDispatchToProps)(TimetableComponent)
+
+export default Timetable
diff --git a/app/javascript/time_tables/reducers/index.js b/app/javascript/time_tables/reducers/index.js
new file mode 100644
index 000000000..aed9035b5
--- /dev/null
+++ b/app/javascript/time_tables/reducers/index.js
@@ -0,0 +1,16 @@
+import { combineReducers } from 'redux'
+import status from './status'
+import pagination from './pagination'
+import modal from './modal'
+import timetable from './timetable'
+import metas from './metas'
+
+const timeTablesApp = combineReducers({
+ timetable,
+ metas,
+ status,
+ pagination,
+ modal
+})
+
+export default timeTablesApp
diff --git a/app/javascript/time_tables/reducers/metas.js b/app/javascript/time_tables/reducers/metas.js
new file mode 100644
index 000000000..548798012
--- /dev/null
+++ b/app/javascript/time_tables/reducers/metas.js
@@ -0,0 +1,40 @@
+import _ from 'lodash'
+import actions from '../actions'
+
+export default function metas(state = {}, action) {
+ switch (action.type) {
+ case 'RECEIVE_TIME_TABLES':
+ return _.assign({}, state, {
+ comment: action.json.comment,
+ day_types: actions.strToArrayDayTypes(action.json.day_types),
+ tags: action.json.tags,
+ initial_tags: action.json.tags,
+ color: action.json.color,
+ calendar: action.json.calendar ? action.json.calendar : null
+ })
+ case 'RECEIVE_MONTH':
+ let dt = (typeof state.day_types === 'string') ? actions.strToArrayDayTypes(state.day_types) : state.day_types
+ return _.assign({}, state, {day_types: dt})
+ case 'ADD_INCLUDED_DATE':
+ case 'REMOVE_INCLUDED_DATE':
+ case 'ADD_EXCLUDED_DATE':
+ case 'REMOVE_EXCLUDED_DATE':
+ case 'DELETE_PERIOD':
+ case 'VALIDATE_PERIOD_FORM':
+ return _.assign({}, state, {calendar: null})
+ case 'UPDATE_DAY_TYPES':
+ return _.assign({}, state, {day_types: action.dayTypes, calendar : null})
+ case 'UPDATE_COMMENT':
+ return _.assign({}, state, {comment: action.comment})
+ case 'UPDATE_COLOR':
+ return _.assign({}, state, {color: action.color})
+ case 'UPDATE_SELECT_TAG':
+ let tags = [...state.tags]
+ tags.push(action.selectedItem)
+ return _.assign({}, state, {tags: tags})
+ case 'UPDATE_UNSELECT_TAG':
+ return _.assign({}, state, {tags: _.filter(state.tags, (t) => (t.id != action.selectedItem.id))})
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/reducers/modal.js b/app/javascript/time_tables/reducers/modal.js
new file mode 100644
index 000000000..a530b2717
--- /dev/null
+++ b/app/javascript/time_tables/reducers/modal.js
@@ -0,0 +1,64 @@
+import _ from 'lodash'
+import actions from '../actions'
+
+let newModalProps = {}
+let emptyDate = {
+ day: '01',
+ month: '01',
+ year: String(new Date().getFullYear())
+}
+let period_start = '', period_end = ''
+
+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 'OPEN_ERROR_MODAL':
+ $('#ErrorModal').modal('show')
+ newModalProps = _.assign({}, state.modalProps, {error: action.error})
+ return _.assign({}, state, {type: 'error'}, {modalProps: newModalProps})
+ case 'RESET_MODAL_ERRORS':
+ newModalProps = _.assign({}, state.modalProps, {error: ''})
+ return _.assign({}, state, {type: ''}, {modalProps: newModalProps})
+ case 'CLOSE_PERIOD_FORM':
+ newModalProps = _.assign({}, state.modalProps, {active: false, error: ""})
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'OPEN_EDIT_PERIOD_FORM':
+ period_start = action.period.period_start.split('-')
+ period_end = action.period.period_end.split('-')
+ newModalProps = JSON.parse(JSON.stringify(state.modalProps))
+
+ newModalProps.begin.year = period_start[0]
+ newModalProps.begin.month = period_start[1]
+ newModalProps.begin.day = period_start[2]
+
+ newModalProps.end.year = period_end[0]
+ newModalProps.end.month = period_end[1]
+ newModalProps.end.day = period_end[2]
+
+ newModalProps.active = true
+ newModalProps.index = action.index
+ newModalProps.error = ''
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'OPEN_ADD_PERIOD_FORM':
+ newModalProps = _.assign({}, state.modalProps, {active: true, begin: emptyDate, end: emptyDate, index: false, error: ''})
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'UPDATE_PERIOD_FORM':
+ newModalProps = JSON.parse(JSON.stringify(state.modalProps))
+ newModalProps[action.group][action.selectType] = action.val
+ return _.assign({}, state, {modalProps: newModalProps})
+ case 'VALIDATE_PERIOD_FORM':
+ newModalProps = JSON.parse(JSON.stringify(state.modalProps))
+ newModalProps.error = action.error
+ newModalProps.active = (newModalProps.error == '') ? false : true
+ return _.assign({}, state, {modalProps: newModalProps})
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/reducers/pagination.js b/app/javascript/time_tables/reducers/pagination.js
new file mode 100644
index 000000000..e9ca9e1ec
--- /dev/null
+++ b/app/javascript/time_tables/reducers/pagination.js
@@ -0,0 +1,44 @@
+import _ from 'lodash'
+
+export default function pagination(state = {}, action) {
+ switch (action.type) {
+ case 'RECEIVE_TIME_TABLES':
+ return _.assign({}, state, {
+ currentPage: action.json.current_periode_range,
+ periode_range: action.json.periode_range,
+ stateChanged: false
+ })
+ case 'RECEIVE_MONTH':
+ case 'RECEIVE_ERRORS':
+ return _.assign({}, state, {stateChanged: false})
+ case 'GO_TO_PREVIOUS_PAGE':
+ case 'GO_TO_NEXT_PAGE':
+ let nextPage = action.nextPage ? 1 : -1
+ let newPage = action.pagination.periode_range[action.pagination.periode_range.indexOf(action.pagination.currentPage) + nextPage]
+ toggleOnConfirmModal()
+ return _.assign({}, state, {currentPage : newPage, stateChanged: false})
+ case 'CHANGE_PAGE':
+ toggleOnConfirmModal()
+ return _.assign({}, state, {currentPage : action.page, stateChanged: false})
+ case 'ADD_INCLUDED_DATE':
+ case 'REMOVE_INCLUDED_DATE':
+ case 'ADD_EXCLUDED_DATE':
+ case 'REMOVE_EXCLUDED_DATE':
+ case 'DELETE_PERIOD':
+ case 'VALIDATE_PERIOD_FORM':
+ case 'UPDATE_COMMENT':
+ case 'UPDATE_COLOR':
+ case 'UPDATE_DAY_TYPES':
+ case 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES':
+ toggleOnConfirmModal('modal')
+ return _.assign({}, state, {stateChanged: true})
+ default:
+ return state
+ }
+}
+
+const toggleOnConfirmModal = (arg = '') =>{
+ $('.confirm').each(function(){
+ $(this).data('toggle','')
+ })
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/reducers/status.js b/app/javascript/time_tables/reducers/status.js
new file mode 100644
index 000000000..8d93bc2e2
--- /dev/null
+++ b/app/javascript/time_tables/reducers/status.js
@@ -0,0 +1,15 @@
+import _ from 'lodash'
+
+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_TIME_TABLES':
+ case 'RECEIVE_MONTH':
+ return _.assign({}, state, {fetchSuccess: true, isFetching: false})
+ default:
+ return state
+ }
+} \ No newline at end of file
diff --git a/app/javascript/time_tables/reducers/timetable.js b/app/javascript/time_tables/reducers/timetable.js
new file mode 100644
index 000000000..274153a69
--- /dev/null
+++ b/app/javascript/time_tables/reducers/timetable.js
@@ -0,0 +1,117 @@
+import _ from 'lodash'
+import actions from '../actions'
+let newState, newPeriods, newDates, newCM
+
+export default function timetable(state = {}, action) {
+ switch (action.type) {
+ case 'RECEIVE_TIME_TABLES':
+ let fetchedState = _.assign({}, state, {
+ current_month: action.json.current_month,
+ current_periode_range: action.json.current_periode_range,
+ periode_range: action.json.periode_range,
+ time_table_periods: action.json.time_table_periods,
+ time_table_dates: _.sortBy(action.json.time_table_dates, ['date'])
+ })
+ return _.assign({}, fetchedState, {current_month: actions.updateSynthesis(fetchedState)})
+ case 'RECEIVE_MONTH':
+ newState = _.assign({}, state, {
+ current_month: action.json.days
+ })
+ return _.assign({}, newState, {current_month: actions.updateSynthesis(newState)})
+ case 'GO_TO_PREVIOUS_PAGE':
+ case 'GO_TO_NEXT_PAGE':
+ let nextPage = action.nextPage ? 1 : -1
+ let newPage = action.pagination.periode_range[action.pagination.periode_range.indexOf(action.pagination.currentPage) + nextPage]
+ $('#ConfirmModal').modal('hide')
+ actions.fetchTimeTables(action.dispatch, newPage)
+ return _.assign({}, state, {current_periode_range: newPage})
+ case 'CHANGE_PAGE':
+ $('#ConfirmModal').modal('hide')
+ actions.fetchTimeTables(action.dispatch, action.page)
+ return _.assign({}, state, {current_periode_range: action.page})
+ case 'DELETE_PERIOD':
+ newPeriods = state.time_table_periods.map((period, i) =>{
+ if(i == action.index){
+ period.deleted = true
+ }
+ return period
+ })
+ let deletedPeriod = Array.of(state.time_table_periods[action.index])
+ newDates = _.reject(state.time_table_dates, d => actions.isInPeriod(deletedPeriod, d.date) && !d.in_out)
+ newState = _.assign({}, state, {time_table_periods : newPeriods, time_table_dates: newDates})
+ return _.assign({}, newState, { current_month: actions.updateSynthesis(newState)})
+ case 'ADD_INCLUDED_DATE':
+ newDates = state.time_table_dates.concat({date: action.date, in_out: true})
+ newCM = state.current_month.map((d, i) => {
+ if (i == action.index) d.include_date = true
+ return d
+ })
+ return _.assign({}, state, {current_month: newCM, time_table_dates: newDates})
+ case 'REMOVE_INCLUDED_DATE':
+ newDates = _.reject(state.time_table_dates, ['date', action.date])
+ newCM = state.current_month.map((d, i) => {
+ if (i == action.index) d.include_date = false
+ return d
+ })
+ return _.assign({}, state, {current_month: newCM, time_table_dates: newDates})
+ case 'ADD_EXCLUDED_DATE':
+ newDates = state.time_table_dates.concat({date: action.date, in_out: false})
+ newCM = state.current_month.map((d, i) => {
+ if (i == action.index) d.excluded_date = true
+ return d
+ })
+ return _.assign({}, state, {current_month: newCM, time_table_dates: newDates})
+ case 'REMOVE_EXCLUDED_DATE':
+ newDates = _.reject(state.time_table_dates, ['date', action.date])
+ newCM = state.current_month.map((d, i) => {
+ if (i == action.index) d.excluded_date = false
+ return d
+ })
+ return _.assign({}, state, {current_month: newCM, time_table_dates: newDates})
+ case 'UPDATE_DAY_TYPES':
+ // We get the week days of the activated day types to reject the out_dates that that are out of newDayTypes
+ let weekDays = _.reduce(action.dayTypes, (array, dt, i) => {
+ if (dt) array.push(i)
+ return array
+ }, [])
+
+ newDates = _.reject(state.time_table_dates, (d) => {
+ let weekDay = new Date(d.date).getDay()
+
+ if (d.in_out) {
+ return actions.isInPeriod(state.time_table_periods, d.date) && weekDays.includes(weekDay)
+ } else {
+ return !weekDays.includes(weekDay)
+ }
+ })
+ return _.assign({}, state, {time_table_dates: newDates})
+ case 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES':
+ return _.assign({}, state, {current_month: actions.updateSynthesis(state)})
+ case 'VALIDATE_PERIOD_FORM':
+ if (action.error != '') return state
+
+ let period_start = actions.formatDate(action.modalProps.begin)
+ let period_end = actions.formatDate(action.modalProps.end)
+
+ let newPeriods = JSON.parse(JSON.stringify(action.timeTablePeriods))
+
+ if (action.modalProps.index !== false){
+ let updatedPeriod = newPeriods[action.modalProps.index]
+ updatedPeriod.period_start = period_start
+ updatedPeriod.period_end = period_end
+ newDates = _.reject(state.time_table_dates, d => actions.isInPeriod(newPeriods, d.date) && !d.in_out)
+ }else{
+ let newPeriod = {
+ period_start: period_start,
+ period_end: period_end
+ }
+ newPeriods.push(newPeriod)
+ }
+
+ newDates = newDates || state.time_table_dates
+ newState =_.assign({}, state, {time_table_periods: newPeriods, time_table_dates: newDates})
+ return _.assign({}, newState, {current_month: actions.updateSynthesis(newState)})
+ default:
+ return state
+ }
+} \ No newline at end of file