diff --git a/example.js b/example.js
index 9c42dafe0..e3bb59688 100644
--- a/example.js
+++ b/example.js
@@ -20,6 +20,7 @@ import {
CallTakerWindows,
DefaultItinerary,
DefaultMainPanel,
+ FieldTripWindows,
MobileSearchScreen,
ResponsiveWebapp,
createCallTakerReducer,
@@ -89,7 +90,12 @@ const components = {
: isBatchRoutingEnabled
? BatchRoutingPanel
: DefaultMainPanel,
- MapWindows: isCallTakerModuleEnabled ? CallTakerWindows : null,
+ MapWindows: isCallTakerModuleEnabled
+ ? () => <>
+
+
+ >
+ : null,
MobileSearchScreen: isBatchRoutingEnabled
? BatchSearchScreen
: MobileSearchScreen,
diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js
index 25ccb2ae7..5a48b8284 100644
--- a/lib/actions/call-taker.js
+++ b/lib/actions/call-taker.js
@@ -1,8 +1,9 @@
import { getUrlParams } from '@opentripplanner/core-utils/lib/query'
+import { serialize } from 'object-to-formdata'
import qs from 'qs'
import { createAction } from 'redux-actions'
-import {searchToQuery} from '../util/call-taker'
+import {searchToQuery, sessionIsInvalid} from '../util/call-taker'
import {URL_ROOT} from '../util/constants'
import {getTimestamp} from '../util/state'
@@ -29,12 +30,16 @@ export function endCall () {
return function (dispatch, getState) {
const {callTaker, otp} = getState()
const {activeCall, session} = callTaker
+ const { sessionId } = session
if (sessionIsInvalid(session)) return
- // Make POST request to store call.
- const callData = new FormData()
- callData.append('sessionId', session.sessionId)
- callData.append('call.startTime', activeCall.startTime)
- callData.append('call.endTime', getTimestamp())
+ // Make POST request to store new call.
+ const callData = serialize({
+ sessionId,
+ call: {
+ startTime: activeCall.startTime,
+ endTime: getTimestamp()
+ }
+ })
fetch(`${otp.config.datastoreUrl}/calltaker/call`,
{method: 'POST', body: callData}
)
@@ -84,9 +89,8 @@ function newSession (datastoreUrl, verifyLoginUrl, redirect) {
.then(res => res.json())
.then(data => {
const {sessionId: session} = data
- console.log('newSession success: ' + session)
const windowUrl = `${verifyLoginUrl}?${qs.stringify({session, redirect})}`
- console.log('redirecting to: ' + windowUrl)
+ // Redirect to login url.
window.location = windowUrl
})
.catch(error => {
@@ -104,8 +108,8 @@ function checkSession (datastoreUrl, sessionId) {
.then(res => res.json())
.then(session => dispatch(storeSession({session})))
.catch(error => {
- console.error('checkSession error', error)
dispatch(storeSession({session: null}))
+ alert(`Error establishing auth session: ${JSON.stringify(error)}`)
})
}
}
@@ -120,28 +124,12 @@ export function fetchCalls () {
if (sessionIsInvalid(callTaker.session)) return
const {datastoreUrl} = otp.config
const {sessionId} = callTaker.session
- const limit = 10
+ const limit = 30
fetch(`${datastoreUrl}/calltaker/call?${qs.stringify({limit, sessionId})}`)
.then(res => res.json())
- .then(calls => {
- console.log('GET calls response', calls)
- dispatch(receivedCalls({calls}))
- })
- .catch(err => {
- alert(`Could not fetch calls: ${JSON.stringify(err)}`)
- })
- }
-}
-
-/**
- * @return {boolean} - whether a calltaker session is invalid
- */
-function sessionIsInvalid (session) {
- if (!session || !session.sessionId) {
- console.error('No valid OTP datastore session found.')
- return true
+ .then(calls => dispatch(receivedCalls({calls})))
+ .catch(err => alert(`Error fetching calls: ${JSON.stringify(err)}`))
}
- return false
}
/**
@@ -161,19 +149,13 @@ export function saveQueriesForCall (call) {
const search = otp.searches[searchId]
const query = searchToQuery(search, call, otp.config)
const {sessionId} = callTaker.session
- const queryData = new FormData()
- queryData.append('sessionId', sessionId)
- queryData.append('query.queryParams', query.queryParams)
- queryData.append('query.fromPlace', query.fromPlace)
- queryData.append('query.toPlace', query.toPlace)
- queryData.append('query.timeStamp', query.timeStamp)
- queryData.append('query.call.id', call.id)
+ const queryData = serialize({ sessionId, query })
return fetch(`${datastoreUrl}/calltaker/callQuery?sessionId=${sessionId}`,
{method: 'POST', body: queryData}
)
.then(res => res.json())
.catch(err => {
- alert(`Could not fetch calls: ${JSON.stringify(err)}`)
+ alert(`Error storing call queries: ${JSON.stringify(err)}`)
})
}))
}
@@ -195,7 +177,7 @@ export function fetchQueries (callId) {
dispatch(receivedQueries({callId, queries}))
})
.catch(err => {
- alert(`Could not fetch calls: ${JSON.stringify(err)}`)
+ alert(`Error fetching queries: ${JSON.stringify(err)}`)
})
}
}
diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js
new file mode 100644
index 000000000..69d072347
--- /dev/null
+++ b/lib/actions/field-trip.js
@@ -0,0 +1,322 @@
+import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query'
+import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time'
+import moment from 'moment'
+import { serialize } from 'object-to-formdata'
+import qs from 'qs'
+import { createAction } from 'redux-actions'
+
+import {routingQuery} from './api'
+import {setQueryParam} from './form'
+import {getGroupSize, getTripFromRequest, sessionIsInvalid} from '../util/call-taker'
+
+if (typeof (fetch) === 'undefined') require('isomorphic-fetch')
+
+/// PRIVATE ACTIONS
+
+const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS')
+const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS')
+const receivedFieldTripDetails = createAction('RECEIVED_FIELD_TRIP_DETAILS')
+const requestingFieldTripDetails = createAction('REQUESTING_FIELD_TRIP_DETAILS')
+
+// PUBLIC ACTIONS
+
+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 toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS')
+
+/**
+ * Fetch all field trip requests (as summaries).
+ */
+export function fetchFieldTrips () {
+ return async function (dispatch, getState) {
+ dispatch(requestingFieldTrips())
+ const {callTaker, otp} = getState()
+ if (sessionIsInvalid(callTaker.session)) return
+ const {datastoreUrl} = otp.config
+ const {sessionId} = callTaker.session
+ let fieldTrips = []
+ try {
+ const res = await fetch(`${datastoreUrl}/fieldtrip/getRequestsSummary?${qs.stringify({sessionId})}`)
+ fieldTrips = await res.json()
+ } catch (e) {
+ alert(`Error fetching field trips: ${JSON.stringify(e)}`)
+ }
+ dispatch(receivedFieldTrips({fieldTrips}))
+ }
+}
+
+/**
+ * Fetch details for a particular field trip request.
+ */
+export function fetchFieldTripDetails (requestId) {
+ return function (dispatch, getState) {
+ dispatch(requestingFieldTripDetails())
+ const {callTaker, otp} = getState()
+ if (sessionIsInvalid(callTaker.session)) return
+ const {datastoreUrl} = otp.config
+ const {sessionId} = callTaker.session
+ fetch(`${datastoreUrl}/fieldtrip/getRequest?${qs.stringify({requestId, sessionId})}`)
+ .then(res => res.json())
+ .then(fieldTrip => dispatch(receivedFieldTripDetails({fieldTrip})))
+ .catch(err => {
+ alert(`Error fetching field trips: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
+/**
+ * Add note for field trip request.
+ */
+export function addFieldTripNote (request, note) {
+ return function (dispatch, getState) {
+ const {callTaker, otp} = getState()
+ const {datastoreUrl} = otp.config
+ if (sessionIsInvalid(callTaker.session)) return
+ const {sessionId, username} = callTaker.session
+ const noteData = serialize({
+ sessionId,
+ note: {...note, userName: username},
+ requestId: request.id
+ })
+ return fetch(`${datastoreUrl}/fieldtrip/addNote`,
+ {method: 'POST', body: noteData}
+ )
+ .then(() => dispatch(fetchFieldTripDetails(request.id)))
+ .catch(err => {
+ alert(`Error adding field trip note: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
+/**
+ * Delete a specific note for a field trip request.
+ */
+export function deleteFieldTripNote (request, noteId) {
+ return function (dispatch, getState) {
+ const {callTaker, otp} = getState()
+ const {datastoreUrl} = otp.config
+ if (sessionIsInvalid(callTaker.session)) return
+ const {sessionId} = callTaker.session
+ return fetch(`${datastoreUrl}/fieldtrip/deleteNote`,
+ {method: 'POST', body: serialize({ noteId, sessionId })}
+ )
+ .then(() => dispatch(fetchFieldTripDetails(request.id)))
+ .catch(err => {
+ alert(`Error deleting field trip note: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
+/**
+ * Edit teacher (AKA submitter) notes for a field trip request.
+ */
+export function editSubmitterNotes (request, submitterNotes) {
+ return function (dispatch, getState) {
+ const {callTaker, otp} = getState()
+ const {datastoreUrl} = otp.config
+ if (sessionIsInvalid(callTaker.session)) return
+ const {sessionId} = callTaker.session
+ const noteData = serialize({
+ notes: submitterNotes,
+ requestId: request.id,
+ sessionId
+ })
+ return fetch(`${datastoreUrl}/fieldtrip/editSubmitterNotes`,
+ {method: 'POST', body: noteData}
+ )
+ .then(() => dispatch(fetchFieldTripDetails(request.id)))
+ .catch(err => {
+ alert(`Error editing submitter notes: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
+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 = getTripFromRequest(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
+ }
+ alert(`TODO: Save trip in request order ${requestOrder}!`)
+ // TODO: Enable saveTrip
+ // dispatch(saveTrip(request, requestOrder))
+ }
+}
+
+/**
+ * @typedef {Object} ValidationCheck
+ * @property {boolean} isValid - Whether the check is valid
+ * @property {string} message - The message explaining why the check returned
+ * invalid.
+ */
+
+/**
+ * Checks that a group plan is valid for a given request, i.e., that it occurs
+ * on the requested travel date.
+ * @param request field trip request
+ * @param groupPlan the group plan to check
+ * @return {ValidationCheck}
+ */
+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, message: null }
+}
+
+export function planTrip (request, outbound) {
+ return async function (dispatch, getState) {
+ dispatch(setGroupSize(getGroupSize(request)))
+ const trip = getTripFromRequest(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
+ // clearTrip()
+ const locations = await planParamsToQueryAsync({
+ fromPlace: request.startLocation,
+ toPlace: request.endLocation
+ }, config)
+ const queryParams = {
+ date: moment(request.travelDate).format(OTP_API_DATE_FORMAT),
+ departArrive: 'ARRIVE',
+ groupSize: getGroupSize(request),
+ time: moment(request.arriveDestinationTime).format(OTP_API_TIME_FORMAT),
+ ...locations
+ }
+ dispatch(setQueryParam(queryParams))
+ dispatch(routingQuery())
+ }
+}
+
+function planInbound (request) {
+ return async function (dispatch, getState) {
+ const {config} = getState().otp
+ const locations = await planParamsToQueryAsync({
+ fromPlace: request.endLocation,
+ toPlace: request.startLocation
+ }, config)
+ // clearTrip()
+ const queryParams = {
+ date: moment(request.travelDate).format(OTP_API_DATE_FORMAT),
+ departArrive: 'DEPART',
+ groupSize: getGroupSize(request),
+ time: moment(request.leaveDestinationTime).format(OTP_API_TIME_FORMAT),
+ ...locations
+ }
+ dispatch(setQueryParam(queryParams))
+ dispatch(routingQuery())
+ }
+}
+
+/**
+ * Set group size for a field trip request. Group size consists of numStudents,
+ * numFreeStudents, and numChaperones.
+ */
+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 groupSizeData = serialize({
+ ...groupSize,
+ requestId: request.id,
+ sessionId
+ })
+ return fetch(`${datastoreUrl}/fieldtrip/setRequestGroupSize`,
+ {method: 'POST', body: groupSizeData}
+ )
+ .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 paymentData = serialize({
+ ...paymentInfo,
+ requestId: request.id,
+ sessionId
+ })
+ return fetch(`${datastoreUrl}/fieldtrip/setRequestPaymentInfo`,
+ {method: 'POST', body: paymentData}
+ )
+ .then(() => dispatch(fetchFieldTripDetails(request.id)))
+ .catch(err => {
+ alert(`Error setting payment info: ${JSON.stringify(err)}`)
+ })
+ }
+}
+
+/**
+ * Set field trip request status (e.g., cancelled).
+ */
+export function setRequestStatus (request, status) {
+ return function (dispatch, getState) {
+ const {callTaker, otp} = getState()
+ const {datastoreUrl} = otp.config
+ if (sessionIsInvalid(callTaker.session)) return
+ const {sessionId} = callTaker.session
+ const statusData = serialize({
+ requestId: request.id,
+ sessionId,
+ status
+ })
+ return fetch(`${datastoreUrl}/fieldtrip/setRequestStatus`,
+ {method: 'POST', body: statusData}
+ )
+ .then(() => dispatch(fetchFieldTripDetails(request.id)))
+ .catch(err => {
+ alert(`Error setting request status: ${JSON.stringify(err)}`)
+ })
+ }
+}
diff --git a/lib/components/admin/call-record.js b/lib/components/admin/call-record.js
index 602599109..ff8ae8ab4 100644
--- a/lib/components/admin/call-record.js
+++ b/lib/components/admin/call-record.js
@@ -38,7 +38,7 @@ export default class CallRecord extends Component {
style={{color: RED, fontSize: '8px', verticalAlign: '2px'}}
type='circle'
className='animate-flicker' />
-
+
{' '}
[Active call]
diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js
index 56e4b39c6..a5f4ff803 100644
--- a/lib/components/admin/call-taker-controls.js
+++ b/lib/components/admin/call-taker-controls.js
@@ -1,30 +1,39 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
+import * as apiActions from '../../actions/api'
import * as callTakerActions from '../../actions/call-taker'
-import { routingQuery } from '../../actions/api'
-import { setMainPanelContent } from '../../actions/ui'
-import CallTimeCounter from './call-time-counter'
+import * as fieldTripActions from '../../actions/field-trip'
+import * as uiActions from '../../actions/ui'
import Icon from '../narrative/icon'
-
-const RED = '#C35134'
-const BLUE = '#1C4D89'
-const GREEN = '#6B931B'
+import {
+ CallHistoryButton,
+ CallTimeCounter,
+ ControlsContainer,
+ FieldTripsButton,
+ ToggleCallButton
+} from './styled'
/**
* This component displays the controls for the Call Taker/Field Trip modules,
* including:
* - start/end call button
* - view call list
- * TODO
* - view field trip list
*/
class CallTakerControls extends Component {
componentDidUpdate (prevProps) {
- const {session} = this.props
+ const {
+ callTakerEnabled,
+ fetchCalls,
+ fetchFieldTrips,
+ fieldTripEnabled,
+ session
+ } = this.props
// Once session is available, fetch calls.
if (session && !prevProps.session) {
- this.props.fetchCalls()
+ if (callTakerEnabled) fetchCalls()
+ if (fieldTripEnabled) fetchFieldTrips()
}
}
@@ -33,7 +42,7 @@ class CallTakerControls extends Component {
else this.props.beginCall()
}
- _renderCallButton = () => {
+ _renderCallButtonIcon = () => {
// Show stop button if call not in progress.
if (this._callInProgress()) {
return (
@@ -57,74 +66,57 @@ class CallTakerControls extends Component {
_onToggleCallHistory = () => this.props.toggleCallHistory()
+ _onToggleFieldTrips = () => this.props.toggleFieldTrips()
+
_callInProgress = () => Boolean(this.props.activeCall)
render () {
- const {session} = this.props
+ const {callTakerEnabled, fieldTripEnabled, session} = this.props
// If no valid session is found, do not show calltaker controls.
if (!session) return null
- // FIXME: styled component
- const circleButtonStyle = {
- position: 'absolute',
- zIndex: 999999,
- color: 'white',
- borderRadius: '50%',
- border: 'none',
- boxShadow: '2px 2px 4px #000000'
- }
return (
- <>
+
{/* Start/End Call button */}
-
+ {callTakerEnabled &&
+
+ {this._renderCallButtonIcon()}
+
+ }
{this._callInProgress()
- ?
+ ?
: null
}
{/* Call History toggle button */}
-
- {/* Field Trip toggle button TODO */}
- >
+ {callTakerEnabled &&
+
+
+
+ }
+ {/* Field Trip toggle button */}
+ {fieldTripEnabled &&
+
+
+
+ }
+
)
}
}
@@ -132,19 +124,21 @@ class CallTakerControls extends Component {
const mapStateToProps = (state, ownProps) => {
return {
activeCall: state.callTaker.activeCall,
+ callTakerEnabled: Boolean(state.otp.config.modules.find(m => m.id === 'call')),
+ fieldTripEnabled: Boolean(state.otp.config.modules.find(m => m.id === 'ft')),
session: state.callTaker.session
}
}
-const {beginCall, endCall, fetchCalls, toggleCallHistory} = callTakerActions
-
const mapDispatchToProps = {
- beginCall,
- endCall,
- fetchCalls,
- routingQuery,
- setMainPanelContent,
- toggleCallHistory
+ beginCall: callTakerActions.beginCall,
+ endCall: callTakerActions.endCall,
+ fetchCalls: callTakerActions.fetchCalls,
+ fetchFieldTrips: fieldTripActions.fetchFieldTrips,
+ routingQuery: apiActions.routingQuery,
+ setMainPanelContent: uiActions.setMainPanelContent,
+ toggleCallHistory: callTakerActions.toggleCallHistory,
+ toggleFieldTrips: fieldTripActions.toggleFieldTrips
}
export default connect(mapStateToProps, mapDispatchToProps)(CallTakerControls)
diff --git a/lib/components/admin/call-taker-windows.js b/lib/components/admin/call-taker-windows.js
index b575cc2ca..a169c2a09 100644
--- a/lib/components/admin/call-taker-windows.js
+++ b/lib/components/admin/call-taker-windows.js
@@ -5,6 +5,7 @@ import * as callTakerActions from '../../actions/call-taker'
import CallRecord from './call-record'
import DraggableWindow from './draggable-window'
import Icon from '../narrative/icon'
+import {WindowHeader} from './styled'
/**
* Collects the various draggable windows used in the Call Taker module to
@@ -12,40 +13,33 @@ import Icon from '../narrative/icon'
*/
class CallTakerWindows extends Component {
render () {
- const {callTaker, fetchQueries, searches} = this.props
+ const {callTaker, fetchQueries, searches, toggleCallHistory} = this.props
const {activeCall, callHistory} = callTaker
+ if (!callHistory.visible) return null
return (
- <>
- {callHistory.visible
- // Active call window
- ? Call history}
- onClickClose={this.props.toggleCallHistory}
- >
- {activeCall
- ?
- : null
- }
- {callHistory.calls.data.length > 0
- ? callHistory.calls.data.map((call, i) => (
-
- ))
- : No calls in history
- }
-
+ Call history}
+ onClickClose={toggleCallHistory}
+ style={{right: '15px', top: '50px'}}
+ >
+ {activeCall
+ ?
: null
}
- >
+ {callHistory.calls.data.length > 0
+ ? callHistory.calls.data.map((call, i) => (
+
+ ))
+ : No calls in history
+ }
+
)
}
}
@@ -58,11 +52,9 @@ const mapStateToProps = (state, ownProps) => {
}
}
-const {
- fetchQueries,
- toggleCallHistory
-} = callTakerActions
-
-const mapDispatchToProps = { fetchQueries, toggleCallHistory }
+const mapDispatchToProps = {
+ fetchQueries: callTakerActions.fetchQueries,
+ toggleCallHistory: callTakerActions.toggleCallHistory
+}
export default connect(mapStateToProps, mapDispatchToProps)(CallTakerWindows)
diff --git a/lib/components/admin/call-time-counter.js b/lib/components/admin/call-time-counter.js
index 1d8dfe763..1383687a4 100644
--- a/lib/components/admin/call-time-counter.js
+++ b/lib/components/admin/call-time-counter.js
@@ -43,9 +43,14 @@ export default class CallTimeCounter extends Component {
}
render () {
- const {className, style} = this.props
+ const {className} = this.props
return (
-
+
{this._formatSeconds(this.state.counterString)}
)
diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js
index 1159cdd39..a43789139 100644
--- a/lib/components/admin/draggable-window.js
+++ b/lib/components/admin/draggable-window.js
@@ -7,30 +7,34 @@ const noop = () => {}
export default class DraggableWindow extends Component {
render () {
- const {children, draggableProps, header} = this.props
+ const {
+ children,
+ draggableProps,
+ footer,
+ header,
+ height = '245px',
+ onClickClose,
+ style
+ } = this.props
const GREY_BORDER = '#777 1.3px solid'
return (
{header}
{children}
+ {footer &&
+
+ {footer}
+
+ }
)
diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js
new file mode 100644
index 000000000..e5b8cf27f
--- /dev/null
+++ b/lib/components/admin/editable-section.js
@@ -0,0 +1,147 @@
+import { Field, Form, Formik } from 'formik'
+import React, {Component} from 'react'
+
+import {
+ Button,
+ Para,
+ Val
+} from './styled'
+
+/**
+ * This component will render a set of fields along with a 'Change/Edit' button
+ * that toggles an editable state. In its editable state, the fields/values are
+ * rendered as form inputs, which can be edited and then saved/persisted.
+ */
+export default class EditableSection extends Component {
+ state = {
+ isEditing: false
+ }
+
+ _exists = (val) => val !== null && typeof val !== 'undefined'
+
+ _getVal = (fieldName) => this._exists(this.props.request[fieldName])
+ ? this.props.request[fieldName]
+ : ''
+
+ _onClickSave = data => {
+ const {request, onChange} = this.props
+ // Convert all null values received to '',
+ // otherwise they will appear in the backend as the 'null' string.
+ for (const field in data) {
+ if (data[field] === null) data[field] = ''
+ }
+
+ onChange(request, data)
+ this.setState({isEditing: false})
+ }
+
+ _toggleEditing = () => {
+ this.setState({isEditing: !this.state.isEditing})
+ }
+
+ render () {
+ const {children, fields, inputStyle, request, valueFirst} = this.props
+ const {isEditing} = this.state
+ if (!request) return null
+ return (
+
+ {
+ formikProps => (
+
+ )
+ }
+
+ )
+ }
+}
+
+/**
+ * This component renders either the specified value for a given field or, if
+ * in an active editing state, the associated field's respective input (e.g.
+ * text, number, or select).
+ */
+class InputToggle extends Component {
+ render () {
+ const {inputProps, fieldName, isEditing, options, style, value} = this.props
+ if (isEditing) {
+ if (options) {
+ return (
+
+ {Object.keys(options).map(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
new file mode 100644
index 000000000..91d2f2af6
--- /dev/null
+++ b/lib/components/admin/field-trip-details.js
@@ -0,0 +1,233 @@
+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 styled from 'styled-components'
+
+import * as fieldTripActions from '../../actions/field-trip'
+import DraggableWindow from './draggable-window'
+import EditableSection from './editable-section'
+import FieldTripNotes from './field-trip-notes'
+import Icon from '../narrative/icon'
+import {
+ Bold,
+ Button,
+ Container,
+ FullWithMargin,
+ Half,
+ Header,
+ InlineHeader,
+ Para,
+ Text,
+ Val,
+ WindowHeader as DefaultWindowHeader
+} from './styled'
+import TripStatus from './trip-status'
+import Updatable from './updatable'
+import {
+ getGroupSize,
+ GROUP_FIELDS,
+ PAYMENT_FIELDS,
+ TICKET_TYPES
+} from '../../util/call-taker'
+
+const WindowHeader = styled(DefaultWindowHeader)`
+ margin-bottom: 0px;
+`
+
+/**
+ * Shows the details for the active Field Trip Request.
+ */
+class FieldTripDetails extends Component {
+ _editSubmitterNotes = (val) => this.props.editSubmitterNotes(this.props.request, val)
+
+ _getRequestLink = (path, isPublic = false) =>
+ `${this.props.datastoreUrl}/${isPublic ? 'public/' : ''}fieldtrip/${path}?requestId=${this.props.request.id}`
+
+ _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null)
+
+ _onToggleStatus = () => {
+ const {request, setRequestStatus} = this.props
+ if (request.status !== 'cancelled') {
+ if (confirm('Are you sure you want to cancel this request? Any associated trips will be deleted.')) {
+ setRequestStatus(request, 'cancelled')
+ }
+ } else {
+ setRequestStatus(request, 'active')
+ }
+ }
+
+ _renderFooter = () => {
+ const cancelled = this.props.request.status === 'cancelled'
+ return (
+
+
+
+
+ )
+ }
+
+ _renderHeader = () => {
+ const {dateFormat, request} = this.props
+ const {
+ id,
+ schoolName,
+ travelDate
+ } = request
+ const travelDateAsMoment = moment(travelDate)
+ return (
+
+ {schoolName} Trip (#{id})
+
+
+ Travel date: {travelDateAsMoment.format(dateFormat)}{' '}
+ ({travelDateAsMoment.fromNow()})
+
+
+
+ )
+ }
+
+ render () {
+ const {
+ addFieldTripNote,
+ deleteFieldTripNote,
+ request,
+ setRequestGroupSize,
+ setRequestPaymentInfo,
+ style
+ } = this.props
+ if (!request) return null
+ const {
+ invoiceRequired,
+ notes,
+ schoolName,
+ submitterNotes,
+ teacherName,
+ ticketType
+ } = request
+ const internalNotes = []
+ const operationalNotes = []
+ notes && notes.forEach(note => {
+ if (note.type === 'internal') internalNotes.push(note)
+ else operationalNotes.push(note)
+ })
+ return (
+
+
+
+
+ {schoolName}
+ Teacher: {teacherName}
+ Ticket type: {TICKET_TYPES[ticketType]}
+ Invoice required: {invoiceRequired ? 'Yes' : 'No'}
+
+ }
+ onUpdate={this._editSubmitterNotes}
+ value={submitterNotes}
+ />
+
+
+
+
+ {getGroupSize(request)} total
+
+ }
+ fields={GROUP_FIELDS}
+ inputStyle={{lineHeight: '0.8em', padding: '0px', width: '50px'}}
+ onChange={setRequestGroupSize}
+ request={request}
+ valueFirst
+ />
+
+
+
+
+
+ Payment information
+
+ }
+ fields={PAYMENT_FIELDS}
+ inputStyle={{lineHeight: '0.8em', padding: '0px', width: '100px'}}
+ onChange={setRequestPaymentInfo}
+ request={request}
+ />
+
+
+
+
+ )
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ const {activeId, requests} = state.callTaker.fieldTrip
+ const request = requests.data.find(req => req.id === activeId)
+ return {
+ currentQuery: state.otp.currentQuery,
+ datastoreUrl: state.otp.config.datastoreUrl,
+ dateFormat: getDateFormat(state.otp.config),
+ request
+ }
+}
+
+const mapDispatchToProps = {
+ addFieldTripNote: fieldTripActions.addFieldTripNote,
+ deleteFieldTripNote: fieldTripActions.deleteFieldTripNote,
+ editSubmitterNotes: fieldTripActions.editSubmitterNotes,
+ fetchQueries: fieldTripActions.fetchQueries,
+ setActiveFieldTrip: fieldTripActions.setActiveFieldTrip,
+ setRequestGroupSize: fieldTripActions.setRequestGroupSize,
+ setRequestPaymentInfo: fieldTripActions.setRequestPaymentInfo,
+ setRequestStatus: fieldTripActions.setRequestStatus,
+ toggleFieldTrips: fieldTripActions.toggleFieldTrips
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(FieldTripDetails)
diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js
new file mode 100644
index 000000000..84152aa2d
--- /dev/null
+++ b/lib/components/admin/field-trip-list.js
@@ -0,0 +1,206 @@
+import React, { Component } from 'react'
+import { Badge } from 'react-bootstrap'
+import { connect } from 'react-redux'
+
+import * as fieldTripActions from '../../actions/field-trip'
+import DraggableWindow from './draggable-window'
+import Icon from '../narrative/icon'
+import Loading from '../narrative/loading'
+import {FieldTripRecordButton, WindowHeader} from './styled'
+import {getVisibleRequests, TABS} from '../../util/call-taker'
+import {FETCH_STATUS} from '../../util/constants'
+
+/**
+ * Displays a searchable list of field trip requests in a draggable window.
+ */
+class FieldTripList extends Component {
+ _onClickFieldTrip = (request) => {
+ const {callTaker, fetchFieldTripDetails, setActiveFieldTrip} = this.props
+ if (request.id === callTaker.fieldTrip.activeId) {
+ this._onCloseActiveFieldTrip()
+ } else {
+ setActiveFieldTrip(request.id)
+ fetchFieldTripDetails(request.id)
+ }
+ }
+
+ _onClickRefresh = () => this.props.fetchFieldTrips()
+
+ _onCloseActiveFieldTrip = () => {
+ this.props.setActiveFieldTrip(null)
+ }
+
+ /**
+ * Change search input selectively. This is to prevent excessive rendering
+ * each time the search input changes (on TriMet's production instance there
+ * are thousands of field trip requests).
+ */
+ _handleSearchKeyUp = e => {
+ const {callTaker, setFieldTripFilter} = this.props
+ const {search} = callTaker.fieldTrip.filter
+ const newSearch = e.target.value
+ // Update filter if Enter is pressed or search value is entirely cleared.
+ const newSearchEntered = e.keyCode === 13 && newSearch !== search
+ const searchCleared = search && !newSearch
+ if (newSearchEntered || searchCleared) {
+ setFieldTripFilter({search: newSearch})
+ }
+ }
+
+ _onTabChange = e => {
+ this.props.setFieldTripFilter({tab: e.currentTarget.name})
+ }
+
+ render () {
+ const {callTaker, style, toggleFieldTrips, visibleRequests} = this.props
+ const {fieldTrip} = callTaker
+ const {activeId, filter} = fieldTrip
+ const {search} = filter
+ return (
+
+
+ Field Trip Requests{' '}
+
+
+
+
+
+ {TABS.map(tab => {
+ const active = tab.id === filter.tab
+ const style = {
+ backgroundColor: active ? 'navy' : undefined,
+ borderRadius: 5,
+ color: active ? 'white' : undefined,
+ padding: '2px 3px'
+ }
+ const requestCount = fieldTrip.requests.data.filter(tab.filter).length
+ return (
+
+ )
+ })}
+ >
+ }
+ onClickClose={toggleFieldTrips}
+ style={style}
+ >
+ {fieldTrip.requests.status === FETCH_STATUS.FETCHING
+ ?
+ : visibleRequests.length > 0
+ ? visibleRequests.map((request, i) => (
+
+ ))
+ : No field trips found.
+ }
+
+ )
+ }
+}
+
+class FieldTripRequestRecord extends Component {
+ _onClick = () => {
+ const {onClick, request} = this.props
+ onClick(request)
+ }
+
+ _getStatusIcon = (status) => status
+ ?
+ :
+
+ render () {
+ const {active, request} = this.props
+ const style = {
+ backgroundColor: active ? 'lightgrey' : undefined,
+ borderBottom: '1px solid grey'
+ }
+ const {
+ endLocation,
+ id,
+ inboundTripStatus,
+ outboundTripStatus,
+ schoolName,
+ startLocation,
+ teacherName,
+ timeStamp
+ } = request
+ return (
+
+
+
+ {schoolName} Trip (#{id})
+
+
+
+ {this._getStatusIcon(inboundTripStatus)} Inbound
+
+
+ {this._getStatusIcon(outboundTripStatus)} Outbound
+
+
+
+ Submitted by {teacherName} on {timeStamp}
+
+
+ {startLocation} to {endLocation}
+
+
+
+ )
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ callTaker: state.callTaker,
+ currentQuery: state.otp.currentQuery,
+ searches: state.otp.searches,
+ visibleRequests: getVisibleRequests(state)
+ }
+}
+
+const mapDispatchToProps = {
+ fetchFieldTripDetails: fieldTripActions.fetchFieldTripDetails,
+ fetchFieldTrips: fieldTripActions.fetchFieldTrips,
+ setActiveFieldTrip: fieldTripActions.setActiveFieldTrip,
+ setFieldTripFilter: fieldTripActions.setFieldTripFilter,
+ toggleFieldTrips: fieldTripActions.toggleFieldTrips
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(FieldTripList)
diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js
new file mode 100644
index 000000000..a190f5f5d
--- /dev/null
+++ b/lib/components/admin/field-trip-notes.js
@@ -0,0 +1,116 @@
+import React, { Component } from 'react'
+import { Badge, Button as BsButton } from 'react-bootstrap'
+import styled from 'styled-components'
+
+import Icon from '../narrative/icon'
+import {
+ Button,
+ Full,
+ Header
+} from './styled'
+
+const Quote = styled.p`
+ font-size: small;
+ margin-bottom: 5px;
+`
+
+const Footer = styled.footer`
+ font-size: x-small;
+`
+
+const Note = ({note, onClickDelete}) => (
+
+ onClickDelete(note)}
+ >
+
+
+ {note.note}
+
+
+)
+
+const Feedback = ({feedback}) => (
+
+ {feedback.feedback}
+
+
+)
+/**
+ * Renders the various notes/feedback for a field trip request.
+ */
+export default class FieldTripNotes extends Component {
+ _getNotesCount = () => {
+ const {request} = this.props
+ const {feedback, notes} = request
+ let notesCount = 0
+ if (notes && notes.length) notesCount += notes.length
+ if (feedback && feedback.length) notesCount += feedback.length
+ return notesCount
+ }
+
+ _addInternalNote = () => this._addNote('internal')
+
+ _addOperationalNote = () => this._addNote('operational')
+
+ _addNote = (type) => {
+ const {addFieldTripNote, request} = this.props
+ const note = prompt(`Type ${type} note to be attached to this request:`)
+ if (note) addFieldTripNote(request, {note, type})
+ }
+
+ _deleteNote = (note) => {
+ const {deleteFieldTripNote, request} = this.props
+ if (confirm(`Are you sure you want to delete note "${note.note}"?`)) {
+ deleteFieldTripNote(request, note.id)
+ }
+ }
+
+ render () {
+ const {request} = this.props
+ if (!request) return null
+ const {
+ feedback,
+ notes
+ } = request
+ const internalNotes = []
+ const operationalNotes = []
+ notes && notes.forEach(note => {
+ if (note.type === 'internal') internalNotes.push(note)
+ else operationalNotes.push(note)
+ })
+ return (
+
+
+ Notes/Feedback{' '}
+ {this._getNotesCount()}
+
+
+
+ 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.'
+ }
+
+ )
+ }
+}
diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js
new file mode 100644
index 000000000..ddb6ee7ca
--- /dev/null
+++ b/lib/components/admin/field-trip-windows.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import { connect } from 'react-redux'
+
+import FieldTripDetails from './field-trip-details'
+import FieldTripList from './field-trip-list'
+
+const WINDOW_WIDTH = 450
+const MARGIN = 15
+
+/**
+ * Collects the various draggable windows for the Field Trip module.
+ */
+const FieldTripWindows = ({callTaker}) => {
+ const {fieldTrip} = callTaker
+ // Do not render details or list if visible is false.
+ if (!fieldTrip.visible) return null
+ return (
+ <>
+
+
+ >
+ )
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ callTaker: state.callTaker
+ }
+}
+
+export default connect(mapStateToProps)(FieldTripWindows)
diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js
new file mode 100644
index 000000000..c5f53259d
--- /dev/null
+++ b/lib/components/admin/styled.js
@@ -0,0 +1,134 @@
+import { Button as BsButton } from 'react-bootstrap'
+import styled, {css} from 'styled-components'
+
+import DefaultCounter from './call-time-counter'
+
+// Call Taker Controls Components
+
+const RED = '#C35134'
+const BLUE = '#1C4D89'
+const GREEN = '#6B931B'
+const PURPLE = '#8134D3'
+
+const circleButtonStyle = css`
+ border: none;
+ border-radius: 50%;
+ box-shadow: 2px 2px 4px #000000;
+ color: white;
+ position: absolute;
+ z-index: 999999;
+`
+
+export const CallHistoryButton = styled.button`
+ ${circleButtonStyle}
+ background-color: ${GREEN};
+ height: 40px;
+ margin-left: 69px;
+ top: 140px;
+ width: 40px;
+`
+
+export const CallTimeCounter = styled(DefaultCounter)`
+ background-color: ${BLUE};
+ border-radius: 20px;
+ box-shadow: 2px 2px 4px #000000;
+ color: white;
+ font-weight: 600;
+ margin-left: -8px;
+ position: absolute;
+ text-align: center;
+ top: 241px;
+ width: 80px;
+ z-index: 999999;
+`
+
+export const ControlsContainer = styled.div`
+ position: relative;
+`
+
+export const FieldTripsButton = styled.button`
+ ${circleButtonStyle}
+ background-color: ${PURPLE};
+ height: 50px;
+ margin-left: 80px;
+ top: 190px;
+ width: 50px;
+`
+
+export const ToggleCallButton = styled.button`
+ ${circleButtonStyle}
+ background-color: ${props => props.callInProgress ? RED : BLUE};
+ height: 80px;
+ margin-left: -8px;
+ top: 154px;
+ width: 80px;
+`
+
+// Field Trip Windows Components
+
+export const Bold = styled.strong``
+
+export const Button = styled(BsButton)`
+ margin-left: 5px;
+`
+
+export const Container = styled.div`
+ display: flex;
+ flex-flow: row wrap;
+`
+
+export const Half = styled.div`
+ width: 50%
+`
+
+// Make sure button extends to end of window.
+export const FieldTripRecordButton = styled.button`
+ display: inline-block;
+ width: 100%;
+ width: -moz-available;
+ width: -webkit-fill-available;
+ width: fill-available;
+`
+
+export const Full = styled.div`
+ width: 100%
+`
+
+export const FullWithMargin = styled(Full)`
+ margin-top: 10px;
+`
+
+export const Header = styled.h4`
+ margin-bottom: 5px;
+ width: 100%;
+`
+
+export const InlineHeader = styled(Header)`
+ display: inline;
+`
+
+export const textCss = css`
+ font-size: 0.9em;
+ margin-bottom: 0px;
+`
+
+export const Para = styled.p`
+ ${textCss}
+`
+
+export const Text = styled.span`
+ ${textCss}
+`
+
+export const Val = styled.span`
+ :empty:before {
+ color: grey;
+ content: 'N/A';
+ }
+`
+
+export const WindowHeader = styled.h3`
+ font-size: 18px;
+ margin-bottom: 10px;
+ margin-top: 10px;
+`
diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js
new file mode 100644
index 000000000..741b12912
--- /dev/null
+++ b/lib/components/admin/trip-status.js
@@ -0,0 +1,109 @@
+import { getTimeFormat } from '@opentripplanner/core-utils/lib/time'
+import moment from 'moment'
+import React, {Component} from 'react'
+import { connect } from 'react-redux'
+
+import * as fieldTripActions from '../../actions/field-trip'
+import * as formActions from '../../actions/form'
+import Icon from '../narrative/icon'
+import {
+ Bold,
+ Button,
+ Full,
+ Header,
+ Para
+} from './styled'
+import { getTripFromRequest } from '../../util/call-taker'
+
+class TripStatus extends Component {
+ _getTrip = () => getTripFromRequest(this.props.request, this.props.outbound)
+
+ _formatTime = (time) => moment(time).format(this.props.timeFormat)
+
+ _formatTripStatus = () => {
+ if (!this._getStatus()) {
+ return (
+
+ No itineraries planned! Click Plan to plan trip.
+
+ )
+ }
+ const trip = this._getTrip()
+ if (!trip) return
Error finding trip!
+ return (
+
+ {trip.groupItineraries.length} group itineraries, planned by{' '}
+ {trip.createdBy} at {trip.timeStamp}
+
+ )
+ }
+
+ _getStatus = () => {
+ const {outbound, request} = this.props
+ 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,
+ leaveDestinationTime,
+ startLocation
+ } = request
+ if (!request) {
+ console.warn('Could not find field trip request')
+ return null
+ }
+ const start = outbound ? startLocation : endLocation
+ const end = outbound ? endLocation : startLocation
+ return (
+
+
+ {this._getStatusIcon()}
+ {outbound ? 'Outbound' : 'Inbound'} trip
+
+
+
+ From {start} to {end}
+ {outbound
+ ?
+ Arriving at {this._formatTime(arriveDestinationTime)}
+
+ : <>
+
+ Leave at {this._formatTime(leaveDestinationTime)},{' '}
+ due back at {this._formatTime(arriveSchoolTime)}
+
+ >
+ }
+ {this._formatTripStatus()}
+
+ )
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ callTaker: state.callTaker,
+ currentQuery: state.otp.currentQuery,
+ timeFormat: getTimeFormat(state.otp.config)
+ }
+}
+
+const mapDispatchToProps = {
+ planTrip: fieldTripActions.planTrip,
+ saveRequestTrip: fieldTripActions.saveRequestTrip,
+ setQueryParam: formActions.setQueryParam
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(TripStatus)
diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js
new file mode 100644
index 000000000..16bd37246
--- /dev/null
+++ b/lib/components/admin/updatable.js
@@ -0,0 +1,28 @@
+import React, { Component } from 'react'
+import { Button } from 'react-bootstrap'
+
+import { Val } from './styled'
+
+export default class Updatable extends Component {
+ _onClick = () => {
+ const {fieldName, onUpdate, value} = this.props
+ const newValue = window.prompt(
+ `Please input new value for ${fieldName}:`,
+ value
+ )
+ if (newValue !== null) onUpdate(newValue)
+ }
+
+ render () {
+ const {fieldName, label, value} = this.props
+ return (
+ <>
+ {label || fieldName}:{' '}
+
{value}
+
+ >
+ )
+ }
+}
diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js
index 6bf4d9374..703c0752d 100644
--- a/lib/components/app/app-menu.js
+++ b/lib/components/app/app-menu.js
@@ -1,7 +1,9 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
+import qs from 'qs'
import { connect } from 'react-redux'
import { DropdownButton, MenuItem } from 'react-bootstrap'
+import { withRouter } from 'react-router'
import Icon from '../narrative/icon'
@@ -19,11 +21,21 @@ class AppMenu extends Component {
}
_startOver = () => {
- const { reactRouterConfig } = this.props
+ const { location, reactRouterConfig } = this.props
+ const { search } = location
let startOverUrl = '/'
if (reactRouterConfig && reactRouterConfig.basename) {
startOverUrl += reactRouterConfig.basename
}
+ // If search contains sessionId, preserve this so that the current session
+ // is not lost when the page reloads.
+ if (search) {
+ const params = qs.parse(search, { ignoreQueryPrefix: true })
+ const { sessionId } = params
+ if (sessionId) {
+ startOverUrl += `?${qs.stringify({sessionId})}`
+ }
+ }
window.location.href = startOverUrl
}
@@ -62,4 +74,4 @@ const mapDispatchToProps = {
setMainPanelContent
}
-export default connect(mapStateToProps, mapDispatchToProps)(AppMenu)
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppMenu))
diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js
index 055b2a8d0..a15366b49 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 fieldTripActions from '../../actions/field-trip'
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'
@@ -42,6 +44,7 @@ class CallTakerPanel extends Component {
window.alert(`Please define the following fields to plan a trip: ${issues.join(', ')}`)
return
}
+ if (this.state.expandAdvanced) this.setState({expandAdvanced: false})
routingQuery()
}
@@ -91,11 +94,15 @@ class CallTakerPanel extends Component {
}
}
+ _updateGroupSize = (evt) => this.props.setGroupSize(+evt.target.value)
+
render () {
const {
activeSearch,
currentQuery,
+ groupSize,
mainPanelContent,
+ maxGroupSize,
mobile,
modes,
routes,
@@ -124,7 +131,7 @@ class CallTakerPanel extends Component {
right: '0px',
left: '0px',
padding: '0px 8px 5px',
- display: expandAdvanced ? 'none' : undefined
+ display: expandAdvanced ? undefined : 'none'
}
return (
@@ -206,6 +213,21 @@ class CallTakerPanel extends Component {
+ {groupSize !== null && maxGroupSize &&
+
+ Group size:{' '}
+
+
+ }