Browse Source

Basic Fare manager

dev
sipp11 6 years ago
parent
commit
0de7cc7309
  1. 10
      src/actions/fare.js
  2. 88
      src/components/FareAttributesForm.js
  3. 107
      src/components/FareList.js
  4. 123
      src/components/FareRulesForm.js
  5. 12
      src/constants/choices.js
  6. 2
      src/container/Main.js
  7. 3
      src/reducers/fareattr.js
  8. 3
      src/reducers/farerule.js
  9. 2
      src/sagas.js
  10. 36
      src/utils/index.js

10
src/actions/fare.js

@ -10,11 +10,12 @@ export const getFareAttr = (query) => ({
endpoint: `${API_URL}/fare-attribute/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.fareattr.fetching || state.fareattr.query === query,
bailout: (state) => state.fareattr.fetching || (
state.fareattr.query !== undefined && state.fareattr.query === query),
types: [
{
type: types.FAREATTR_REQUEST,
meta: { query: query },
meta: { query: query || '' },
},
types.FAREATTR_SUCCESS,
types.FAREATTR_FAILURE,
@ -72,11 +73,12 @@ export const getFareRule = (query) => ({
endpoint: `${API_URL}/fare-rule/?${query || ''}`,
method: 'GET',
headers: RSAAHeaders,
bailout: (state) => state.fareattr.fetching || state.fareattr.query === query,
bailout: (state) => state.farerule.fetching || (
state.farerule.query !== undefined && state.farerule.query === query),
types: [
{
type: types.FARERULE_REQUEST,
meta: { query: query },
meta: { query: query || '' },
},
types.FARERULE_SUCCESS,
types.FARERULE_FAILURE,

88
src/components/FareAttributesForm.js

@ -9,9 +9,14 @@ import {
deleteFareAttr
} from '../actions/fare'
import store from '../store'
import HorizontalInput from './parts/HorizontalInput'
import HorizontalDate from './parts/HorizontalDate'
import HorizontalCheckbox from './parts/HorizontalCheckbox'
import Input from './parts/Input'
import OurSelect from './parts/Select'
import AsyncSelect from 'react-select/lib/Async'
import { components } from 'react-select'
import {
PaymentMethodChoices, TransferChoices
} from '../constants/choices'
import { getItemFromList, getAgencyAsyncSelect } from '../utils'
const StyledFareAttributesForm = styled.div`
padding: 1rem;
@ -19,6 +24,17 @@ background: #fafafa;
`;
const AgencyOption = (props) => {
const { agency_id, name } = props.data
return (
<components.Option {...props}>
<code>{agency_id}</code> {name}
</components.Option>
)
}
class FareAttributesForm extends Component {
state = {
@ -26,10 +42,10 @@ class FareAttributesForm extends Component {
fare_id: "",
price: 0,
currency_type: "THB", // ISO 4217
payment_method: "", // 0 - paid on board or 1 - paid before boarding
payment_method: "0", // 0 - paid on board or 1 - paid before boarding
transfer: "0", // 0 no transfer, 1 - transfer once, 2 tranfer twice,
// '' - unlimited transfer
agency_id: false, // optional
agency: null, // optional
transfer_duration: "0", // optional 0 - no transfer allowed,
// otherwise it's length of time in seconds before
// transfer expires
@ -69,9 +85,9 @@ class FareAttributesForm extends Component {
componentWillMount() {
const { props } = this
const { serviceId } = props.match.params
const { attribID } = props.match.params
const { results } = props.fareattr
const ones = results.filter(ele => ele.service_id === serviceId)
const ones = results.filter(ele => ele.id === +attribID)
if (ones.length > 0) {
this.setState(ones[0])
}
@ -84,21 +100,21 @@ class FareAttributesForm extends Component {
<StyledFareAttributesForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="content">
<HorizontalInput
<Input
label="Fare ID"
type="text"
fieldName="fare_id"
value={one.fare_id || ''}
handleChange={this.handleChange} />
<HorizontalInput
<Input
label="Price"
type="text"
fieldName="price"
value={one.price || ''}
handleChange={this.handleChange} />
<HorizontalInput
<Input
key="currency_type"
label="Currency"
type="text"
@ -106,15 +122,23 @@ class FareAttributesForm extends Component {
value={one.currency_type || 'THB'}
handleChange={this.handleChange} />
<HorizontalInput
key="payment_method"
label="Payment method"
<OurSelect
label="Payment Method"
type="text"
fieldName="payment_method"
value={one.payment_method || '0'}
handleChange={this.handleChange} />
value={getItemFromList(one.payment_method, PaymentMethodChoices, '0')}
handleChange={this.handleChange}
choices={PaymentMethodChoices} />
<HorizontalInput
<OurSelect
label="Transfer"
type="text"
fieldName="transfer"
value={getItemFromList(one.transfer, TransferChoices, '0')}
handleChange={this.handleChange}
choices={TransferChoices} />
<Input
key="transfer"
label="Transfer"
type="text"
@ -122,18 +146,30 @@ class FareAttributesForm extends Component {
value={one.transfer || '0'}
handleChange={this.handleChange} />
{/* TODO: probably need HorizontalSelect */}
<HorizontalInput
key="agency_id"
label="Agency ID"
type="text"
fieldName="agency_id"
value={one.agency_id || ''}
handleChange={this.handleChange} />
<div className="field">
<label className="label">Agency</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.agency && {...one.agency, label: one.agency.name}}
loadOptions={getAgencyAsyncSelect}
components={{ Option: AgencyOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'agency',
value: resp,
}}
this.handleChange(evt)
}
}}
/>
</div>
<HorizontalInput
<Input
key="transfer_duration"
label="Transfer Duration"
label="Transfer Duration (sec)"
type="text"
fieldName="transfer_duration"
value={one.transfer_duration || '0'}

107
src/components/FareList.js

@ -3,7 +3,11 @@ import styled from 'styled-components'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { getCalendar } from '../actions/calendar'
import { getFareAttr, getFareRule } from '../actions/fare'
import { getItemFromList } from '../utils'
import {
PaymentMethodChoices, TransferChoices
} from '../constants/choices'
import store from '../store'
@ -12,11 +16,6 @@ padding: 1rem;
background: #fafafa;
`
const GapBox = styled.span`
margin-right: 5px;
color: ${props => props.checked ? 'green' : 'gray'};;
`
const FakeRow = styled.nav`
padding-top: 5px;
padding-bottom: 5px;
@ -24,20 +23,15 @@ background: white;
margin-bottom: 1rem;
`
const CheckboxIcon = (props) => (
<GapBox checked={props.checked}>
{props.checked === true && <span><i className="fas fa-check-square"></i></span>}
{props.checked !== true && <span><i className="fas fa-square"></i></span>}
</GapBox>
)
class FareList extends Component {
componentWillMount() {
const { count } = this.props.fareattr
if (count === 0)
store.dispatch(getCalendar())
const { fareattr, farerule } = this.props
if (fareattr.count === 0)
store.dispatch(getFareAttr())
if (farerule.count === 0)
store.dispatch(getFareRule())
}
render() {
@ -47,105 +41,94 @@ class FareList extends Component {
<StyledBox>
<h1 className="title">Fare</h1>
<div className="columns">
<div className="column is-6">
<nav className="level is-mobile">
<p className="level-item has-text-centered">
<Link className="link is-info" to={`${match.url}/rules/new`}>
<i className="fas fa-plus" /> New fare rule
<Link className="link is-info" to={`${match.url}/attributes/new`}>
<i className="fas fa-plus" /> New fare attributes
</Link>
</p>
</nav>
{fareattr.results && Object.keys(farerule.results).map(i => (
{fareattr.results && Object.keys(fareattr.results).map(i => (
<FakeRow className="level panel" key={fareattr.results[i].fare_id}>
<div className="level-item has-text-centered">
<div>
<p className="heading">Fare ID</p>
<p className="title"><Link to={`${match.url}/${fareattr.results[i].fare_id}`}>{fareattr.results[i].fare_id}</Link></p>
<p className="title"><Link to={`${match.url}/attributes/${fareattr.results[i].id}`}>{fareattr.results[i].fare_id}</Link></p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Origin ID</p>
<p className="title">{fareattr.results[i].origin_id}</p>
<p className="heading">Price</p>
<p key={fareattr.results[i].id}>{fareattr.results[i].price}
<small>{fareattr.results[i].currency_type}</small>
<br />
{getItemFromList(fareattr.results[i].payment_method, PaymentMethodChoices, '') && getItemFromList(fareattr.results[i].payment_method, PaymentMethodChoices, '').label}
</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Destination ID (or contains)</p>
<p className="title">
{fareattr.results[i].destination_id}
{fareattr.results[i].contains_id}
</p>
<p className="heading">Transfer / sec</p>
<p>
{getItemFromList(fareattr.results[i].transfer, TransferChoices, '') && getItemFromList(fareattr.results[i].transfer, TransferChoices, '').label}
/ {fareattr.results[i].transfer_duration}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Route ID</p>
<p className="title">
{fareattr.results[i].route_id}
</p>
<p className="heading">Agency</p>
<p className="title">{fareattr.results[i].agency.name}</p>
</div>
</div>
</FakeRow>
))}
</div>
<div className="column is-6">
<nav className="level is-mobile">
<p className="level-item has-text-centered">
<Link className="link is-info" to={`${match.url}/attributes/new`}>
<i className="fas fa-plus" /> New fare attributes
<Link className="link is-info" to={`${match.url}/rules/new`}>
<i className="fas fa-plus" /> New fare rule
</Link>
</p>
</nav>
{fareattr.results && Object.keys(fareattr.results).map(i => (
<FakeRow className="level panel" key={fareattr.results[i].fare_id}>
{farerule.results && Object.keys(farerule.results).map(i => (
<FakeRow className="level panel" key={farerule.results[i].fare_id}>
<div className="level-item has-text-centered">
<div>
<p className="heading">Fare ID</p>
<p className="title"><Link to={`${match.url}/${fareattr.results[i].fare_id}`}>{fareattr.results[i].fare_id}</Link></p>
<p className="title"><Link to={`${match.url}/rules/${farerule.results[i].id}`}>{farerule.results[i].fare.fare_id}</Link></p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Price</p>
<p className="title">{fareattr.results[i].price}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Currency</p>
<p className="title">{fareattr.results[i].currency_type}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Payment Method</p>
<p className="title">{fareattr.results[i].payment_method}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Transfer</p>
<p className="title">{fareattr.results[i].transfer}</p>
<p className="heading">Origin ID</p>
<p className="title">{farerule.results[i].origin_id}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Duration</p>
<p className="title">{fareattr.results[i].transfer_duration}</p>
<p className="heading">Destination ID (or contains)</p>
<p className="title">
{farerule.results[i].destination_id}
{farerule.results[i].contains_id}
</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Agency</p>
<p className="title">{fareattr.results[i].agency_id}</p>
<p className="heading">Route ID</p>
<p className="title">
{farerule.results[i].route_id}
</p>
</div>
</div>
</FakeRow>
))}
</div>
</div>
</StyledBox>
)

123
src/components/FareRulesForm.js

@ -9,15 +9,34 @@ import {
deleteFareRule
} from '../actions/fare'
import store from '../store'
import HorizontalInput from './parts/HorizontalInput'
import HorizontalDate from './parts/HorizontalDate'
import HorizontalCheckbox from './parts/HorizontalCheckbox'
import Input from './parts/Input'
import AsyncSelect from 'react-select/lib/Async'
import { components } from 'react-select'
import { getStopsAsyncSelect, getFareAttrAsyncSelect } from '../utils'
const StyledFareRulesForm = styled.div`
padding: 1rem;
background: #fafafa;
`;
const StopOption = (props) => {
const { stop_id, name } = props.data
return (
<components.Option {...props}>
<code>{stop_id}</code> {name}
</components.Option>
)
}
const FareAttrOption = (props) => {
const { fare_id, price, currency_type } = props.data
return (
<components.Option {...props}>
<code>{fare_id}</code> {price} {currency_type}
</components.Option>
)
}
class FareRulesForm extends Component {
@ -64,9 +83,10 @@ class FareRulesForm extends Component {
componentWillMount() {
const { props } = this
const { serviceId } = props.match.params
const { ruleID } = props.match.params
const { results } = props.farerule
const ones = results.filter(ele => ele.service_id === serviceId)
const ones = results.filter(ele => ele.id === +ruleID)
if (ones.length > 0) {
this.setState(ones[0])
}
@ -79,35 +99,78 @@ class FareRulesForm extends Component {
<StyledFareRulesForm>
<h1 className="title">{one.name}&nbsp;&nbsp;</h1>
<div className="content">
<HorizontalInput
label="Fare ID"
type="text"
fieldName="fare_id"
value={one.fare_id || ''}
handleChange={this.handleChange} />
<HorizontalInput
<div className="field">
<label className="label">Fare ID</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.fare && {...one.fare, label: one.fare.fare_id}}
loadOptions={getFareAttrAsyncSelect}
components={{ Option: FareAttrOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'fare',
value: resp,
}}
this.handleChange(evt)
}
}}
/>
</div>
<Input
label="Route ID"
type="text"
fieldName="route_id"
value={one.route_id || ''}
handleChange={this.handleChange} />
<HorizontalInput
label="Origin ID"
type="text"
fieldName="origin_id"
value={one.origin_id || ''}
handleChange={this.handleChange} />
<div className="field">
<label className="label">Origin ID</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.origin_id && {value: one.origin_id, label: one.origin_id}}
loadOptions={getStopsAsyncSelect}
components={{ Option: StopOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'origin_id',
value: resp.stop_id,
}}
this.handleChange(evt)
}
}}
/>
</div>
<HorizontalInput
label="Destination ID"
type="text"
fieldName="destination_id"
value={one.destination_id || ''}
handleChange={this.handleChange} />
<div className="field">
<label className="label">Destination ID</label>
<AsyncSelect
cacheOptions={true}
defaultOptions
defaultValue={one.destination_id && {value: one.destination_id, label: one.destination_id}}
loadOptions={getStopsAsyncSelect}
components={{ Option: StopOption }}
onChange={(resp, evt) => {
if (evt.action === 'select-option') {
let evt = {
target: {
name: 'destination_id',
value: resp.stop_id,
}}
this.handleChange(evt)
}
}}
/>
</div>
<HorizontalInput
<Input
label="Contains ID"
type="text"
fieldName="contains_id"
@ -130,7 +193,7 @@ class FareRulesForm extends Component {
DELETE</button>
</div>}
<div className="control">
<Link to={`/farerule`} className="button is-text">Cancel</Link>
<Link to={`/fare`} className="button is-text">Cancel</Link>
</div>
</div>
</StyledFareRulesForm>
@ -145,17 +208,17 @@ class FareRulesForm extends Component {
// this is a create form
if (serviceId === undefined) {
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/farerule`} />
return <Redirect to={`/fare`} />
}
return this.renderForm()
}
if (one.id === null && serviceId.length > 0)
return <Redirect to={`/farerule`} />
return <Redirect to={`/fare`} />
// redirect to fare list if submitted
if (one.justSubmit === true && !fetching) {
return <Redirect to={`/farerule`} />
return <Redirect to={`/fare`} />
}
return this.renderForm()
}
@ -164,7 +227,7 @@ class FareRulesForm extends Component {
const mapStateToProps = state => ({
farerule: state.farerule
farerule: state.farerule,
})
const connectFareRulesForm = connect(

12
src/constants/choices.js

@ -62,3 +62,15 @@ export const ExactTimeChoices = [
{ value: '0', label: 'Not exactly scheduled.' },
{ value: '1', label: 'Exactly scheduled' },
]
export const PaymentMethodChoices = [
{ value: '0', label: 'Paid on board' },
{ value: '1', label: 'Paid before boarding' },
]
export const TransferChoices = [
{ value: '0', label: 'No transfer' },
{ value: '1', label: 'Transfer once' },
{ value: '2', label: 'Transfer twice' },
{ value: '', label: 'Unlimited transfer' },
]

2
src/container/Main.js

@ -55,7 +55,9 @@ class Main extends Component {
<Route exact path={`${match.url}fare`} component={FareList} />
<Route exact path={`${match.url}fare/rules/new`} component={FareRulesForm} />
<Route exact path={`${match.url}fare/rules/:ruleID`} component={FareRulesForm} />
<Route exact path={`${match.url}fare/attributes/new`} component={FareAttributesForm} />
<Route exact path={`${match.url}fare/attributes/:attribID`} component={FareAttributesForm} />
<Route exact path={`${match.url}calendar/new`} component={CalendarForm} />
<Route exact path={`${match.url}calendar/:serviceId`} component={CalendarForm} />

3
src/reducers/fareattr.js

@ -14,11 +14,10 @@ const fareAttrInitState = {
const fareAttr = (state = fareAttrInitState, action) => {
switch(action.type) {
case FAREATTR_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
query: action.meta !== undefined ? action.meta.query : state.query,
}
case FAREATTR_SUCCESS:
const { count, next, prev, results } = action.payload

3
src/reducers/farerule.js

@ -14,11 +14,10 @@ const fareRuleInitState = {
const fareRule = (state = fareRuleInitState, action) => {
switch(action.type) {
case FARERULE_REQUEST:
const { query } = action.meta
return {
...state,
fetching: true,
query,
query: action.meta !== undefined ? action.meta.query : state.query,
}
case FARERULE_SUCCESS:
const { count, next, prev, results } = action.payload

2
src/sagas.js

@ -1,4 +1,4 @@
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import { call, put, takeLatest } from 'redux-saga/effects' // takeEvery
import { apiClient } from './utils/ApiClient'
import * as types from './constants/ActionTypes'

36
src/utils/index.js

@ -31,3 +31,39 @@ export const getStopsAsyncSelect = (inputValue, callback) => {
})))
})
}
export const getFareAttrAsyncSelect = (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(`/fare-attribute/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
...i,
label: i.fare_id
})))
})
}
export const getAgencyAsyncSelect = (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(`/agency/?search=${inputValue}`, { cancelToken })
.then((resp) => {
callback(resp.data.results.map(i => ({
...i,
label: i.name
})))
})
}

Loading…
Cancel
Save