Browse Source

Frequency, stop merger

dev
sipp11 6 years ago
parent
commit
bcb837c19c
  1. 67
      src/actions/frequency.js
  2. 9
      src/actions/index.js
  3. 17
      src/actions/stop.js
  4. 126
      src/components/FrequencyForm.js
  5. 73
      src/components/FrequencyOne.js
  6. 61
      src/components/StopForm.js
  7. 24
      src/components/StopTimeForm.js
  8. 50
      src/components/TripForm.js
  9. 7
      src/constants/ActionTypes.js
  10. 4
      src/constants/choices.js
  11. 5
      src/container/Geo.js
  12. 88
      src/reducers/frequency.js
  13. 3
      src/reducers/geo.js
  14. 2
      src/reducers/index.js
  15. 10
      src/reducers/stop.js
  16. 20
      src/utils/index.js

67
src/actions/frequency.js

@ -0,0 +1,67 @@
import { RSAA } from 'redux-api-middleware'
import * as types from '../constants/ActionTypes'
import { RSAAHeaders } from '../utils/ApiClient'
import { API_URL } from '../constants/Api'
export const getFrequency = (query) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.frequency.fetching || state.frequency.query === query,
types: [
{
type: types.FREQUENCY_REQUEST,
meta: { query: query },
},
types.FREQUENCY_SUCCESS,
types.FREQUENCY_FAILURE,
]
}
})
export const updateFrequency = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/${id}/`,
body: JSON.stringify(body),
method: 'PATCH',
headers: RSAAHeaders,
types: [
types.FREQUENCY_REQUEST,
types.FREQUENCY_UPDATE,
types.FREQUENCY_FAILURE,
]
}
})
export const createFrequency = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.FREQUENCY_REQUEST,
types.FREQUENCY_CREATE,
types.FREQUENCY_FAILURE,
]
}
})
export const deleteFrequency = (id) => ({
[RSAA]: {
endpoint: `${API_URL}/frequency/${id}/`,
method: 'DELETE',
headers: RSAAHeaders,
types: [
types.FREQUENCY_REQUEST,
{
type: types.FREQUENCY_DELETE,
meta: { id }
},
types.FREQUENCY_FAILURE,
]
}
})

9
src/actions/index.js

@ -97,11 +97,16 @@ export const polygonReset = () => {
}
}
export const lastCenterUpdate = (lat, lon) => {
export const lastCenterUpdate = (lat, lon, zoom) => {
// this only update current map center from map movement alone
let payload = {
lastCenter: [lat, lon]
}
if (zoom !== undefined)
payload['zoom'] = zoom
return {
type: types.GEO_LASTCENTER_UPDATE,
payload: [lat, lon],
payload,
}
}

17
src/actions/stop.js

@ -33,6 +33,23 @@ export const updateStop = (id, body) => ({
}
})
export const mergeStop = (id, body) => ({
[RSAA]: {
endpoint: `${API_URL}/stop/${id}/merge/`,
body: JSON.stringify(body),
method: 'POST',
headers: RSAAHeaders,
types: [
types.STOP_REQUEST,
{
type: types.STOP_UPDATE,
meta: { id, opt: 'merge', mergeWith: body }
},
types.STOP_FAILURE,
]
}
})
export const createStop = (body) => ({
[RSAA]: {
endpoint: `${API_URL}/stop/`,

126
src/components/FrequencyForm.js

@ -0,0 +1,126 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import Input from './parts/Input'
import Select from './parts/Select'
import {
updateFrequency, createFrequency, deleteFrequency
} from '../actions/frequency'
import store from '../store'
import {
ExactTimeChoices
} from '../constants/choices'
import { getItemFromList } from '../utils'
const StyleBox = styled.div`
padding: 5px;
background: white;
margin-bottom: 1rem;
`
class FrequencyForm extends Component {
cancel = null
state = {
editMode: false,
trip: null,
id: null,
}
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleDelete = this.handleDelete.bind(this)
}
handleChange(evt) {
let updated = {}
updated[evt.target.name] = evt.target.value
this.setState(updated)
}
handleSubmit() {
const { id } = this.state
let body = {...this.state }
delete body.id
if (id !== null) {
store.dispatch(updateFrequency(id, body))
} else {
store.dispatch(createFrequency(body))
}
this.props.toggleEditMode()
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteFrequency(id))
this.props.toggleEditMode()
}
static getDerivedStateFromProps(props, state) {
if (props.item && props.item.id !== null && state.id === null) {
return props.item
} else if (props.trip !== undefined) {
return { trip: props.trip }
}
return null
}
render() {
const item = this.state
return (
<StyleBox>
<Input
label="Start"
type="text"
fieldName="start_time"
value={item.start_time || ''}
handleChange={this.handleChange} />
<Input
label="End"
type="text"
fieldName="end_time"
value={item.end_time || ''}
handleChange={this.handleChange} />
<Input
label="Headway secs"
type="text"
fieldName="headway_secs"
value={item.headway_secs || ''}
handleChange={this.handleChange} />
<Select
label="Exact Times"
type="text"
fieldName="exact_times"
value={getItemFromList(item.exact_times, ExactTimeChoices, '0')}
handleChange={this.handleChange}
choices={ExactTimeChoices} />
<div className="field is-grouped">
<div className="control">
<button className="button is-link"
onClick={this.handleSubmit}
disabled={false}>
Save</button>
</div>
{item.id !== null && <div className="control">
<button className="button is-danger"
onClick={this.handleDelete}
disabled={false}>
DELETE</button>
</div>}
{this.props.toggleEditMode &&
<div className="control">
<a onClick={() => this.props.toggleEditMode()} className="button is-text">Cancel</a>
</div>}
</div>
</StyleBox>
)
}
}
export default FrequencyForm

73
src/components/FrequencyOne.js

@ -0,0 +1,73 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import FrequencyForm from './FrequencyForm'
import { ExactTimeChoices } from '../constants/choices'
import { getItemFromList } from '../utils'
const StyledRow = styled.div`
padding-top: 5px;
padding-bottom: 5px;
background: white;
margin-bottom: 1rem;
`
class FrequencyOne extends Component {
state = {
editMode: false
}
constructor(props) {
super(props)
this.toggleEditMode = this.toggleEditMode.bind(this)
}
toggleEditMode() {
this.setState({editMode: !this.state.editMode})
}
renderReadOnly = (item) => (
<StyledRow className="level panel" key={`st-item-${item.id}`}>
<div className="level-item has-text-centered">
<div>
<p className="heading">
<a onClick={() => this.toggleEditMode()}> EDIT</a>
&nbsp;Start Time</p>
<p className="title">{item.start_time}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">End Time</p>
<p className="title">{item.end_time}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Headway secs</p>
<p className="title">{item.headway_secs}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Exact Times</p>
<p className="title">{getItemFromList(item.exact_times, ExactTimeChoices).value}</p>
</div>
</div>
</StyledRow>
)
render() {
const { item, tripId } = this.props
if (this.state.editMode)
return <FrequencyForm
item={item}
trip={tripId}
toggleEditMode={this.toggleEditMode} />
return this.renderReadOnly(item)
}
}
export default FrequencyOne

61
src/components/StopForm.js

@ -2,24 +2,36 @@ import React, { Component } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import AsyncSelect from 'react-select/lib/Async'
import { components } from 'react-select'
import Input from './parts/Input'
import Select from './parts/Select'
import {
mapCenterUpdate, draggableMarkerEnable, draggableMarkerDisable
} from '../actions'
import { updateStop, createStop, deleteStop, getStop } from '../actions/stop'
import { updateStop, createStop, deleteStop, getStop, mergeStop } from '../actions/stop'
import store from '../store'
import {
StopLocationTypes, StopWheelChairInfo
} from '../constants/choices'
import { getItemFromList } from '../utils'
import { getItemFromList, getStopsAsyncSelect } from '../utils'
const StyledStopForm = styled.div`
padding: 1rem;
background: #fafafa;
`
const Option = (props) => {
const { stop_id, name, stop_desc } = props.data
return (
<components.Option {...props}>
<code>{stop_id}</code> {name}
{stop_desc.length > 0 && <small><br />{stop_desc}</small>}
</components.Option>
)
}
// TODO: need to deal with shapes
class StopForm extends Component {
@ -35,6 +47,7 @@ class StopForm extends Component {
stop_timezone: 'Asia/Bangkok',
wheelchair_boarding: '0',
latlon: [],
mergeWith: null,
// parent_station: null,
}
@ -42,6 +55,7 @@ class StopForm extends Component {
super()
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleMerge = this.handleMerge.bind(this)
this.handleDelete = this.handleDelete.bind(this)
this.renderForm = this.renderForm.bind(this)
this.setToCurrentLocation = this.setToCurrentLocation.bind(this)
@ -62,6 +76,8 @@ class StopForm extends Component {
delete body.id
delete body.geojson
delete body.latlon
// this is for merger only
delete body.mergeWith
if (id !== null) {
store.dispatch(updateStop(id, body))
} else {
@ -70,6 +86,15 @@ class StopForm extends Component {
this.setState({justSubmit: true})
}
handleMerge() {
const { id } = this.state
let body = {
stop: this.state.mergeWith,
}
store.dispatch(mergeStop(id, body))
this.setState({justSubmit: true})
}
handleDelete() {
const { id } = this.state
store.dispatch(deleteStop(id))
@ -244,6 +269,38 @@ class StopForm extends Component {
className="button is-text">Cancel</Link>}
</div>
</div>
<div className="content">
<div className="field">
<label className="label">Merge with stop</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={null}
loadOptions={getStopsAsyncSelect}
components={{ Option }}
onChange={(item, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'mergeWith',
value: item,
}}
this.handleChange(evt)
}
}}
/>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-warning"
onClick={this.handleMerge}
disabled={fetching || this.state.mergeWith === null}>
Merge</button>
</div>
</div>
</div>
</StyledStopForm>
)
}

24
src/components/StopTimeForm.js

@ -2,7 +2,6 @@ import React, { Component } from 'react'
import styled from 'styled-components'
import AsyncSelect from 'react-select/lib/Async'
import { components } from 'react-select'
import { CancelToken } from 'axios'
import Input from './parts/Input'
import OurSelect from './parts/Select'
@ -13,8 +12,7 @@ import store from '../store'
import {
PickUpTypes, DropOffTypes, TimePointChoices
} from '../constants/choices'
import { getItemFromList } from '../utils'
import { apiClient } from '../utils/ApiClient'
import { getItemFromList, getStopsAsyncSelect } from '../utils'
const StyleBox = styled.div`
padding: 5px;
@ -23,7 +21,6 @@ margin-bottom: 1rem;
`
const Option = (props) => {
console.log('option: ', props)
const { stop_id, name, stop_desc } = props.data
return (
<components.Option {...props}>
@ -83,23 +80,6 @@ class StopTimeForm extends Component {
return null
}
getStops = (inputValue, callback) => {
const that = this
const cancelToken = new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
if (that.cancel)
that.cancel()
that.cancel = c
})
apiClient(`/stop/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
...i,
label: i.name
})))
})
}
render() {
const item = this.state
return (
@ -117,7 +97,7 @@ class StopTimeForm extends Component {
cacheOptions={true}
defaultOptions
defaultValue={item.stop && {...item.stop, label: item.stop.name}}
loadOptions={this.getStops}
loadOptions={getStopsAsyncSelect}
components={{ Option }}
onChange={(item, evt) => {
if (evt.action === 'select-option') {

50
src/components/TripForm.js

@ -4,13 +4,17 @@ import { connect } from 'react-redux'
import { Redirect, Link } from 'react-router-dom'
import StopTimeOne from './StopTimeOne'
import FrequencyOne from './FrequencyOne'
import StopTimeForm from './StopTimeForm'
import FrequencyForm from './FrequencyForm'
import HorizontalInput from './parts/HorizontalInput'
import HorizontalSelect from './parts/HorizontalSelect'
import { updateTrip, createTrip, deleteTrip } from '../actions/trip'
import { getCalendar } from '../actions/calendar'
import { getRoute } from '../actions'
import { getTrip } from '../actions/trip'
import { getStopTime } from '../actions/stoptime'
import { getFrequency } from '../actions/frequency'
import store from '../store'
import {
DirectionChoices, WheelChairAccessibles,
@ -37,6 +41,7 @@ class TripForm extends Component {
service: null,
frequency_set: [],
newStopTime: false,
newFrequency: false,
}
constructor() {
@ -51,6 +56,11 @@ class TripForm extends Component {
this.setState({ newStopTime: !this.state.newStopTime })
}
toggleNewFrequency = () => {
this.setState({ newFrequency: !this.state.newFrequency })
}
handleChange(evt) {
let updated = {}
updated[evt.target.name] = evt.target.value
@ -90,16 +100,14 @@ class TripForm extends Component {
}
}
if (tripId !== undefined && state.id === null) {
const { route } = props
const tRoute = route.results.filter(ele => ele.route_id === routeId)
if (tRoute.length > 0) {
const trips = tRoute[0].trip_set.filter(ele => ele.trip_id === tripId)
if (trips.length > 0) {
store.dispatch(getStopTime(`trip=${trips[0].id}&limit=100`))
return trips[0]
}
const { trip } = props
const matches = trip.results.filter(ele => ele.trip_id === tripId)
if (matches.length > 0) {
store.dispatch(getStopTime(`trip=${matches[0].id}&limit=100`))
store.dispatch(getFrequency(`trip=${matches[0].id}`))
return matches[0]
} else {
store.dispatch(getRoute(`agency=${agencyId}`))
store.dispatch(getTrip(`route=${routeId}`))
}
}
return null
@ -215,24 +223,20 @@ class TripForm extends Component {
render () {
const one = this.state
const { frequency } = this.props
const { fetching } = this.props.trip
// redirect to view page if no data
const { agencyId, routeId } = this.props.match.params
// this is a create form
// if (tripId === undefined) {
// if (one.justSubmit === true && !fetching) {
// return <Redirect to={`/trip/`} />
// }
// return this.renderForm()
// }
// if (one.id === null && tripId.length > 0)
// return <Redirect to={`/trip/`} />
let freq = (one.frequency_set.length > 0) ? one.frequency_set[0] : null
if (freq === null && frequency.count > 0) {
freq = frequency.results[0]
}
// redirect to trip list if submitted
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/map/${agencyId}/route/${routeId}/trip`} />
}
let StopTimePane = null
if (this.state.id !== null) {
StopTimePane = (
@ -253,6 +257,13 @@ class TripForm extends Component {
<div className="columns">
<div className="column is-half">
{this.renderForm()}
{!this.state.newFrequency && <button className="button is-outlined" onClick={this.toggleNewFrequency}>new Frequency</button>}
{this.state.newFrequency && <FrequencyForm {...this.props}
trip={this.state.id}
toggleEditMode={this.toggleNewFrequency} /> }
{frequency.results.map(
ele => <FrequencyOne key={`frqy-${ele.id}`} tripId={one.id} item={ele} />)}
</div>
{StopTimePane}
</div>
@ -266,6 +277,7 @@ const mapStateToProps = state => ({
trip: state.trip,
calendar: state.calendar,
stoptime: state.stoptime,
frequency: state.frequency,
})
const connectTripForm = connect(

7
src/constants/ActionTypes.js

@ -70,6 +70,13 @@ export const CALENDAR_CREATE = 'CALENDAR_CREATE'
export const CALENDAR_DELETE = 'CALENDAR_DELETE'
export const CALENDAR_UPDATE = 'CALENDAR_UPDATE'
export const FREQUENCY_REQUEST = 'FREQUENCY_REQUEST'
export const FREQUENCY_FAILURE = 'FREQUENCY_FAILURE'
export const FREQUENCY_SUCCESS = 'FREQUENCY_SUCCESS'
// below items are SUCCESS for other tasks
export const FREQUENCY_CREATE = 'FREQUENCY_CREATE'
export const FREQUENCY_DELETE = 'FREQUENCY_DELETE'
export const FREQUENCY_UPDATE = 'FREQUENCY_UPDATE'
export const FAREATTR_REQUEST = 'FAREATTR_REQUEST'
export const FAREATTR_FAILURE = 'FAREATTR_FAILURE'

4
src/constants/choices.js

@ -58,3 +58,7 @@ export const BikeAllowanceChoices = [
{ value: '2', label: 'Not possible' },
]
export const ExactTimeChoices = [
{ value: '0', label: 'Not exactly scheduled.' },
{ value: '1', label: 'Exactly scheduled' },
]

5
src/container/Geo.js

@ -276,14 +276,15 @@ class Geo extends Component {
</StopToggler>
<Map
center={this.state.mapCenter}
zoom={13}
zoom={geo.zoom}
length={4}
zoomControl={false}
animate
style={{flex: 1}}
onMoveend={(e) => {
const lc = e.target.getCenter()
store.dispatch(lastCenterUpdate(lc.lat, lc.lng))
const z = e.target.getZoom()
store.dispatch(lastCenterUpdate(lc.lat, lc.lng, z))
}}
ref='map'>
<TileLayer

88
src/reducers/frequency.js

@ -0,0 +1,88 @@
import {
FREQUENCY_CREATE, FREQUENCY_DELETE, FREQUENCY_UPDATE,
FREQUENCY_REQUEST, FREQUENCY_SUCCESS, FREQUENCY_FAILURE,
GEO_POLYGON_RESET,
} from '../constants/ActionTypes'
const frequencyInitState = {
results: [],
next: null,
count: 0,
fetching: false,
query: '',
}
const frequency = (state = frequencyInitState, action) => {
switch (action.type) {
case GEO_POLYGON_RESET:
return {
...state,
fetching: false,
count: 0,
next: null,
query: '',
results: [],
}
case FREQUENCY_REQUEST:
return {
...state,
fetching: true,
query: action.meta !== undefined ? action.meta.query : state.query,
}
case FREQUENCY_SUCCESS:
const { count, next, prev, results } = action.payload
return {
...state,
fetching: false,
count,
next,
results: [
...((prev) ? state.results : []),
...results,
]
}
case FREQUENCY_UPDATE:
const { id } = action.payload
const oldResults = state.results
const targetInd = oldResults.findIndex(ele => ele.id === id)
return {
...state,
fetching: false,
results: [
...oldResults.slice(0, targetInd),
action.payload,
...oldResults.slice(targetInd + 1)
]
}
case FREQUENCY_CREATE:
return {
...state,
fetching: false,
count: state.count + 1,
results: [
...state.results,
action.payload,
]
}
case FREQUENCY_DELETE:
const deleteInd = state.results.findIndex(ele => ele.id === action.meta.id)
return {
...state,
count: state.count - 1,
fetching: false,
results: [
...state.results.slice(0, deleteInd),
...state.results.slice(deleteInd + 1)
]
}
case FREQUENCY_FAILURE:
return {
...state,
fetching: false,
}
default:
return state;
}
}
export default frequency

3
src/reducers/geo.js

@ -14,6 +14,7 @@ const initialState = {
markers: [],
lastCenter: [13.84626739, 100.538],
mapCenter: [13.84626739, 100.538],
zoom: 13,
showStopMarker: false,
draggableMarker: false,
draggableMarkerLatlon: [13.8462745, 100.5382592],
@ -45,7 +46,7 @@ const geo = (state = initialState, action) => {
case GEO_LASTCENTER_UPDATE:
return {
...state,
lastCenter: action.payload,
...action.payload,
}
case GEO_MAPCENTER_UPDATE:
return {

2
src/reducers/index.js

@ -6,6 +6,7 @@ import route from './route'
import fareattr from './fareattr'
import calendar from './calendar'
import stoptime from './stoptime'
import frequency from './frequency'
import stop from './stop'
import trip from './trip'
@ -16,6 +17,7 @@ export default combineReducers({
route,
stop,
fareattr,
frequency,
calendar,
stoptime,
trip,

10
src/reducers/stop.js

@ -38,7 +38,15 @@ const stop = (state = stopInitState, action) => {
}
case STOP_UPDATE:
const { id } = action.payload
const oldResults = state.results
let oldResults = state.results
if (action.meta && action.meta.opt === "merge") {
const rmStop = action.meta.mergeWith.stop
const rmInd = oldResults.findIndex(ele => ele.id === rmStop.id)
oldResults = [
...oldResults.slice(0, rmInd),
...oldResults.slice(rmInd + 1)
]
}
const targetInd = oldResults.findIndex(ele => ele.id === id)
return {
...state,

20
src/utils/index.js

@ -1,3 +1,5 @@
import { CancelToken } from 'axios'
import { apiClient } from './ApiClient'
export const getItemFromList = (targetValue, list, defaultValue) => {
if (!targetValue && defaultValue === undefined)
@ -11,3 +13,21 @@ export const getItemFromList = (targetValue, list, defaultValue) => {
return f[0]
return ''
}
export const getStopsAsyncSelect = (inputValue, callback) => {
const that = this
const cancelToken = new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
if (that.cancel)
that.cancel()
that.cancel = c
})
apiClient(`/stop/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
...i,
label: i.name
})))
})
}

Loading…
Cancel
Save