diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js
index b0929234d..4f6b61ccb 100644
--- a/lib/actions/call-taker.js
+++ b/lib/actions/call-taker.js
@@ -6,7 +6,7 @@ import { createAction } from 'redux-actions'
import {routingQuery} from './api'
import {setQueryParam} from './form'
-import {getGroupSize, searchToQuery} from '../util/call-taker'
+import {getGroupSize, getTrip, searchToQuery} from '../util/call-taker'
import {URL_ROOT} from '../util/constants'
import {getTimestamp} from '../util/state'
@@ -31,6 +31,7 @@ const requestingFieldTripDetails = createAction('REQUESTING_FIELD_TRIP_DETAILS')
export const beginCall = createAction('BEGIN_CALL')
export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER')
export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP')
+export const setGroupSize = createAction('SET_GROUP_SIZE')
export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY')
export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS')
@@ -183,12 +184,13 @@ export function addFieldTripNote (request, note) {
const {callTaker, otp} = getState()
const {datastoreUrl} = otp.config
if (sessionIsInvalid(callTaker.session)) return
- const {sessionId, userName} = callTaker.session
+ console.log(callTaker.session)
+ const {sessionId, username} = callTaker.session
const queryData = new FormData()
queryData.append('sessionId', sessionId)
queryData.append('note.note', note.note)
queryData.append('note.type', note.type)
- queryData.append('note.userName', userName)
+ queryData.append('note.userName', username)
queryData.append('requestId', request.id)
return fetch(`${datastoreUrl}/fieldtrip/addNote`,
{method: 'POST', body: queryData}
@@ -222,6 +224,56 @@ export function deleteFieldTripNote (request, noteId) {
}
}
+function processPlan (tripPlan, restoring) {
+ if (updateActiveOnly) {
+ var itinIndex = itinWidget.activeIndex
+ tripPlan.itineraries[0].groupSize = groupPlan.itineraries[itinIndex].groupSize
+ itinWidget.updateItineraries(tripPlan.itineraries)
+ updateActiveOnly = false
+ drawItinerary(tripPlan.itineraries[0])
+ return
+ }
+
+ if (groupPlan == null) {
+ groupPlan = new otp.modules.planner.TripPlan(null, _.extend(tripPlan.queryParams, { groupSize : groupSize }))
+ }
+
+ if (itinWidget == null) createItinerariesWidget()
+
+ var itin = tripPlan.itineraries[0]
+ var capacity = itin.getGroupTripCapacity()
+
+ // if this itin shares a vehicle trip with another one already in use, only use the remainingCapacity (as set in checkTripValidity())
+ if (itinCapacity) capacity = Math.min(capacity, itinCapacity)
+
+ groupPlan.addItinerary(itin)
+
+ var transitLegs = itin.getTransitLegs()
+ for (var i = 0; i < transitLegs.length; i++) {
+ var leg = transitLegs[i]
+ bannedSegments.push({
+ tripId : leg.tripId,
+ fromStopIndex : leg.from.stopIndex,
+ toStopIndex : leg.to.stopIndex,
+ })
+ }
+
+ setBannedTrips()
+
+ if (currentGroupSize > capacity) {
+ // group members remain. plan another trip
+ currentGroupSize -= capacity
+ itin.groupSize = capacity
+ //console.log("remaining: "+currentGroupSize)
+ itinCapacity = null
+ planTrip()
+ } else {
+ // we're done. show the results
+ itin.groupSize = currentGroupSize
+ showResults()
+ }
+}
+
/**
* Edit teacher (AKA submitter) notes for a field trip request.
*/
@@ -245,10 +297,132 @@ export function editSubmitterNotes (request, submitterNotes) {
}
}
-export function planOutbound (request) {
+export function saveRequestTrip (request, outbound, groupPlan) {
+ return function (dispatch, getState) {
+ // If plan is not valid, return before persisting trip.
+ const check = checkPlanValidity(request, groupPlan)
+ if (!check.isValid) return alert(check.message)
+ const requestOrder = outbound ? 0 : 1
+ const type = outbound ? 'outbound' : 'inbound'
+ const preExistingTrip = getTrip(request, outbound)
+ if (preExistingTrip) {
+ const msg = `This action will overwrite a previously planned ${type} itinerary for this request. Do you wish to continue?`
+ if (!confirm(msg)) return
+ }
+ dispatch(saveTrip(request, requestOrder))
+ }
+}
+
+function checkPlanValidity (request, groupPlan) {
+ if (groupPlan == null) {
+ return {
+ isValid: false,
+ message: 'No active plan to save'
+ }
+ }
+
+ // FIXME: add back in offset?
+ const planDeparture = moment(groupPlan.earliestStartTime) // .add('hours', otp.config.timeOffset)
+ const requestDate = moment(request.travelDate)
+
+ if (
+ planDeparture.date() !== requestDate.date() ||
+ planDeparture.month() !== requestDate.month() ||
+ planDeparture.year() !== requestDate.year()
+ ) {
+ return {
+ isValid: false,
+ message: `Planned trip date (${planDeparture.format('MM/DD/YYYY')}) is not the requested day of travel (${requestDate.format('MM/DD/YYYY')})`
+ }
+ }
+
+ // FIXME More checks? E.g., origin/destination
+
+ return { isValid: true }
+}
+
+function saveTrip (request, requestOrder) {
+ return function (dispatch, getState) {
+ const {callTaker, otp} = getState()
+ const {datastoreUrl} = otp.config
+ if (sessionIsInvalid(callTaker.session)) return
+ const {sessionId, username} = callTaker.session
+ const data = {
+ sessionId: sessionId,
+ requestId: request.id,
+ 'trip.requestOrder': requestOrder,
+ 'trip.origin': getStartOTPString(),
+ 'trip.destination': getEndOTPString(),
+ 'trip.createdBy': username,
+ 'trip.passengers': groupSize,
+ 'trip.departure': moment(groupPlan.earliestStartTime).add('hours', otp.config.timeOffset).format('YYYY-MM-DDTHH:mm:ss'),
+ 'trip.queryParams': JSON.stringify(groupPlan.queryParams)
+ }
+
+ for (let i = 0; i < groupPlan.itineraries.length; i++) {
+ const itin = groupPlan.itineraries[i]
+ data[`itins[${i}].passengers`] = itin.groupSize
+ data[`itins[${i}].itinData`] = otp.util.Text.lzwEncode(JSON.stringify(itin.itinData))
+ data[`itins[${i}].timeOffset`] = otp.config.timeOffset || 0
+
+ const legs = itin.getTransitLegs()
+
+ for (let l = 0; l < legs.length; l++) {
+ const leg = legs[l]
+ const routeName = (leg.routeShortName !== null ? ('(' + leg.routeShortName + ') ') : '') + (leg.routeLongName || '')
+ const tripHash = tripHashLookup[leg.tripId]
+
+ data[`gtfsTrips[${i}][${l}].depart`] = moment(leg.startTime).format('HH:mm:ss')
+ data[`gtfsTrips[${i}][${l}].arrive`] = moment(leg.endTime).format('HH:mm:ss')
+ data[`gtfsTrips[${i}][${l}].agencyAndId`] = leg.tripId
+ data[`gtfsTrips[${i}][${l}].tripHash`] = tripHash
+ data[`gtfsTrips[${i}][${l}].routeName`] = routeName
+ data[`gtfsTrips[${i}][${l}].fromStopIndex`] = leg.from.stopIndex
+ data[`gtfsTrips[${i}][${l}].toStopIndex`] = leg.to.stopIndex
+ data[`gtfsTrips[${i}][${l}].fromStopName`] = leg.from.name
+ data[`gtfsTrips[${i}][${l}].toStopName`] = leg.to.name
+ data[`gtfsTrips[${i}][${l}].headsign`] = leg.headsign
+ data[`gtfsTrips[${i}][${l}].capacity`] = itin.getModeCapacity(leg.mode)
+ if (leg.tripBlockId) data[`gtfsTrips[${i}][${l}].blockId`] = leg.tripBlockId
+ }
+ }
+ return fetch(`${datastoreUrl}/fieldtrip/newTrip`,
+ {method: 'POST', body: data}
+ )
+ .then((res) => {
+ console.log(res)
+ if (res === -1) {
+ alert('This plan could not be saved due to a lack of capacity on one or more vehicles. Please re-plan your trip.')
+ } else {
+ dispatch(fetchFieldTripDetails(request.id))
+ }
+ })
+ .catch(err => {
+ alert(`Error saving trip: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
+export function planTrip (request, outbound) {
+ return async function (dispatch, getState) {
+ dispatch(setGroupSize(getGroupSize(request)))
+ const trip = getTrip(request, outbound)
+ if (!trip) {
+ // Construct params from request details
+ if (outbound) dispatch(planOutbound(request))
+ else dispatch(planInbound(request))
+ } else {
+ // Populate params from saved query params
+ const params = await planParamsToQueryAsync(JSON.parse(trip.queryParams))
+ dispatch(setQueryParam(params, trip.id))
+ }
+ }
+}
+
+function planOutbound (request) {
return async function (dispatch, getState) {
const {config} = getState().otp
- // this.clearTrip()
+ // clearTrip()
const locations = await planParamsToQueryAsync({
fromPlace: request.startLocation,
toPlace: request.endLocation
@@ -265,14 +439,14 @@ export function planOutbound (request) {
}
}
-export function planInbound (request) {
+function planInbound (request) {
return async function (dispatch, getState) {
const {config} = getState().otp
const locations = await planParamsToQueryAsync({
fromPlace: request.endLocation,
toPlace: request.startLocation
}, config)
- // this.clearTrip()
+ // clearTrip()
const queryParams = {
date: moment(request.travelDate).format(OTP_API_DATE_FORMAT),
departArrive: 'DEPART',
@@ -285,6 +459,59 @@ export function planInbound (request) {
}
}
+/**
+ * Set group size for a field trip request.
+ */
+export function setRequestGroupSize (request, groupSize) {
+ return function (dispatch, getState) {
+ const {callTaker, otp} = getState()
+ const {datastoreUrl} = otp.config
+ if (sessionIsInvalid(callTaker.session)) return
+ const {sessionId} = callTaker.session
+ const queryData = new FormData()
+ queryData.append('sessionId', sessionId)
+ queryData.append('numStudents', groupSize.numStudents)
+ queryData.append('numFreeStudents', groupSize.numFreeStudents)
+ queryData.append('numChaperones', groupSize.numChaperones)
+ queryData.append('requestId', request.id)
+ return fetch(`${datastoreUrl}/fieldtrip/setRequestGroupSize`,
+ {method: 'POST', body: queryData}
+ )
+ .then(() => dispatch(fetchFieldTripDetails(request.id)))
+ .catch(err => {
+ alert(`Error setting group size: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
+/**
+ * Set payment info for a field trip request.
+ */
+export function setRequestPaymentInfo (request, paymentInfo) {
+ return function (dispatch, getState) {
+ const {callTaker, otp} = getState()
+ const {datastoreUrl} = otp.config
+ if (sessionIsInvalid(callTaker.session)) return
+ const {sessionId} = callTaker.session
+ const queryData = new FormData()
+ queryData.append('sessionId', sessionId)
+ queryData.append('classpassId', paymentInfo.classpassId)
+ queryData.append('paymentPreference', paymentInfo.paymentPreference)
+ queryData.append('ccType', paymentInfo.ccType)
+ queryData.append('ccName', paymentInfo.ccName)
+ queryData.append('ccLastFour', paymentInfo.ccLastFour)
+ queryData.append('checkNumber', paymentInfo.checkNumber)
+ queryData.append('requestId', request.id)
+ return fetch(`${datastoreUrl}/fieldtrip/setRequestPaymentInfo`,
+ {method: 'POST', body: queryData}
+ )
+ .then(() => dispatch(fetchFieldTripDetails(request.id)))
+ .catch(err => {
+ alert(`Error setting payment info: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
/**
* Set field trip request status (e.g., cancelled).
*/
diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js
new file mode 100644
index 000000000..1ad5d263b
--- /dev/null
+++ b/lib/components/admin/editable-section.js
@@ -0,0 +1,138 @@
+import React, {Component} from 'react'
+
+import {
+ Button,
+ P,
+ Val
+} from './styled'
+
+export default class EditableSection extends Component {
+ state = {
+ data: {},
+ isEditing: false
+ }
+
+ _exists = (val) => val !== null && typeof val !== 'undefined'
+
+ _getVal = (fieldName) => this._exists(this.state.data[fieldName])
+ ? this.state.data[fieldName]
+ : this.props.request[fieldName]
+
+ _onChange = (fieldName, value) => {
+ const stateUpdate = this.state.data
+ stateUpdate[fieldName] = value
+ this.setState({data: stateUpdate})
+ }
+
+ _onClickSave = () => {
+ const {request, onChange} = this.props
+ const data = {}
+ this.props.fields.forEach(f => {
+ data[f.fieldName] = this._getVal(f.fieldName)
+ })
+ onChange(request, data)
+ this.setState({data: {}, isEditing: false})
+ }
+
+ _toggleEditing = () => {
+ const stateUpdate = {isEditing: !this.state.isEditing}
+ if (this.state.isEditing) {
+ stateUpdate.data = {}
+ }
+ this.setState(stateUpdate)
+ }
+
+ render () {
+ const {fields, header, inputStyle, request, valueFirst} = this.props
+ const {isEditing} = this.state
+ if (!request) return null
+ return (
+ <>
+
+ {header}
+
+ {!isEditing
+ ?
+ Change
+
+ : <>
+
+ Cancel
+
+
+ Save
+
+ >
+ }
+
+
+ {fields.map(f => {
+ const input = (
+
+ )
+ return (
+
+ {valueFirst
+ ? <>{input} {f.label}>
+ : <>{f.label}: {input}>
+ }
+
+ )
+ })}
+ >
+ )
+ }
+}
+
+class InputToggle extends Component {
+ _onChange = (evt) => {
+ const {fieldName, inputProps = {}, onChange} = this.props
+ let value = evt.target.value
+ if (inputProps.type === 'number') {
+ value = +evt.target.value
+ }
+ onChange(fieldName, value)
+ }
+ render () {
+ const {inputProps, fieldName, isEditing, options, style, value} = this.props
+ if (isEditing) {
+ if (options) {
+ return (
+
+ {Object.keys(options).map(k =>
+ {options[k]}
+ )}
+
+ )
+ } else {
+ return
+ }
+ }
+ return {options ? options[value] : value}
+ }
+}
diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js
index 527cd8e03..29a53b5a7 100644
--- a/lib/components/admin/field-trip-details.js
+++ b/lib/components/admin/field-trip-details.js
@@ -1,23 +1,26 @@
+import { getDateFormat } from '@opentripplanner/core-utils/lib/time'
+import moment from 'moment'
import React, { Component } from 'react'
import { DropdownButton, MenuItem } from 'react-bootstrap'
import { connect } from 'react-redux'
import * as callTakerActions from '../../actions/call-taker'
import DraggableWindow from './draggable-window'
+import EditableSection from './editable-section'
import FieldTripNotes from './field-trip-notes'
import Icon from '../narrative/icon'
import {
B,
Button,
Container,
- Half,
Full,
+ Half,
Header,
- P,
- Val
+ P
} from './styled'
import TripStatus from './trip-status'
import Updatable from './updatable'
+import {getGroupSize} from '../../util/call-taker'
const TICKET_TYPES = {
own_tickets: 'Will use own tickets',
@@ -30,14 +33,31 @@ const PAYMENT_PREFS = {
fax_cc: 'Will fax credit card info to TriMet',
mail_check: 'Will mail check to TriMet'
}
+
+const inputProps = {
+ min: 0,
+ step: 1,
+ type: 'number'
+}
+const GROUP_FIELDS = [
+ {inputProps, fieldName: 'numStudents', label: 'students 7 or older'},
+ {inputProps, fieldName: 'numFreeStudents', label: 'students under 7'},
+ {inputProps, fieldName: 'numChaperones', label: 'chaperones'}
+]
+const PAYMENT_FIELDS = [
+ {label: 'Ticket type', fieldName: 'ticketType', options: TICKET_TYPES},
+ {label: 'Payment preference', fieldName: 'paymentPreference', options: PAYMENT_PREFS},
+ {label: 'Invoice required', fieldName: 'requireInvoice', options: ['Yes', 'No']},
+ {label: 'Class Pass Hop Card #', fieldName: 'classpassId'},
+ {label: 'Credit card type', fieldName: 'ccType'},
+ {label: 'Name on credit card', fieldName: 'ccName'},
+ {label: 'Credit card last 4 digits', fieldName: 'ccLastFour'},
+ {label: 'Check/Money order number', fieldName: 'checkNumber'}
+]
/**
* Shows the details for the active Field Trip Request.
*/
class FieldTripDetails extends Component {
- state ={
- expandNotes: true
- }
-
_editSubmitterNotes = (val) => this.props.editSubmitterNotes(this.props.request, val)
_onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null)
@@ -49,36 +69,25 @@ class FieldTripDetails extends Component {
}
}
- _toggleNotes = () => this.setState({expandNotes: !this.state.expandNotes})
-
render () {
const {
addFieldTripNote,
callTaker,
+ dateFormat,
deleteFieldTripNote,
- request
+ request,
+ setRequestGroupSize,
+ setRequestPaymentInfo
} = this.props
if (!request) return null
const {
- ccLastFour,
- ccName,
- ccType,
- checkNumber,
- classpassId,
id,
notes,
- numChaperones,
- numFreeStudents,
- numStudents,
- paymentPreference,
- requireInvoice,
schoolName,
submitterNotes,
teacherName,
- ticketType,
travelDate
} = request
- const total = numStudents + numChaperones + numFreeStudents
const {fieldTrip} = callTaker
const defaultPosition = {...fieldTrip.position}
const internalNotes = []
@@ -89,6 +98,8 @@ class FieldTripDetails extends Component {
})
defaultPosition.x = defaultPosition.x - 460
defaultPosition.y = defaultPosition.y - 100
+ const travelDateAsMoment = moment(travelDate)
+ const total = getGroupSize(request)
return (
}
header={
-
+
{schoolName} Trip (#{id})
+
+
+ Travel date: {travelDateAsMoment.format(dateFormat)}{' '}
+ ({travelDateAsMoment.fromNow()})
+
+
}
height='375px'
@@ -128,28 +145,28 @@ class FieldTripDetails extends Component {
style={{width: '450px'}}
>
- Group Information ({travelDate})
+
{schoolName}
Teacher: {teacherName}
}
onUpdate={this._editSubmitterNotes}
value={submitterNotes}
/>
-
- Total group size: {total}
-
- Change
-
-
- {numStudents} students 7 or older
- {numFreeStudents} students under 7
- {numStudents} chaperones
+ {total} total group size}
+ inputStyle={{lineHeight: '0.8em', padding: '0px', width: '50px'}}
+ onChange={setRequestGroupSize}
+ request={request}
+ valueFirst
+ />
-
- Ticket type: {TICKET_TYPES[ticketType]}
- Payment preference: {PAYMENT_PREFS[paymentPreference]}
- Invoice required:
- Class Pass Hop Card #: {classpassId}
- Credit card type: {ccType}
- Name on credit card: {ccName}
- Credit card last 4 digits: {ccLastFour}
- Check/Money order number: {checkNumber}
+
+ Payment information
+
+ }
+ inputStyle={{lineHeight: '0.8em', padding: '0px', width: '100px'}}
+ onChange={setRequestPaymentInfo}
+ request={request}
+ />
@@ -188,6 +203,7 @@ const mapStateToProps = (state, ownProps) => {
return {
callTaker: state.callTaker,
currentQuery: state.otp.currentQuery,
+ dateFormat: getDateFormat(state.otp.config),
request
}
}
@@ -199,6 +215,8 @@ const mapDispatchToProps = {
fetchQueries: callTakerActions.fetchQueries,
setActiveFieldTrip: callTakerActions.setActiveFieldTrip,
setFieldTripFilter: callTakerActions.setFieldTripFilter,
+ setRequestGroupSize: callTakerActions.setRequestGroupSize,
+ setRequestPaymentInfo: callTakerActions.setRequestPaymentInfo,
setRequestStatus: callTakerActions.setRequestStatus,
toggleFieldTrips: callTakerActions.toggleFieldTrips
}
diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js
index d87ce5aaf..9ff371bce 100644
--- a/lib/components/admin/field-trip-notes.js
+++ b/lib/components/admin/field-trip-notes.js
@@ -1,5 +1,5 @@
import React, { Component } from 'react'
-import { Badge } from 'react-bootstrap'
+import { Badge, Button as BsButton } from 'react-bootstrap'
import styled from 'styled-components'
import Icon from '../narrative/icon'
@@ -21,10 +21,14 @@ const Footer = styled.footer`
const Note = ({note, onClickDelete}) => {
return (
+ onClickDelete(note)}
+ >
+
+
{note.note}
- onClickDelete(note)}>
- x
-
{note.userName} on {note.timeStamp}
)
@@ -63,14 +67,14 @@ export default class FieldTripNotes extends Component {
_deleteNote = (note) => {
const {deleteFieldTripNote, request} = this.props
- if (confirm(`Are you sure you want to delete note: ${note.note}?`)) {
+ if (confirm(`Are you sure you want to delete note "${note.note}"?`)) {
console.log('OK deleting')
deleteFieldTripNote(request, note.id)
}
}
render () {
- const {expanded, onClickToggle, request} = this.props
+ const {request} = this.props
if (!request) return null
const {
feedback,
@@ -87,9 +91,6 @@ export default class FieldTripNotes extends Component {
Notes/Feedback{' '}
{this._getNotesCount()}
-
- {expanded ? 'Hide' : 'Show'}
-
Internal note
@@ -97,24 +98,22 @@ export default class FieldTripNotes extends Component {
Ops. note
- {expanded &&
- <>
- User feedback
- {feedback && feedback.length > 0
- ? feedback.map((f, i) => )
- : 'No feedback submitted.'
- }
- Internal agent notes
- {internalNotes && internalNotes.length > 0
- ? internalNotes.map(n => )
- : 'No internal notes submitted.'
- }
- Operational notes
- {operationalNotes && operationalNotes.length > 0
- ? operationalNotes.map(n => )
- : 'No operational notes submitted.'
- }
- >
+ User feedback
+ {feedback && feedback.length > 0
+ ? feedback.map((f, i) => )
+ : 'No feedback submitted.'
+ }
+ Internal agent notes
+ {internalNotes && internalNotes.length > 0
+ ? internalNotes.map(n =>
+ )
+ : 'No internal notes submitted.'
+ }
+ Operational notes
+ {operationalNotes && operationalNotes.length > 0
+ ? operationalNotes.map(n =>
+ )
+ : 'No operational notes submitted.'
}
)
diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js
index 02d3d7603..d09e6c30d 100644
--- a/lib/components/admin/styled.js
+++ b/lib/components/admin/styled.js
@@ -26,6 +26,7 @@ export const Header = styled.h4`
`
export const P = styled.p`
+ font-size: 0.9em;
margin-bottom: 0px;
`
diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js
index a0e99a318..f50ab1368 100644
--- a/lib/components/admin/trip-status.js
+++ b/lib/components/admin/trip-status.js
@@ -1,4 +1,3 @@
-import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query'
import { getTimeFormat } from '@opentripplanner/core-utils/lib/time'
import moment from 'moment'
import React, {Component} from 'react'
@@ -6,6 +5,7 @@ import { connect } from 'react-redux'
import * as callTakerActions from '../../actions/call-taker'
import * as formActions from '../../actions/form'
+import Icon from '../narrative/icon'
import {
B,
Button,
@@ -13,88 +13,80 @@ import {
Header,
P
} from './styled'
+import { getTrip } from '../../util/call-taker'
class TripStatus extends Component {
+ _getTrip = () => getTrip(this.props.request, this.props.outbound)
+
_formatTime = (time) => moment(time).format(this.props.timeFormat)
- _formatTripStatus = (tripStatus) => {
- if (!tripStatus) {
+ _formatTripStatus = () => {
+ if (!this._getStatus()) {
return (
No itineraries planned! Click Plan to plan trip.
)
}
+ const trip = this._getTrip()
+ if (!trip) return Error finding trip!
return (
- {JSON.stringify(tripStatus)}
+ {trip.groupItineraries.length} group itineraries, planned by{' '}
+ {trip.createdBy} at {trip.timeStamp}
)
}
- _onPlanTrip = async () => {
- const {outbound, planInbound, planOutbound, request} = this.props
- const trip = this._getTrip()
- if (!trip) {
- // Construct params from request details
- if (outbound) planOutbound(request)
- else planInbound(request)
- } else {
- // Populate params from saved query params
- const params = await planParamsToQueryAsync(JSON.parse(trip.queryParams))
- this.props.setQueryParam(params, trip.id)
- }
- }
-
- _getTrip = () => {
+ _getStatus = () => {
const {outbound, request} = this.props
- if (!request || !request.trips) return null
- return outbound
- ? request.trips[0]
- : request.trips[1]
+ return outbound ? request.outboundTripStatus : request.inboundTripStatus
}
+ _getStatusIcon = () => this._getStatus()
+ ?
+ :
+
+ _onPlanTrip = () => this.props.planTrip(this.props.request, this.props.outbound)
+
+ _onSaveTrip = () => this.props.saveRequestTrip(this.props.request, this.props.outbound)
+
render () {
const {outbound, request} = this.props
const {
arriveDestinationTime,
arriveSchoolTime,
endLocation,
- inboundTripStatus,
leaveDestinationTime,
- outboundTripStatus,
startLocation
} = request
if (!request) {
console.warn('Could not find field trip request')
return null
}
- const status = outbound ? outboundTripStatus : inboundTripStatus
const start = outbound ? startLocation : endLocation
const end = outbound ? endLocation : startLocation
- const trip = this._getTrip()
return (
+ {this._getStatusIcon()}
{outbound ? 'Outbound' : 'Inbound'} trip
Plan
Save
- {status &&
- Clear
- }
From {start} to {end}
{outbound
?
- Arriving at {this._formatTime(arriveDestinationTime)},{' '}
- leave at {this._formatTime(leaveDestinationTime)}
+ Arriving at {this._formatTime(arriveDestinationTime)}
: <>
- From {start} to {end}
- Due back at {this._formatTime(arriveSchoolTime)}
+
+ Leave at {this._formatTime(leaveDestinationTime)},{' '}
+ due back at {this._formatTime(arriveSchoolTime)}
+
>
}
- {this._formatTripStatus(status)}
+ {this._formatTripStatus()}
)
}
@@ -109,8 +101,8 @@ const mapStateToProps = (state, ownProps) => {
}
const mapDispatchToProps = {
- planInbound: callTakerActions.planInbound,
- planOutbound: callTakerActions.planOutbound,
+ planTrip: callTakerActions.planTrip,
+ saveRequestTrip: callTakerActions.saveRequestTrip,
setQueryParam: formActions.setQueryParam
}
diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js
index a36f073c6..c69800b55 100644
--- a/lib/components/admin/updatable.js
+++ b/lib/components/admin/updatable.js
@@ -18,10 +18,10 @@ export default class Updatable extends Component {
}
render () {
- const {fieldName, value} = this.props
+ const {fieldName, label, value} = this.props
return (
<>
- {fieldName}:{' '}
+ {label || fieldName}:{' '}
{value}
Update
diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js
index 055b2a8d0..5a949173f 100644
--- a/lib/components/app/call-taker-panel.js
+++ b/lib/components/app/call-taker-panel.js
@@ -5,6 +5,7 @@ import { Button } from 'react-bootstrap'
import { connect } from 'react-redux'
import * as apiActions from '../../actions/api'
+import * as callTakerActions from '../../actions/call-taker'
import * as formActions from '../../actions/form'
import AddPlaceButton from '../form/add-place-button'
import AdvancedOptions from '../form/call-taker/advanced-options'
@@ -15,6 +16,7 @@ import LocationField from '../form/connected-location-field'
import SwitchButton from '../form/switch-button'
import UserSettings from '../form/user-settings'
import NarrativeItineraries from '../narrative/narrative-itineraries'
+import { getGroupSize } from '../../util/call-taker'
import { hasValidLocation, getActiveSearch, getShowUserSettings } from '../../util/state'
import ViewerContainer from '../viewers/viewer-container'
@@ -91,11 +93,15 @@ class CallTakerPanel extends Component {
}
}
+ _updateGroupSize = (evt) => this.props.setGroupSize(+evt.target.value)
+
render () {
const {
activeSearch,
currentQuery,
+ groupSize,
mainPanelContent,
+ maxGroupSize,
mobile,
modes,
routes,
@@ -206,6 +212,21 @@ class CallTakerPanel extends Component {
+ {groupSize !== null && maxGroupSize &&
+
+ Group size:{' '}
+
+
+ }
@@ -248,12 +269,16 @@ class CallTakerPanel extends Component {
// connect to the redux store
const mapStateToProps = (state, ownProps) => {
+ const {activeId, requests} = state.callTaker.fieldTrip
+ const request = requests.data.find(req => req.id === activeId)
const showUserSettings = getShowUserSettings(state.otp)
return {
activeSearch: getActiveSearch(state.otp),
currentQuery: state.otp.currentQuery,
expandAdvanced: state.otp.user.expandAdvanced,
+ groupSize: state.callTaker.fieldTrip.groupSize,
mainPanelContent: state.otp.ui.mainPanelContent,
+ maxGroupSize: getGroupSize(request),
modes: state.otp.config.modes,
routes: state.otp.transitIndex.routes,
showUserSettings,
@@ -264,6 +289,7 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = {
findRoutes: apiActions.findRoutes,
routingQuery: apiActions.routingQuery,
+ setGroupSize: callTakerActions.setGroupSize,
setQueryParam: formActions.setQueryParam
}
diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js
index 94b7679de..4d26a741a 100644
--- a/lib/reducers/call-taker.js
+++ b/lib/reducers/call-taker.js
@@ -26,6 +26,7 @@ function createCallTakerReducer () {
filter: {
tab: 'new'
},
+ groupSize: null,
position: LOWER_RIGHT_CORNER,
requests: {
status: FETCH_STATUS.UNFETCHED,
@@ -84,9 +85,17 @@ function createCallTakerReducer () {
fieldTrip: { filter: { $merge: action.payload } }
})
}
+ case 'SET_GROUP_SIZE': {
+ return update(state, {
+ fieldTrip: { groupSize: { $set: action.payload } }
+ })
+ }
case 'SET_ACTIVE_FIELD_TRIP': {
return update(state, {
- fieldTrip: { activeId: { $set: action.payload } }
+ fieldTrip: {
+ activeId: { $set: action.payload },
+ groupSize: { $set: null }
+ }
})
}
case 'RECEIVED_FIELD_TRIP_DETAILS': {
diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js
index 34ca52d40..0ba06def1 100644
--- a/lib/util/call-taker.js
+++ b/lib/util/call-taker.js
@@ -1,3 +1,5 @@
+import {isTransit} from '@opentripplanner/core-utils/lib/itinerary'
+
import {getRoutingParams} from '../actions/api'
function placeToLatLonStr (place) {
@@ -23,7 +25,68 @@ export function searchToQuery (search, call, otpConfig) {
export function getGroupSize (request) {
let groupSize = 0
- if (request.numStudents) groupSize += request.numStudents
- if (request.numChaperones) groupSize += request.numChaperones
+ if (request && request.numStudents) groupSize += request.numStudents
+ if (request && request.numChaperones) groupSize += request.numChaperones
return groupSize
}
+
+export function getTrip (request, outbound = false) {
+ if (!request || !request.trips) return null
+ let trip
+ request.trips.forEach(t => {
+ const tripIsOutbound = t.requestOrder === 0
+ if (outbound && tripIsOutbound) trip = t
+ else if (!outbound && !tripIsOutbound) trip = t
+ })
+ return trip
+}
+
+export function createTripPlan (planData, queryParams) {
+ const tripPlan = {
+ earliestStartTime: null,
+ latestEndTime: null,
+ planData,
+ queryParams,
+ itineraries: []
+ }
+ if (!planData) return
+ for (let i = 0; i < tripPlan.planData.itineraries.length; i++) {
+ const itinData = tripPlan.planData.itineraries[i]
+ tripPlan.itineraries.push(createItinerary(itinData, tripPlan))
+ }
+ const timeBounds = calculateTimeBounds(tripPlan.itineraries)
+ return {...tripPlan, ...timeBounds}
+}
+
+function calculateTimeBounds (itineraries) {
+ let earliestStartTime = null
+ let latestEndTime = null
+ for (var i = 0; i < itineraries.length; i++) {
+ var itin = itineraries[i]
+ earliestStartTime = (earliestStartTime == null || itin.getStartTime() < earliestStartTime)
+ ? itin.getStartTime()
+ : earliestStartTime
+ latestEndTime = (latestEndTime == null || itin.getEndTime() > latestEndTime)
+ ? itin.getEndTime()
+ : latestEndTime
+ }
+}
+
+function createItinerary (itinData, tripPlan) {
+ const itin = {
+ itinData,
+ tripPlan,
+ firstStopIds: [],
+ hasTransit: false,
+ totalWalk: 0
+ }
+ for (let l = 0; l < itinData.legs.length; l++) {
+ var leg = itinData.legs[l]
+ if (isTransit(leg.mode)) {
+ itin.hasTransit = true
+ itin.firstStopIDs.push(leg.from.stopId)
+ }
+ if (leg.mode === 'WALK') itin.totalWalk += leg.distance
+ }
+ return itin
+}