From a8160f33a28a50617d0f5cd7911c900b7690c1e6 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 11 Sep 2020 11:45:48 -0400 Subject: [PATCH 01/35] feat(field-trip): add field trip module (WIP) --- example.js | 9 +- lib/actions/call-taker.js | 26 +++++ lib/components/admin/call-taker-controls.js | 110 ++++++++++++++------ lib/components/admin/field-trip-windows.js | 67 ++++++++++++ lib/index.js | 2 + lib/reducers/call-taker.js | 30 +++++- 6 files changed, 208 insertions(+), 36 deletions(-) create mode 100644 lib/components/admin/field-trip-windows.js diff --git a/example.js b/example.js index 59a3f0dc8..fb6693761 100644 --- a/example.js +++ b/example.js @@ -19,6 +19,7 @@ import { CallTakerControls, CallTakerPanel, CallTakerWindows, + FieldTripWindows, DefaultMainPanel, DesktopNav, BatchRoutingPanel, @@ -96,7 +97,13 @@ class OtpRRExample extends Component { {otpConfig.datastoreUrl ? : null} - {otpConfig.datastoreUrl ? : null} + {otpConfig.datastoreUrl + ? <> + + + + : null + } diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index 25ccb2ae7..96d2b1241 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -17,10 +17,14 @@ const requestingCalls = createAction('REQUESTING_CALLS') const requestingQueries = createAction('REQUESTING_QUERIES') const storeSession = createAction('STORE_SESSION') +const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS') +const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS') + /// PUBLIC ACTIONS export const beginCall = createAction('BEGIN_CALL') export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY') +export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') /** * End the active call and store the queries made during the call. @@ -133,6 +137,28 @@ export function fetchCalls () { } } +/** + * Fetch latest calls for a particular session. + */ +export function fetchFieldTrips () { + return function (dispatch, getState) { + dispatch(requestingFieldTrips()) + const {callTaker, otp} = getState() + if (sessionIsInvalid(callTaker.session)) return + const {datastoreUrl} = otp.config + const {sessionId} = callTaker.session + fetch(`${datastoreUrl}/fieldtrip/getRequestsSummary?${qs.stringify({sessionId})}`) + .then(res => res.json()) + .then(fieldTrips => { + console.log('GET field trips response', fieldTrips) + dispatch(receivedFieldTrips({fieldTrips})) + }) + .catch(err => { + alert(`Could not fetch field trips: ${JSON.stringify(err)}`) + }) + } +} + /** * @return {boolean} - whether a calltaker session is invalid */ diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index d5b8f1833..244dce7b8 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -10,6 +10,7 @@ import Icon from '../narrative/icon' const RED = '#C35134' const BLUE = '#1C4D89' const GREEN = '#6B931B' +const PURPLE = '#8134D3' /** * This component displays the controls for the Call Taker/Field Trip modules, @@ -21,10 +22,17 @@ const GREEN = '#6B931B' */ class CallTakerControls extends Component { componentWillReceiveProps (nextProps) { - const {session} = nextProps + const { + callTakerEnabled, + fetchCalls, + fetchFieldTrips, + fieldTripEnabled, + session + } = nextProps // Once session is available, fetch calls. if (session && !this.props.session) { - this.props.fetchCalls() + if (callTakerEnabled) fetchCalls() + if (fieldTripEnabled) fetchFieldTrips() } } @@ -57,10 +65,12 @@ 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 @@ -75,21 +85,23 @@ class CallTakerControls extends Component { return ( <> {/* Start/End Call button */} - + }} + className='call-taker-button' + onClick={this._onClickCall} + > + {this._renderCallButton()} + + } {this._callInProgress() ? - - + {callTakerEnabled && + + } {/* Field Trip toggle button TODO */} + {fieldTripEnabled && + + } ) } @@ -132,19 +163,30 @@ 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 { + beginCall, + endCall, + fetchCalls, + fetchFieldTrips, + toggleCallHistory, + toggleFieldTrips +} = callTakerActions const mapDispatchToProps = { beginCall, endCall, fetchCalls, + fetchFieldTrips, routingQuery, setMainPanelContent, - toggleCallHistory + toggleCallHistory, + toggleFieldTrips } export default connect(mapStateToProps, mapDispatchToProps)(CallTakerControls) diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js new file mode 100644 index 000000000..1fca110c1 --- /dev/null +++ b/lib/components/admin/field-trip-windows.js @@ -0,0 +1,67 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import * as callTakerActions from '../../actions/call-taker' +import CallRecord from './call-record' +import DraggableWindow from './draggable-window' +import Icon from '../narrative/icon' + +/** + * Collects the various draggable windows used in the Call Taker module to + * display, for example, the call record list and (TODO) the list of field trips. + */ +class FieldTripWindows extends Component { + render () { + const {callTaker, fetchQueries, searches} = this.props + const {activeCall, fieldTrip} = callTaker + console.log(fieldTrip) + return ( + <> + {fieldTrip.visible + // Active call window + ? Field Trip Requests} + onClickClose={this.props.toggleCallHistory} + > + {activeCall + ? + : null + } + {fieldTrip.requests.data.length > 0 + ? fieldTrip.requests.data.map((request, i) => ( +
+ {request.startLocation} to {request.endLocation} +
+ )) + :
No calls in history
+ } +
+ : null + } + + ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + callTaker: state.callTaker, + currentQuery: state.otp.currentQuery, + searches: state.otp.searches + } +} + +const { + fetchQueries, + toggleCallHistory +} = callTakerActions + +const mapDispatchToProps = { fetchQueries, toggleCallHistory } + +export default connect(mapStateToProps, mapDispatchToProps)(FieldTripWindows) diff --git a/lib/index.js b/lib/index.js index c23962411..0007a8669 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,6 @@ import CallTakerControls from './components/admin/call-taker-controls' import CallTakerWindows from './components/admin/call-taker-windows' +import FieldTripWindows from './components/admin/field-trip-windows' import DateTimeModal from './components/form/date-time-modal' import DateTimePreview from './components/form/date-time-preview' @@ -65,6 +66,7 @@ export { DateTimePreview, DefaultSearchForm, ErrorMessage, + FieldTripWindows, LocationField, PlanTripButton, SettingsPreview, diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 57f5e885c..92cdfce61 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -7,6 +7,7 @@ import {getTimestamp} from '../util/state' const { randId } = coreUtils.storage const UPPER_RIGHT_CORNER = {x: 604, y: 53} +const LOWER_RIGHT_CORNER = {x: 604, y: 400} const FETCH_STATUS = { UNFETCHED: 0, @@ -26,7 +27,14 @@ function createCallTakerReducer () { data: [] } }, - fieldTrips: [], + fieldTrip: { + position: LOWER_RIGHT_CORNER, + requests: { + status: FETCH_STATUS.UNFETCHED, + data: [] + }, + visible: false, + }, session: null } return (state = initialState, action) => { @@ -58,6 +66,21 @@ function createCallTakerReducer () { callHistory: { calls: { $set: calls } } }) } + case 'REQUESTING_FIELD_TRIPS': { + return update(state, { + fieldTrip: { requests: { status: { $set: FETCH_STATUS.FETCHING } } } + }) + } + case 'RECEIVED_FIELD_TRIPS': { + const data = action.payload.fieldTrips + const requests = { + status: FETCH_STATUS.FETCHED, + data: data.sort((a, b) => moment(b.endTime) - moment(a.endTime)) + } + return update(state, { + fieldTrip: { requests: { $set: requests } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls @@ -95,6 +118,11 @@ function createCallTakerReducer () { callHistory: { visible: { $set: !state.callHistory.visible } } }) } + case 'TOGGLE_FIELD_TRIPS': { + return update(state, { + fieldTrip: { visible: { $set: !state.fieldTrip.visible } } + }) + } case 'END_CALL': { return update(state, { activeCall: { $set: null } From 55fb0713142c9c9a7b9604252da38f9e33de961d Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 11 Sep 2020 13:30:42 -0400 Subject: [PATCH 02/35] refactor(field-trip): add search/filtering for field trip requests --- lib/actions/call-taker.js | 1 + lib/components/admin/call-taker-windows.js | 14 ++-- lib/components/admin/draggable-window.js | 5 +- lib/components/admin/field-trip-windows.js | 95 ++++++++++++++++++---- lib/reducers/call-taker.js | 13 ++- 5 files changed, 101 insertions(+), 27 deletions(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index 96d2b1241..df15ec0a7 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -23,6 +23,7 @@ const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS') /// PUBLIC ACTIONS export const beginCall = createAction('BEGIN_CALL') +export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER') export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') diff --git a/lib/components/admin/call-taker-windows.js b/lib/components/admin/call-taker-windows.js index b575cc2ca..7b1073373 100644 --- a/lib/components/admin/call-taker-windows.js +++ b/lib/components/admin/call-taker-windows.js @@ -12,7 +12,7 @@ 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 return ( <> @@ -23,7 +23,7 @@ class CallTakerWindows extends Component { defaultPosition: callHistory.position }} header={ Call history} - onClickClose={this.props.toggleCallHistory} + onClickClose={toggleCallHistory} > {activeCall ? { } } -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/draggable-window.js b/lib/components/admin/draggable-window.js index 1159cdd39..34d12d569 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -7,7 +7,7 @@ const noop = () => {} export default class DraggableWindow extends Component { render () { - const {children, draggableProps, header} = this.props + const {children, draggableProps, header, style} = this.props const GREY_BORDER = '#777 1.3px solid' return (
req.status !== 'cancelled' && (!req.inboundTripStatus || !req.outboundTripStatus)}, + {id: 'planned', label: 'Planned', filter: (req) => req.status !== 'cancelled' && req.inboundTripStatus && req.outboundTripStatus}, + {id: 'cancelled', label: 'Cancelled', filter: (req) => req.status === 'cancelled'}, + {id: 'past', label: 'Past', filter: (req) => req.travelDate && moment(req.travelDate).diff(moment(), 'days') < 0}, + {id: 'all', label: 'All', filter: (req) => true} +] + +const SEARCH_FIELDS = [ + 'address', + 'ccLastFour', + 'ccName', + 'ccType', + 'checkNumber', + 'city', + 'classpassId', + 'emailAddress', + 'endLocation', + 'grade', + 'phoneNumber', + 'schoolName', + 'startLocation', + 'submitterNotes', + 'teacherName' +] + /** * Collects the various draggable windows used in the Call Taker module to * display, for example, the call record list and (TODO) the list of field trips. */ class FieldTripWindows extends Component { + _onSearchChange = e => { + this.props.setFieldTripFilter({search: e.target.value}) + } + + _onTabChange = e => { + this.props.setFieldTripFilter({tab: e.target.name}) + } + render () { - const {callTaker, fetchQueries, searches} = this.props - const {activeCall, fieldTrip} = callTaker - console.log(fieldTrip) + const {callTaker, searches, toggleFieldTrips} = this.props + const {activeFieldTrip, fieldTrip} = callTaker + const {filter} = fieldTrip + const activeTab = TABS.find(tab => tab.id === filter.tab) + const visibleRequests = fieldTrip.requests.data + .filter(ft => { + if (activeTab) return activeTab.filter(ft) + else return true + }) + .filter(ft => !filter.search || SEARCH_FIELDS.some(key => { + const value = ft[key] + if (value) return value.toLowerCase().indexOf(filter.search.toLowerCase()) !== -1 + else return false + })) return ( <> {fieldTrip.visible @@ -24,22 +70,42 @@ class FieldTripWindows extends Component { defaultPosition: fieldTrip.position }} header={ Field Trip Requests} - onClickClose={this.props.toggleCallHistory} + onClickClose={toggleFieldTrips} + style={{width: '450px'}} > - {activeCall + + {TABS.map(tab => { + const active = tab.id === filter.tab + const requestCount = fieldTrip.requests.data.filter(tab.filter).length + return ( + + ) + })} + {activeFieldTrip ? : null } - {fieldTrip.requests.data.length > 0 - ? fieldTrip.requests.data.map((request, i) => ( + {visibleRequests.length > 0 + ? visibleRequests.map((request, i) => (
{request.startLocation} to {request.endLocation}
)) - :
No calls in history
+ :
No field trips found.
} : null @@ -57,11 +123,10 @@ const mapStateToProps = (state, ownProps) => { } } -const { - fetchQueries, - toggleCallHistory -} = callTakerActions - -const mapDispatchToProps = { fetchQueries, toggleCallHistory } +const mapDispatchToProps = { + fetchQueries: callTakerActions.fetchQueries, + setFieldTripFilter: callTakerActions.setFieldTripFilter, + toggleFieldTrips: callTakerActions.toggleFieldTrips +} export default connect(mapStateToProps, mapDispatchToProps)(FieldTripWindows) diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 92cdfce61..5a5120027 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -7,7 +7,7 @@ import {getTimestamp} from '../util/state' const { randId } = coreUtils.storage const UPPER_RIGHT_CORNER = {x: 604, y: 53} -const LOWER_RIGHT_CORNER = {x: 604, y: 400} +const LOWER_RIGHT_CORNER = {x: 504, y: 400} const FETCH_STATUS = { UNFETCHED: 0, @@ -19,6 +19,7 @@ const FETCH_STATUS = { function createCallTakerReducer () { const initialState = { activeCall: null, + activeFieldTrip: null, callHistory: { position: UPPER_RIGHT_CORNER, visible: false, @@ -28,12 +29,15 @@ function createCallTakerReducer () { } }, fieldTrip: { + filter: { + tab: 'new' + }, position: LOWER_RIGHT_CORNER, requests: { status: FETCH_STATUS.UNFETCHED, data: [] }, - visible: false, + visible: false }, session: null } @@ -81,6 +85,11 @@ function createCallTakerReducer () { fieldTrip: { requests: { $set: requests } } }) } + case 'SET_FIELD_TRIP_FILTER': { + return update(state, { + fieldTrip: { filter: { $merge: action.payload } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls From 231f372e5c9957a48c59570e24357fcc52099f81 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 11 Sep 2020 16:58:13 -0400 Subject: [PATCH 03/35] refactor(field-trip): add field trip details component --- lib/actions/call-taker.js | 25 ++++ lib/components/admin/call-taker-windows.js | 2 +- lib/components/admin/draggable-window.js | 1 - lib/components/admin/field-trip-details.js | 62 ++++++++ lib/components/admin/field-trip-windows.js | 161 ++++++++++++++++----- lib/reducers/call-taker.js | 14 +- 6 files changed, 223 insertions(+), 42 deletions(-) create mode 100644 lib/components/admin/field-trip-details.js diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index df15ec0a7..f9e0e6820 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -19,11 +19,14 @@ const storeSession = createAction('STORE_SESSION') 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 beginCall = createAction('BEGIN_CALL') export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER') +export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP') export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') @@ -160,6 +163,28 @@ export function fetchFieldTrips () { } } +/** + * Fetch latest calls for a particular session. + */ +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 => { + console.log('GET field trip details response', fieldTrip) + dispatch(receivedFieldTripDetails({fieldTrip})) + }) + .catch(err => { + alert(`Could not fetch field trips: ${JSON.stringify(err)}`) + }) + } +} + /** * @return {boolean} - whether a calltaker session is invalid */ diff --git a/lib/components/admin/call-taker-windows.js b/lib/components/admin/call-taker-windows.js index 7b1073373..35a2399d4 100644 --- a/lib/components/admin/call-taker-windows.js +++ b/lib/components/admin/call-taker-windows.js @@ -22,7 +22,7 @@ class CallTakerWindows extends Component { draggableProps={{ defaultPosition: callHistory.position }} - header={ Call history} + header={

Call history

} onClickClose={toggleCallHistory} > {activeCall diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 34d12d569..6705b158f 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -39,7 +39,6 @@ export default class DraggableWindow extends Component { style={{ borderBottom: GREY_BORDER, cursor: 'move', - fontSize: 'large', paddingBottom: '5px' }} > diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js new file mode 100644 index 000000000..6397cc2f2 --- /dev/null +++ b/lib/components/admin/field-trip-details.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import * as callTakerActions from '../../actions/call-taker' +import DraggableWindow from './draggable-window' +import Icon from '../narrative/icon' + +/** + * Shows the details for the active Field Trip Request. + */ +class FieldTripDetails extends Component { + _onCloseActiveFieldTrip = () => { + this.props.setActiveFieldTrip(null) + } + + render () { + const {callTaker, request} = this.props + if (!request) return null + const {id, schoolName} = request + const {fieldTrip} = callTaker + const defaultPosition = {...fieldTrip.position} + defaultPosition.x = defaultPosition.x - 460 + return ( + + {schoolName} Trip (#{id}) + + } + onClickClose={this._onCloseActiveFieldTrip} + style={{width: '450px'}} + > + {Object.keys(request).map(key => { + const value = typeof request[key] === 'object' + ? 'TODO: object' + : request[key] + return (
{key}: {value}
) + })} +
+ ) + } +} + +const mapStateToProps = (state, ownProps) => { + const {activeId, requests} = state.callTaker.fieldTrip + const request = requests.data.find(req => req.id === activeId) + return { + callTaker: state.callTaker, + currentQuery: state.otp.currentQuery, + request + } +} + +const mapDispatchToProps = { + fetchQueries: callTakerActions.fetchQueries, + setActiveFieldTrip: callTakerActions.setActiveFieldTrip, + setFieldTripFilter: callTakerActions.setFieldTripFilter, + toggleFieldTrips: callTakerActions.toggleFieldTrips +} + +export default connect(mapStateToProps, mapDispatchToProps)(FieldTripDetails) diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js index a2be65a20..9774e146a 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -1,10 +1,11 @@ import moment from 'moment' import React, { Component } from 'react' +import { Badge } from 'react-bootstrap' import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' -import CallRecord from './call-record' import DraggableWindow from './draggable-window' +import FieldTripDetails from './field-trip-details' import Icon from '../narrative/icon' const TABS = [ @@ -34,22 +35,35 @@ const SEARCH_FIELDS = [ ] /** - * Collects the various draggable windows used in the Call Taker module to - * display, for example, the call record list and (TODO) the list of field trips. + * Collects the various draggable windows for the Field Trip module. */ class FieldTripWindows 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) + } + } + + _onCloseActiveFieldTrip = () => { + this.props.setActiveFieldTrip(null) + } + _onSearchChange = e => { this.props.setFieldTripFilter({search: e.target.value}) } _onTabChange = e => { - this.props.setFieldTripFilter({tab: e.target.name}) + this.props.setFieldTripFilter({tab: e.currentTarget.name}) } render () { - const {callTaker, searches, toggleFieldTrips} = this.props - const {activeFieldTrip, fieldTrip} = callTaker - const {filter} = fieldTrip + const {callTaker, toggleFieldTrips} = this.props + const {fieldTrip} = callTaker + const {activeId, filter} = fieldTrip const activeTab = TABS.find(tab => tab.id === filter.tab) const visibleRequests = fieldTrip.requests.data .filter(ft => { @@ -64,57 +78,125 @@ class FieldTripWindows extends Component { return ( <> {fieldTrip.visible - // Active call window ? Field Trip Requests} + header={ + <> +

+ Field Trip Requests{' '} + +

+ {TABS.map(tab => { + const active = tab.id === filter.tab + const style = { + borderRadius: 5, + backgroundColor: active ? 'navy' : undefined, + color: active ? 'white' : undefined, + padding: '2px 3px' + } + const requestCount = fieldTrip.requests.data.filter(tab.filter).length + return ( + + ) + })} + + } onClickClose={toggleFieldTrips} style={{width: '450px'}} > - - {TABS.map(tab => { - const active = tab.id === filter.tab - const requestCount = fieldTrip.requests.data.filter(tab.filter).length - return ( - - ) - })} - {activeFieldTrip - ? - : null - } {visibleRequests.length > 0 ? visibleRequests.map((request, i) => ( -
- {request.startLocation} to {request.endLocation} -
+ )) :
No field trips found.
}
: null } + ) } } +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 ( +
  • + +
  • + ) + } +} + const mapStateToProps = (state, ownProps) => { return { callTaker: state.callTaker, @@ -124,7 +206,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - fetchQueries: callTakerActions.fetchQueries, + fetchFieldTripDetails: callTakerActions.fetchFieldTripDetails, + setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, toggleFieldTrips: callTakerActions.toggleFieldTrips } diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 5a5120027..c37728b86 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -19,7 +19,6 @@ const FETCH_STATUS = { function createCallTakerReducer () { const initialState = { activeCall: null, - activeFieldTrip: null, callHistory: { position: UPPER_RIGHT_CORNER, visible: false, @@ -29,6 +28,7 @@ function createCallTakerReducer () { } }, fieldTrip: { + activeId: null, filter: { tab: 'new' }, @@ -90,6 +90,18 @@ function createCallTakerReducer () { fieldTrip: { filter: { $merge: action.payload } } }) } + case 'SET_ACTIVE_FIELD_TRIP': { + return update(state, { + fieldTrip: { activeId: { $set: action.payload } } + }) + } + case 'RECEIVED_FIELD_TRIP_DETAILS': { + const {fieldTrip} = action.payload + const index = state.fieldTrip.requests.data.findIndex(req => req.id === fieldTrip.id) + return update(state, { + fieldTrip: { requests: { data: { [index]: { $set: fieldTrip } } } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls From 3e25117dc9215c87416860e32596a643f29c4521 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 22 Oct 2020 13:55:07 -0400 Subject: [PATCH 04/35] refactor(calltaker): increase limit for fetch calls --- lib/actions/call-taker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index f9e0e6820..5b7cd6d44 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -128,7 +128,7 @@ 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 => { From 29f7062adba6246856dcf47deb3b7af272ef86bc Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 5 Jan 2021 16:42:53 -0500 Subject: [PATCH 05/35] refactor(draggable-window): fix draggable handle margins --- lib/components/admin/draggable-window.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 6705b158f..95868dcba 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -28,7 +28,7 @@ export default class DraggableWindow extends Component { width: '350px', backgroundColor: 'white', borderRadius: '5%', - padding: '10px', + paddingBottom: '10px', boxShadow: '2px 2px 8px', border: GREY_BORDER, ...style @@ -39,7 +39,7 @@ export default class DraggableWindow extends Component { style={{ borderBottom: GREY_BORDER, cursor: 'move', - paddingBottom: '5px' + padding: '5px' }} > +
    + } header={

    {schoolName} Trip (#{id})

    } + height='375px' onClickClose={this._onCloseActiveFieldTrip} style={{width: '450px'}} > - {Object.keys(request).map(key => { - const value = typeof request[key] === 'object' - ? 'TODO: object' - : request[key] - return (
    {key}: {value}
    ) - })} + +
    Group Information
    + +

    {schoolName}

    +

    Teacher: {teacherName}

    +
    + +

    + Total group size: {total} + +

    +

    {numStudents} students 7 or older

    +

    {numFreeStudents} students under 7

    +

    {numStudents} chaperones

    +
    + + + + +
    + Payment information +
    +

    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}

    +
    +
    ) } @@ -48,11 +170,13 @@ const mapStateToProps = (state, ownProps) => { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, - request + request, + timeFormat: getTimeFormat(state.otp.config) } } const mapDispatchToProps = { + addFieldTripNote: callTakerActions.addFieldTripNote, fetchQueries: callTakerActions.fetchQueries, setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js new file mode 100644 index 000000000..c7aaaa46e --- /dev/null +++ b/lib/components/admin/field-trip-notes.js @@ -0,0 +1,117 @@ +import React, { Component } from 'react' +import { Badge } from 'react-bootstrap' +import styled from 'styled-components' + +import Icon from '../narrative/icon' +import { + Button, + Full, + Header, + P, + Val +} from './styled' + +const Quote = styled.p` + font-size: small; + margin-bottom: 5px; +` + +const Footer = styled.footer` + font-size: x-small; +` + +const Note = ({note}) => { + return ( +
    + {note.note} +
    {note.userName} on {note.timeStamp}
    +
    + ) +} + +const Feedback = ({feedback}) => { + return ( +
    + {feedback.feedback} +
    {feedback.userName} on {feedback.timeStamp}
    +
    + ) +} +/** + * Renders the various notes/feedback for a field trip request. + */ +export default class FieldTripNotes extends Component { + _getNotesCount = () => { + const {request} = this.props + const {feedback, notes, submitterNotes} = request + let notesCount = 0 + if (notes && notes.length) notesCount += notes.length + if (feedback && feedback.length) notesCount += feedback.length + if (submitterNotes) notesCount++ + 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}) + } + + render () { + const {expanded, onClickToggle, request} = this.props + if (!request) return null + const { + feedback, + notes, + submitterNotes + } = 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()} + + + +
    + {expanded && + <> +
    Teacher notes
    +

    {submitterNotes}

    +
    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 new file mode 100644 index 000000000..02d3d7603 --- /dev/null +++ b/lib/components/admin/styled.js @@ -0,0 +1,37 @@ +import { Button as BsButton } from 'react-bootstrap' +import styled from 'styled-components' + +export const B = 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% +` + +export const Full = styled.div` + width: 100% +` + +export const Header = styled.h4` + margin-bottom: 5px; + width: 100%; +` + +export const P = styled.p` + margin-bottom: 0px; +` + +export const Val = styled.span` + :empty:before { + color: grey; + content: 'N/A'; + } +` diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js new file mode 100644 index 000000000..40d4ecd9e --- /dev/null +++ b/lib/components/admin/trip-status.js @@ -0,0 +1,69 @@ +import moment from 'moment' +import React, {Component} from 'react' + +import { + B, + Button, + Full, + Header, + P +} from './styled' + +export default class TripStatus extends Component { + _formatTime = (time) => moment(time).format(this.props.timeFormat) + + _formatTripStatus = (tripStatus) => { + if (!tripStatus) { + return ( + + No itineraries planned! Click Plan to plan trip. + + ) + } + return ( + + {JSON.stringify(tripStatus)} + + ) + } + + render () { + const {outbound, request} = this.props + const { + arriveDestinationTime, + arriveSchoolTime, + endLocation, + inboundTripStatus, + leaveDestinationTime, + outboundTripStatus, + startLocation + } = request + const status = outbound ? outboundTripStatus : inboundTripStatus + const start = outbound ? startLocation : endLocation + const end = outbound ? endLocation : startLocation + return ( + +
    + {outbound ? 'Outbound' : 'Inbound'} trip + + + {status && + + } +
    +

    From {start} to {end}

    + {outbound + ?

    + Arriving at {this._formatTime(arriveDestinationTime)},{' '} + leave at {this._formatTime(leaveDestinationTime)} +

    + : <> +

    From {endLocation} to {startLocation}

    +

    Due back at {this._formatTime(arriveSchoolTime)}

    + + } +

    {this._formatTripStatus(status)}

    +
    + ) + } +} diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js new file mode 100644 index 000000000..727f2e856 --- /dev/null +++ b/lib/components/admin/updatable.js @@ -0,0 +1,25 @@ +import React, { Component } from 'react' +import { Button } from 'react-bootstrap' + +import { Val } from './styled' + +export default class Updatable extends Component { + _onClick = () => { + const {field, value} = this.props + const newValue = window.prompt(`Please input new value for ${field}`, value) + console.log(newValue) + // FIXME: UPDATE request + } + + render () { + const {value} = this.props + return ( + <> + {value} + + + ) + } +} From 115362d16ece9e9bf596eae3640e1300fbd84c3b Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 1 Feb 2021 17:06:41 -0500 Subject: [PATCH 12/35] refactor(field-trip): hook up UI with call back end --- lib/actions/call-taker.js | 118 ++++++++++++++++++++- lib/components/admin/draggable-window.js | 4 +- lib/components/admin/field-trip-details.js | 52 ++++++--- lib/components/admin/field-trip-notes.js | 29 ++--- lib/components/admin/field-trip-windows.js | 55 ++++++---- lib/components/admin/trip-status.js | 52 ++++++++- lib/components/admin/updatable.js | 17 ++- lib/util/call-taker.js | 7 ++ 8 files changed, 275 insertions(+), 59 deletions(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index ec4fb8439..b0929234d 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -1,8 +1,12 @@ -import { getUrlParams } from '@opentripplanner/core-utils/lib/query' +import { getUrlParams, 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 qs from 'qs' import { createAction } from 'redux-actions' -import {searchToQuery} from '../util/call-taker' +import {routingQuery} from './api' +import {setQueryParam} from './form' +import {getGroupSize, searchToQuery} from '../util/call-taker' import {URL_ROOT} from '../util/constants' import {getTimestamp} from '../util/state' @@ -191,7 +195,115 @@ export function addFieldTripNote (request, note) { ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { - alert(`Error storing call queries: ${JSON.stringify(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 + const queryData = new FormData() + queryData.append('sessionId', sessionId) + queryData.append('noteId', noteId) + return fetch(`${datastoreUrl}/fieldtrip/deleteNote`, + {method: 'POST', body: queryData} + ) + .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 queryData = new FormData() + queryData.append('sessionId', sessionId) + queryData.append('notes', submitterNotes) + queryData.append('requestId', request.id) + return fetch(`${datastoreUrl}/fieldtrip/editSubmitterNotes`, + {method: 'POST', body: queryData} + ) + .then(() => dispatch(fetchFieldTripDetails(request.id))) + .catch(err => { + alert(`Error editing submitter notes: ${JSON.stringify(err)}`) + }) + } +} + +export function planOutbound (request) { + return async function (dispatch, getState) { + const {config} = getState().otp + // this.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()) + } +} + +export 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() + 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 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 queryData = new FormData() + queryData.append('sessionId', sessionId) + queryData.append('status', status) + queryData.append('requestId', request.id) + return fetch(`${datastoreUrl}/fieldtrip/setRequestStatus`, + {method: 'POST', body: queryData} + ) + .then(() => dispatch(fetchFieldTripDetails(request.id))) + .catch(err => { + alert(`Error setting request status: ${JSON.stringify(err)}`) }) } } diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 2af771633..74dcfaf0b 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -43,8 +43,10 @@ export default class DraggableWindow extends Component { }} > {header} diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index e41d0aec6..527cd8e03 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -1,4 +1,3 @@ -import { getTimeFormat } from '@opentripplanner/core-utils/lib/time' import React, { Component } from 'react' import { DropdownButton, MenuItem } from 'react-bootstrap' import { connect } from 'react-redux' @@ -39,16 +38,26 @@ class FieldTripDetails extends Component { expandNotes: true } + _editSubmitterNotes = (val) => this.props.editSubmitterNotes(this.props.request, val) + _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null) _onClickCancel = () => { - + const {request, setRequestStatus} = this.props + if (confirm('Are you sure you want to cancel this request? Any associated trips will be deleted.')) { + setRequestStatus(request, 'cancelled') + } } _toggleNotes = () => this.setState({expandNotes: !this.state.expandNotes}) render () { - const {addFieldTripNote, callTaker, request, timeFormat} = this.props + const { + addFieldTripNote, + callTaker, + deleteFieldTripNote, + request + } = this.props if (!request) return null const { ccLastFour, @@ -64,8 +73,10 @@ class FieldTripDetails extends Component { paymentPreference, requireInvoice, schoolName, + submitterNotes, teacherName, - ticketType + ticketType, + travelDate } = request const total = numStudents + numChaperones + numFreeStudents const {fieldTrip} = callTaker @@ -117,10 +128,17 @@ class FieldTripDetails extends Component { style={{width: '450px'}} > -
    Group Information
    +
    Group Information ({travelDate})

    {schoolName}

    Teacher: {teacherName}

    +

    + +

    @@ -133,31 +151,31 @@ class FieldTripDetails extends Component {

    {numFreeStudents} students under 7

    {numStudents} chaperones

    - - +
    Payment information

    Ticket type: {TICKET_TYPES[ticketType]}

    Payment preference: {PAYMENT_PREFS[paymentPreference]}

    -

    Invoice required:

    +

    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}

    +
    ) @@ -170,16 +188,18 @@ const mapStateToProps = (state, ownProps) => { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, - request, - timeFormat: getTimeFormat(state.otp.config) + request } } const mapDispatchToProps = { addFieldTripNote: callTakerActions.addFieldTripNote, + deleteFieldTripNote: callTakerActions.deleteFieldTripNote, + editSubmitterNotes: callTakerActions.editSubmitterNotes, fetchQueries: callTakerActions.fetchQueries, setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, + 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 c7aaaa46e..d87ce5aaf 100644 --- a/lib/components/admin/field-trip-notes.js +++ b/lib/components/admin/field-trip-notes.js @@ -6,9 +6,7 @@ import Icon from '../narrative/icon' import { Button, Full, - Header, - P, - Val + Header } from './styled' const Quote = styled.p` @@ -20,10 +18,13 @@ const Footer = styled.footer` font-size: x-small; ` -const Note = ({note}) => { +const Note = ({note, onClickDelete}) => { return (
    {note.note} +
    {note.userName} on {note.timeStamp}
    ) @@ -43,11 +44,10 @@ const Feedback = ({feedback}) => { export default class FieldTripNotes extends Component { _getNotesCount = () => { const {request} = this.props - const {feedback, notes, submitterNotes} = request + const {feedback, notes} = request let notesCount = 0 if (notes && notes.length) notesCount += notes.length if (feedback && feedback.length) notesCount += feedback.length - if (submitterNotes) notesCount++ return notesCount } @@ -61,13 +61,20 @@ export default class FieldTripNotes extends Component { 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}?`)) { + console.log('OK deleting') + deleteFieldTripNote(request, note.id) + } + } + render () { const {expanded, onClickToggle, request} = this.props if (!request) return null const { feedback, - notes, - submitterNotes + notes } = request const internalNotes = [] const operationalNotes = [] @@ -92,8 +99,6 @@ export default class FieldTripNotes extends Component { {expanded && <> -
    Teacher notes
    -

    {submitterNotes}

    User feedback
    {feedback && feedback.length > 0 ? feedback.map((f, i) => ) @@ -101,12 +106,12 @@ export default class FieldTripNotes extends Component { }
    Internal agent notes
    {internalNotes && internalNotes.length > 0 - ? internalNotes.map(n => ) + ? internalNotes.map(n => ) : 'No internal notes submitted.' }
    Operational notes
    {operationalNotes && operationalNotes.length > 0 - ? operationalNotes.map(n => ) + ? operationalNotes.map(n => ) : 'No operational notes submitted.' } diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js index 64d426315..73fd29ae7 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -7,6 +7,8 @@ import * as callTakerActions from '../../actions/call-taker' import DraggableWindow from './draggable-window' import FieldTripDetails from './field-trip-details' import Icon from '../narrative/icon' +import Loading from '../narrative/loading' +import {FETCH_STATUS} from '../../util/constants' // List of tabs used for filtering field trips. const TABS = [ @@ -50,6 +52,8 @@ class FieldTripWindows extends Component { } } + _onClickRefresh = () => this.props.fetchFieldTrips() + _onCloseActiveFieldTrip = () => { this.props.setActiveFieldTrip(null) } @@ -99,17 +103,25 @@ class FieldTripWindows extends Component { <>

    Field Trip Requests{' '} - + + + +

    {TABS.map(tab => { const active = tab.id === filter.tab @@ -137,15 +149,17 @@ class FieldTripWindows extends Component { onClickClose={toggleFieldTrips} style={{width: '450px'}} > - {visibleRequests.length > 0 - ? visibleRequests.map((request, i) => ( - - )) - :
    No field trips found.
    + {fieldTrip.requests.status === FETCH_STATUS.FETCHING + ? + : visibleRequests.length > 0 + ? visibleRequests.map((request, i) => ( + + )) + :
    No field trips found.
    } : null @@ -222,6 +236,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { fetchFieldTripDetails: callTakerActions.fetchFieldTripDetails, + fetchFieldTrips: callTakerActions.fetchFieldTrips, setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, toggleFieldTrips: callTakerActions.toggleFieldTrips diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 40d4ecd9e..a0e99a318 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -1,6 +1,11 @@ +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' +import { connect } from 'react-redux' +import * as callTakerActions from '../../actions/call-taker' +import * as formActions from '../../actions/form' import { B, Button, @@ -9,7 +14,7 @@ import { P } from './styled' -export default class TripStatus extends Component { +class TripStatus extends Component { _formatTime = (time) => moment(time).format(this.props.timeFormat) _formatTripStatus = (tripStatus) => { @@ -27,6 +32,28 @@ export default class TripStatus extends Component { ) } + _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 = () => { + const {outbound, request} = this.props + if (!request || !request.trips) return null + return outbound + ? request.trips[0] + : request.trips[1] + } + render () { const {outbound, request} = this.props const { @@ -38,9 +65,14 @@ export default class TripStatus extends Component { 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 (
    @@ -58,7 +90,7 @@ export default class TripStatus extends Component { leave at {this._formatTime(leaveDestinationTime)}

    : <> -

    From {endLocation} to {startLocation}

    +

    From {start} to {end}

    Due back at {this._formatTime(arriveSchoolTime)}

    } @@ -67,3 +99,19 @@ export default class TripStatus extends Component { ) } } + +const mapStateToProps = (state, ownProps) => { + return { + callTaker: state.callTaker, + currentQuery: state.otp.currentQuery, + timeFormat: getTimeFormat(state.otp.config) + } +} + +const mapDispatchToProps = { + planInbound: callTakerActions.planInbound, + planOutbound: callTakerActions.planOutbound, + setQueryParam: formActions.setQueryParam +} + +export default connect(mapStateToProps, mapDispatchToProps)(TripStatus) diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js index 727f2e856..a36f073c6 100644 --- a/lib/components/admin/updatable.js +++ b/lib/components/admin/updatable.js @@ -4,17 +4,24 @@ import { Button } from 'react-bootstrap' import { Val } from './styled' export default class Updatable extends Component { + // static defaultProps = { + // onUpdate: (val) => console.log(val) + // } + _onClick = () => { - const {field, value} = this.props - const newValue = window.prompt(`Please input new value for ${field}`, value) - console.log(newValue) - // FIXME: UPDATE request + const {fieldName, onUpdate, value} = this.props + const newValue = window.prompt( + `Please input new value for ${fieldName}:`, + value + ) + if (newValue !== null) onUpdate(newValue) } render () { - const {value} = this.props + const {fieldName, value} = this.props return ( <> + {fieldName}:{' '} {value} + : <> + + + + } + +

    + {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 ( + + ) + } 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})
    +
    Group Information

    {schoolName}

    Teacher: {teacherName}

    } onUpdate={this._editSubmitterNotes} value={submitterNotes} />

    -

    - Total group size: {total} - -

    -

    {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 + />
    -
    - Payment information -
    -

    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} -
    {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()} - @@ -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 - {status && - - }

    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}
    + {groupSize !== null && maxGroupSize && + + Group size:{' '} + + + } + : <> + - : <> - - - - } - -

    + + + } + {fields.map(f => { const input = ( { const {fieldName, inputProps = {}, onChange} = this.props diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 29a53b5a7..5be84aa05 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -4,7 +4,7 @@ import React, { Component } from 'react' import { DropdownButton, MenuItem } from 'react-bootstrap' import { connect } from 'react-redux' -import * as callTakerActions from '../../actions/call-taker' +import * as fieldTripActions from '../../actions/field-trip' import DraggableWindow from './draggable-window' import EditableSection from './editable-section' import FieldTripNotes from './field-trip-notes' @@ -13,10 +13,12 @@ import { B, Button, Container, - Full, + FullWithMargin, Half, Header, - P + InlineHeader, + P, + Text } from './styled' import TripStatus from './trip-status' import Updatable from './updatable' @@ -60,6 +62,9 @@ const PAYMENT_FIELDS = [ 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) _onClickCancel = () => { @@ -112,10 +117,16 @@ class FieldTripDetails extends Component { id='field-trip-menu' title='View' > - + Feedback link - + Receipt link @@ -160,8 +171,8 @@ class FieldTripDetails extends Component { {total} total group size} fields={GROUP_FIELDS} - header={{total} total group size} inputStyle={{lineHeight: '0.8em', padding: '0px', width: '50px'}} onChange={setRequestGroupSize} request={request} @@ -173,19 +184,20 @@ class FieldTripDetails extends Component { request={request} /> - + + // Use inline header so that 'Change' button appears on top line. + children={ + Payment information - + } + fields={PAYMENT_FIELDS} inputStyle={{lineHeight: '0.8em', padding: '0px', width: '100px'}} onChange={setRequestPaymentInfo} request={request} /> - + { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, + datastoreUrl: state.otp.config.datastoreUrl, dateFormat: getDateFormat(state.otp.config), request } } const mapDispatchToProps = { - addFieldTripNote: callTakerActions.addFieldTripNote, - deleteFieldTripNote: callTakerActions.deleteFieldTripNote, - editSubmitterNotes: callTakerActions.editSubmitterNotes, - fetchQueries: callTakerActions.fetchQueries, - setActiveFieldTrip: callTakerActions.setActiveFieldTrip, - setFieldTripFilter: callTakerActions.setFieldTripFilter, - setRequestGroupSize: callTakerActions.setRequestGroupSize, - setRequestPaymentInfo: callTakerActions.setRequestPaymentInfo, - setRequestStatus: callTakerActions.setRequestStatus, - toggleFieldTrips: callTakerActions.toggleFieldTrips + addFieldTripNote: fieldTripActions.addFieldTripNote, + deleteFieldTripNote: fieldTripActions.deleteFieldTripNote, + editSubmitterNotes: fieldTripActions.editSubmitterNotes, + fetchQueries: fieldTripActions.fetchQueries, + setActiveFieldTrip: fieldTripActions.setActiveFieldTrip, + setFieldTripFilter: fieldTripActions.setFieldTripFilter, + 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-windows.js b/lib/components/admin/field-trip-windows.js index 73fd29ae7..5fb4e0af8 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -3,7 +3,7 @@ import React, { Component } from 'react' import { Badge } from 'react-bootstrap' import { connect } from 'react-redux' -import * as callTakerActions from '../../actions/call-taker' +import * as fieldTripActions from '../../actions/field-trip' import DraggableWindow from './draggable-window' import FieldTripDetails from './field-trip-details' import Icon from '../narrative/icon' @@ -235,11 +235,11 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - fetchFieldTripDetails: callTakerActions.fetchFieldTripDetails, - fetchFieldTrips: callTakerActions.fetchFieldTrips, - setActiveFieldTrip: callTakerActions.setActiveFieldTrip, - setFieldTripFilter: callTakerActions.setFieldTripFilter, - toggleFieldTrips: callTakerActions.toggleFieldTrips + fetchFieldTripDetails: fieldTripActions.fetchFieldTripDetails, + fetchFieldTrips: fieldTripActions.fetchFieldTrips, + setActiveFieldTrip: fieldTripActions.setActiveFieldTrip, + setFieldTripFilter: fieldTripActions.setFieldTripFilter, + toggleFieldTrips: fieldTripActions.toggleFieldTrips } export default connect(mapStateToProps, mapDispatchToProps)(FieldTripWindows) diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js index d09e6c30d..aae999930 100644 --- a/lib/components/admin/styled.js +++ b/lib/components/admin/styled.js @@ -1,5 +1,5 @@ import { Button as BsButton } from 'react-bootstrap' -import styled from 'styled-components' +import styled, {css} from 'styled-components' export const B = styled.strong`` @@ -20,16 +20,32 @@ 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 P = styled.p` +export const InlineHeader = styled(Header)` + display: inline; +` + +export const textCss = css` font-size: 0.9em; margin-bottom: 0px; ` +export const P = styled.p` + ${textCss} +` + +export const Text = styled.span` + ${textCss} +` + export const Val = styled.span` :empty:before { color: grey; diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index f50ab1368..b48b9123b 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -3,7 +3,7 @@ import moment from 'moment' import React, {Component} from 'react' import { connect } from 'react-redux' -import * as callTakerActions from '../../actions/call-taker' +import * as fieldTripActions from '../../actions/field-trip' import * as formActions from '../../actions/form' import Icon from '../narrative/icon' import { @@ -101,8 +101,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - planTrip: callTakerActions.planTrip, - saveRequestTrip: callTakerActions.saveRequestTrip, + planTrip: fieldTripActions.planTrip, + saveRequestTrip: fieldTripActions.saveRequestTrip, setQueryParam: formActions.setQueryParam } diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index 5a949173f..a71099ef2 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -5,7 +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 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' @@ -289,7 +289,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { findRoutes: apiActions.findRoutes, routingQuery: apiActions.routingQuery, - setGroupSize: callTakerActions.setGroupSize, + setGroupSize: fieldTripActions.setGroupSize, setQueryParam: formActions.setQueryParam } diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index 0ba06def1..6c11f844b 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -6,6 +6,17 @@ function placeToLatLonStr (place) { return `${place.lat.toFixed(6)},${place.lon.toFixed(6)}` } +/** + * @return {boolean} - whether a calltaker session is invalid + */ +export function sessionIsInvalid (session) { + if (!session || !session.sessionId) { + console.error('No valid OTP datastore session found.') + return true + } + return false +} + /** * Utility to map an OTP MOD UI search object to a Call Taker datastore query * object. From 7b4ee668e411088a1195af2cf2ccad459a8025d3 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 3 Feb 2021 10:20:20 -0500 Subject: [PATCH 15/35] refactor(updatable): remove commented code --- lib/components/admin/updatable.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js index c69800b55..16bd37246 100644 --- a/lib/components/admin/updatable.js +++ b/lib/components/admin/updatable.js @@ -4,10 +4,6 @@ import { Button } from 'react-bootstrap' import { Val } from './styled' export default class Updatable extends Component { - // static defaultProps = { - // onUpdate: (val) => console.log(val) - // } - _onClick = () => { const {fieldName, onUpdate, value} = this.props const newValue = window.prompt( From 907e9a891388b95b2ede511b92c75306a39490cb Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 3 Feb 2021 10:48:19 -0500 Subject: [PATCH 16/35] refactor(field-trip): move non-payment fields to group info --- lib/components/admin/editable-section.js | 7 +++- lib/components/admin/field-trip-details.js | 46 ++++++---------------- lib/util/call-taker.js | 34 ++++++++++++++++ 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js index cee1149ad..57c089e35 100644 --- a/lib/components/admin/editable-section.js +++ b/lib/components/admin/editable-section.js @@ -56,7 +56,12 @@ export default class EditableSection extends Component { return ( <> {children} - + {!isEditing ? -
    User feedback
    - {feedback && feedback.length > 0 - ? feedback.map((f, i) => ) - : 'No feedback submitted.' - }
    Internal agent notes
    {internalNotes && internalNotes.length > 0 ? internalNotes.map(n => @@ -115,6 +110,11 @@ export default class FieldTripNotes extends Component { ) : '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 index 5fb4e0af8..db358df7e 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -112,33 +112,34 @@ class FieldTripWindows extends Component { + />
    {TABS.map(tab => { const active = tab.id === filter.tab const style = { - borderRadius: 5, backgroundColor: active ? 'navy' : undefined, + borderRadius: 5, color: active ? 'white' : undefined, padding: '2px 3px' } const requestCount = fieldTrip.requests.data.filter(tab.filter).length return ( @@ -154,8 +155,8 @@ class FieldTripWindows extends Component { : visibleRequests.length > 0 ? visibleRequests.map((request, i) => ( )) @@ -203,11 +204,11 @@ class FieldTripRequestRecord extends Component { > + +
    + + {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 ( +
  • + +
  • + ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + callTaker: state.callTaker, + currentQuery: state.otp.currentQuery, + searches: state.otp.searches + } +} + +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-windows.js b/lib/components/admin/field-trip-windows.js index db358df7e..33302e468 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -1,246 +1,49 @@ -import moment from 'moment' 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 FieldTripDetails from './field-trip-details' -import Icon from '../narrative/icon' -import Loading from '../narrative/loading' -import {FETCH_STATUS} from '../../util/constants' +import FieldTripList from './field-trip-list' -// List of tabs used for filtering field trips. -const TABS = [ - {id: 'new', label: 'New', filter: (req) => req.status !== 'cancelled' && (!req.inboundTripStatus || !req.outboundTripStatus)}, - {id: 'planned', label: 'Planned', filter: (req) => req.status !== 'cancelled' && req.inboundTripStatus && req.outboundTripStatus}, - {id: 'cancelled', label: 'Cancelled', filter: (req) => req.status === 'cancelled'}, - {id: 'past', label: 'Past', filter: (req) => req.travelDate && moment(req.travelDate).diff(moment(), 'days') < 0}, - {id: 'all', label: 'All', filter: (req) => true} -] - -// List of fields in field trip object to which user's text search input applies. -const SEARCH_FIELDS = [ - 'address', - 'ccLastFour', - 'ccName', - 'ccType', - 'checkNumber', - 'city', - 'classpassId', - 'emailAddress', - 'endLocation', - 'grade', - 'phoneNumber', - 'schoolName', - 'startLocation', - 'submitterNotes', - 'teacherName' -] +const WINDOW_WIDTH = 450 +const MARGIN = 15 /** * Collects the various draggable windows for the Field Trip module. */ class FieldTripWindows 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) - } - - _onSearchChange = e => { - this.props.setFieldTripFilter({search: e.target.value}) - } - - _onTabChange = e => { - this.props.setFieldTripFilter({tab: e.currentTarget.name}) - } - render () { - const {callTaker, toggleFieldTrips} = this.props + const {callTaker} = this.props const {fieldTrip} = callTaker - const {activeId, filter} = fieldTrip - const {search} = filter - const activeTab = TABS.find(tab => tab.id === filter.tab) - const visibleRequests = fieldTrip.requests.data - .filter(ft => { - let isVisible = false - // First, filter field trip on whether it should be visible for the - // active tab. - if (activeTab) isVisible = activeTab.filter(ft) - // If search input is found, only include field trips with at least one - // field that matches search criteria. - if (search) { - isVisible = SEARCH_FIELDS.some(key => { - const value = ft[key] - let hasMatch = false - if (value) { - hasMatch = value.toLowerCase().indexOf(search.toLowerCase()) !== -1 - } - return hasMatch - }) - } - return isVisible - }) + // Do not render details or list if visible is false. + if (!fieldTrip.visible) return null return ( <> - {fieldTrip.visible - ? -

    - 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={{width: '450px'}} - > - {fieldTrip.requests.status === FETCH_STATUS.FETCHING - ? - : visibleRequests.length > 0 - ? visibleRequests.map((request, i) => ( - - )) - :
    No field trips found.
    - } -
    - : null - } - + + ) } } -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 ( -
  • - -
  • - ) - } -} - const mapStateToProps = (state, ownProps) => { return { - callTaker: state.callTaker, - currentQuery: state.otp.currentQuery, - searches: state.otp.searches + callTaker: state.callTaker } } -const mapDispatchToProps = { - fetchFieldTripDetails: fieldTripActions.fetchFieldTripDetails, - fetchFieldTrips: fieldTripActions.fetchFieldTrips, - setActiveFieldTrip: fieldTripActions.setActiveFieldTrip, - setFieldTripFilter: fieldTripActions.setFieldTripFilter, - toggleFieldTrips: fieldTripActions.toggleFieldTrips -} - -export default connect(mapStateToProps, mapDispatchToProps)(FieldTripWindows) +export default connect(mapStateToProps)(FieldTripWindows) diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 4d26a741a..28d81a590 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -1,25 +1,18 @@ -import coreUtils from '@opentripplanner/core-utils' import update from 'immutability-helper' import moment from 'moment' +import {constructNewCall} from '../util/call-taker' import {FETCH_STATUS} from '../util/constants' -import {getTimestamp} from '../util/state' - -const { randId } = coreUtils.storage - -const UPPER_RIGHT_CORNER = {x: 604, y: 53} -const LOWER_RIGHT_CORNER = {x: 504, y: 365} function createCallTakerReducer () { const initialState = { activeCall: null, callHistory: { - position: UPPER_RIGHT_CORNER, - visible: false, calls: { status: FETCH_STATUS.UNFETCHED, data: [] - } + }, + visible: false }, fieldTrip: { activeId: null, @@ -27,7 +20,6 @@ function createCallTakerReducer () { tab: 'new' }, groupSize: null, - position: LOWER_RIGHT_CORNER, requests: { status: FETCH_STATUS.UNFETCHED, data: [] @@ -39,11 +31,7 @@ function createCallTakerReducer () { return (state = initialState, action) => { switch (action.type) { case 'BEGIN_CALL': { - const newCall = { - startTime: getTimestamp(), - id: randId(), - searches: [] - } + const newCall = constructNewCall() // Initialize new call and show call history window. return update(state, { activeCall: { $set: newCall }, @@ -115,13 +103,20 @@ function createCallTakerReducer () { }) } case 'ROUTING_RESPONSE': { - // If call is in progress, record search ID when a routing response is - // fulfilled. - // TODO: How should we handle routing errors. if (state.activeCall) { + // If call is in progress, record search ID when a routing response is + // fulfilled. + // TODO: How should we handle routing errors. return update(state, { activeCall: { searches: { $push: [action.payload.searchId] } } }) + } else if (state.callHistory.visible) { + // If call not in progress, but history is visible, + // construct new call and add search. + const newCall = constructNewCall() + newCall.searches.push(action.payload.searchId) + // Initialize new call and show call history window. + return update(state, { activeCall: { $set: newCall } }) } // Otherwise, ignore. return state @@ -144,7 +139,10 @@ function createCallTakerReducer () { } case 'TOGGLE_FIELD_TRIPS': { return update(state, { - fieldTrip: { visible: { $set: !state.fieldTrip.visible } } + fieldTrip: { + activeId: { $set: null }, + visible: { $set: !state.fieldTrip.visible } + } }) } case 'END_CALL': { diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index 53bdd2638..e1740911d 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -1,6 +1,8 @@ import {isTransit} from '@opentripplanner/core-utils/lib/itinerary' +import {randId} from '@opentripplanner/core-utils/lib/storage' import {getRoutingParams} from '../actions/api' +import {getTimestamp} from './state' export const TICKET_TYPES = { own_tickets: 'Will use own tickets', @@ -36,6 +38,14 @@ export const PAYMENT_FIELDS = [ {label: 'Check/Money order number', fieldName: 'checkNumber'} ] +export function constructNewCall () { + return { + startTime: getTimestamp(), + id: randId(), + searches: [] + } +} + function placeToLatLonStr (place) { return `${place.lat.toFixed(6)},${place.lon.toFixed(6)}` } @@ -105,7 +115,7 @@ export function createTripPlan (planData, queryParams) { itineraries: [] } if (!planData) return - tripPlan.itineraries = tripPlan.planData.map(itinData => + tripPlan.itineraries = tripPlan.planData.map(itinData => createItinerary(itinData, tripPlan)) const timeBounds = calculateTimeBounds(tripPlan.itineraries) return {...tripPlan, ...timeBounds} From 401d3c8f91502b751265b3449b78cabdc2c3b1f1 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 10 Feb 2021 13:49:08 -0500 Subject: [PATCH 21/35] refactor(call-taker): hide advanced options when trip is planned --- lib/components/app/call-taker-panel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index a71099ef2..c4113baea 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -44,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() } @@ -130,7 +131,7 @@ class CallTakerPanel extends Component { right: '0px', left: '0px', padding: '0px 8px 5px', - display: expandAdvanced ? 'none' : undefined + display: expandAdvanced ? undefined : 'none' } return ( From 8b70703a71bde64d4e8ff7c096fd4aa060a5766a Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 10 Feb 2021 14:03:35 -0500 Subject: [PATCH 22/35] refactor(call-taker-panel): fix liint --- lib/components/app/call-taker-panel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index c4113baea..a15366b49 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -131,7 +131,7 @@ class CallTakerPanel extends Component { right: '0px', left: '0px', padding: '0px 8px 5px', - display: expandAdvanced ? undefined : 'none' + display: expandAdvanced ? undefined : 'none' } return ( From cf6212beed1370ae8e7256782f1ef65039b07fa2 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 10 Feb 2021 14:50:25 -0500 Subject: [PATCH 23/35] refactor(field-trip): address PR comments --- lib/components/admin/call-taker-controls.js | 33 +++++++++++++-------- lib/components/admin/call-taker-windows.js | 3 +- lib/components/admin/editable-section.js | 10 +++---- lib/components/admin/field-trip-details.js | 32 ++++++++++++-------- lib/components/admin/field-trip-list.js | 5 ++-- lib/components/admin/styled.js | 10 +++++-- lib/components/admin/trip-status.js | 16 +++++----- 7 files changed, 65 insertions(+), 44 deletions(-) diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index d429c820c..61626c7a9 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -123,37 +123,44 @@ class CallTakerControls extends Component { {/* Call History toggle button */} {callTakerEnabled && } {/* Field Trip toggle button TODO */} {fieldTripEnabled && } diff --git a/lib/components/admin/call-taker-windows.js b/lib/components/admin/call-taker-windows.js index c920ba75b..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 @@ -17,7 +18,7 @@ class CallTakerWindows extends Component { if (!callHistory.visible) return null return ( Call history} + header={ Call history} onClickClose={toggleCallHistory} style={{right: '15px', top: '50px'}} > diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js index 57c089e35..133f39de1 100644 --- a/lib/components/admin/editable-section.js +++ b/lib/components/admin/editable-section.js @@ -2,7 +2,7 @@ import React, {Component} from 'react' import { Button, - P, + Para, Val } from './styled' @@ -50,7 +50,7 @@ export default class EditableSection extends Component { } render () { - const {fields, children, inputStyle, request, valueFirst} = this.props + const {children, fields, inputStyle, request, valueFirst} = this.props const {isEditing} = this.state if (!request) return null return ( @@ -91,21 +91,21 @@ export default class EditableSection extends Component { {fields.map(f => { const input = ( ) return ( -

    + {valueFirst ? <>{input} {f.label} : <>{f.label}: {input} } -

    + ) })} diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 1f8aa2ede..7bb2fc4d6 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -3,6 +3,7 @@ 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' @@ -10,16 +11,17 @@ import EditableSection from './editable-section' import FieldTripNotes from './field-trip-notes' import Icon from '../narrative/icon' import { - B, + Bold, Button, Container, FullWithMargin, Half, Header, InlineHeader, - P, + Para, Text, - Val + Val, + WindowHeader as DefaultWindowHeader } from './styled' import TripStatus from './trip-status' import Updatable from './updatable' @@ -30,6 +32,10 @@ import { TICKET_TYPES } from '../../util/call-taker' +const WindowHeader = styled(DefaultWindowHeader)` + margin-bottom: 0px; +` + /** * Shows the details for the active Field Trip Request. */ @@ -91,7 +97,7 @@ class FieldTripDetails extends Component { href={this._getRequestLink('feedbackForm', true)} target='_blank' > - Feedback link + Feedback link
    } header={ -

    + {schoolName} Trip (#{id})
    @@ -119,7 +125,7 @@ class FieldTripDetails extends Component { ({travelDateAsMoment.fromNow()})
    -

    + } height='375px' onClickClose={this._onCloseActiveFieldTrip} @@ -128,24 +134,24 @@ class FieldTripDetails extends Component {
    Group Information
    -

    {schoolName}

    -

    Teacher: {teacherName}

    -

    Ticket type: {TICKET_TYPES[ticketType]}

    -

    Invoice required: {invoiceRequired ? 'Yes' : 'No'}

    -

    + {schoolName} + Teacher: {teacherName} + Ticket type: {TICKET_TYPES[ticketType]} + Invoice required: {invoiceRequired ? 'Yes' : 'No'} + } onUpdate={this._editSubmitterNotes} value={submitterNotes} /> -

    +
    - {getGroupSize(request)} total group size + {getGroupSize(request)} total group size } fields={GROUP_FIELDS} diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 2e0701dcc..eee84f397 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -7,6 +7,7 @@ import * as fieldTripActions from '../../actions/field-trip' import DraggableWindow from './draggable-window' import Icon from '../narrative/icon' import Loading from '../narrative/loading' +import {WindowHeader} from './styled' import {FETCH_STATUS} from '../../util/constants' // List of tabs used for filtering field trips. @@ -95,7 +96,7 @@ class FieldTripList extends Component { -

    + Field Trip Requests{' '}

    + {TABS.map(tab => { const active = tab.id === filter.tab const style = { diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js index aae999930..942007575 100644 --- a/lib/components/admin/styled.js +++ b/lib/components/admin/styled.js @@ -1,7 +1,7 @@ import { Button as BsButton } from 'react-bootstrap' import styled, {css} from 'styled-components' -export const B = styled.strong`` +export const Bold = styled.strong`` export const Button = styled(BsButton)` margin-left: 5px; @@ -38,7 +38,7 @@ export const textCss = css` margin-bottom: 0px; ` -export const P = styled.p` +export const Para = styled.p` ${textCss} ` @@ -52,3 +52,9 @@ export const Val = styled.span` 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 index 2b96cab66..741b12912 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -7,11 +7,11 @@ import * as fieldTripActions from '../../actions/field-trip' import * as formActions from '../../actions/form' import Icon from '../narrative/icon' import { - B, + Bold, Button, Full, Header, - P + Para } from './styled' import { getTripFromRequest } from '../../util/call-taker' @@ -74,19 +74,19 @@ class TripStatus extends Component { -

    From {start} to {end}

    + From {start} to {end} {outbound - ?

    + ? Arriving at {this._formatTime(arriveDestinationTime)} -

    + : <> -

    + Leave at {this._formatTime(leaveDestinationTime)},{' '} due back at {this._formatTime(arriveSchoolTime)} -

    + } -

    {this._formatTripStatus()}

    + {this._formatTripStatus()} ) } From 4a2cca2f4eb8262b0a6b19a3af7706e8437aaaf7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 10 Feb 2021 17:59:10 -0500 Subject: [PATCH 24/35] refactor(EditableSection): Use Formik for event/form state handling. --- lib/components/admin/editable-section.js | 167 +++++++++++------------ 1 file changed, 77 insertions(+), 90 deletions(-) diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js index 133f39de1..e4eed7247 100644 --- a/lib/components/admin/editable-section.js +++ b/lib/components/admin/editable-section.js @@ -1,3 +1,4 @@ +import { Field, Form, Formik } from 'formik' import React, {Component} from 'react' import { @@ -13,40 +14,23 @@ import { */ 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._exists(this.props.request[fieldName]) - ? this.props.request[fieldName] - : '' + _getVal = (fieldName) => this._exists(this.props.request[fieldName]) + ? this.props.request[fieldName] + : '' - _onChange = (fieldName, value) => { - const stateUpdate = this.state.data - stateUpdate[fieldName] = value - this.setState({data: stateUpdate}) - } - - _onClickSave = () => { + _onClickSave = data => { 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}) + this.setState({isEditing: false}) } _toggleEditing = () => { - const stateUpdate = {isEditing: !this.state.isEditing} - if (this.state.isEditing) { - stateUpdate.data = {} - } - this.setState(stateUpdate) + this.setState({isEditing: !this.state.isEditing}) } render () { @@ -54,61 +38,71 @@ export default class EditableSection extends Component { const {isEditing} = this.state if (!request) return null return ( - <> - {children} - - {!isEditing - ? - : <> - - - - } - - {fields.map(f => { - const input = ( - + {!isEditing + ? + : <> + + + + } + + {fields.map(f => { + const input = ( + + ) + return ( + + {valueFirst + ? <>{input} {f.label} + : <>{f.label}: {input} + } + + ) + })} + ) - return ( - - {valueFirst - ? <>{input} {f.label} - : <>{f.label}: {input} - } - - ) - })} - + } + ) } } @@ -119,32 +113,25 @@ export default class EditableSection extends Component { * text, number, or select). */ 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 ( - + ) } else { - return } } From 4caafb011663dccc4784b55b4628044ab91f8221 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 11 Feb 2021 16:29:21 -0500 Subject: [PATCH 25/35] refactor(EditableSection): Remove nulls from data sent to backend. Also fix key required warning. --- lib/components/admin/editable-section.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js index e4eed7247..e5b8cf27f 100644 --- a/lib/components/admin/editable-section.js +++ b/lib/components/admin/editable-section.js @@ -25,6 +25,12 @@ export default class EditableSection extends Component { _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}) } @@ -86,6 +92,7 @@ export default class EditableSection extends Component { fieldName={f.fieldName} inputProps={f.inputProps} isEditing={isEditing} + key={f.fieldName} options={f.options} style={inputStyle} value={this._getVal(f.fieldName)} /> From 388a807c49389e10f77eaeef617519c4a4ab5a84 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 12 Feb 2021 15:17:17 -0500 Subject: [PATCH 26/35] refactor(field-trip): change total group size label --- lib/components/admin/field-trip-details.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 7bb2fc4d6..55d13e885 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -151,7 +151,7 @@ class FieldTripDetails extends Component { - {getGroupSize(request)} total group size + {getGroupSize(request)} total } fields={GROUP_FIELDS} From 51ebfc426d441a84ade5454322cc37e5dfd3a976 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 16 Feb 2021 13:19:26 -0500 Subject: [PATCH 27/35] fix(call-taker-controls): fix container css fix #330 --- lib/components/admin/call-taker-controls.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index 61626c7a9..a92fd99ee 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -13,6 +13,10 @@ const BLUE = '#1C4D89' const GREEN = '#6B931B' const PURPLE = '#8134D3' +const ControlsContainer = styled.div` + position: relative; +` + /** * This component displays the controls for the Call Taker/Field Trip modules, * including: @@ -84,7 +88,7 @@ class CallTakerControls extends Component { boxShadow: '2px 2px 4px #000000' } return ( - <> + {/* Start/End Call button */} {callTakerEnabled && } - + ) } } From 3db2cdb06f3fe14173e72e44f04bf57f21ec584c Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 16 Feb 2021 15:07:25 -0500 Subject: [PATCH 28/35] refactor(field-trip): address PR comments, use styled-components --- lib/components/admin/call-record.js | 2 +- lib/components/admin/call-taker-controls.js | 87 +++++---------------- lib/components/admin/call-time-counter.js | 9 ++- lib/components/admin/field-trip-list.js | 23 +++--- lib/components/admin/field-trip-notes.js | 43 +++++----- lib/components/admin/field-trip-windows.js | 55 ++++++------- lib/components/admin/styled.js | 74 ++++++++++++++++++ 7 files changed, 158 insertions(+), 135 deletions(-) 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 a92fd99ee..a5f4ff803 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -5,24 +5,20 @@ import * as apiActions from '../../actions/api' import * as callTakerActions from '../../actions/call-taker' import * as fieldTripActions from '../../actions/field-trip' import * as uiActions from '../../actions/ui' -import CallTimeCounter from './call-time-counter' import Icon from '../narrative/icon' - -const RED = '#C35134' -const BLUE = '#1C4D89' -const GREEN = '#6B931B' -const PURPLE = '#8134D3' - -const ControlsContainer = styled.div` - position: relative; -` +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 { @@ -46,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 ( @@ -78,94 +74,47 @@ class CallTakerControls extends Component { 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 */} {callTakerEnabled && - + } - {/* Field Trip toggle button TODO */} + {/* Field Trip toggle button */} {fieldTripEnabled && - + } ) 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/field-trip-list.js b/lib/components/admin/field-trip-list.js index eee84f397..61ddeae27 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -7,7 +7,7 @@ import * as fieldTripActions from '../../actions/field-trip' import DraggableWindow from './draggable-window' import Icon from '../narrative/icon' import Loading from '../narrative/loading' -import {WindowHeader} from './styled' +import {FieldTripRecordButton, WindowHeader} from './styled' import {FETCH_STATUS} from '../../util/constants' // List of tabs used for filtering field trips. @@ -194,26 +194,29 @@ class FieldTripRequestRecord extends Component { className='list-unstyled' style={style} > - + + + Submitted by {teacherName} on {timeStamp} + + + {startLocation} to {endLocation} + + ) } diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js index 662aaba4f..a190f5f5d 100644 --- a/lib/components/admin/field-trip-notes.js +++ b/lib/components/admin/field-trip-notes.js @@ -18,30 +18,26 @@ const Footer = styled.footer` font-size: x-small; ` -const Note = ({note, onClickDelete}) => { - return ( -
    - onClickDelete(note)} - > - - - {note.note} -
    {note.userName} on {note.timeStamp}
    -
    - ) -} +const Note = ({note, onClickDelete}) => ( +
    + onClickDelete(note)} + > + + + {note.note} +
    {note.userName} on {note.timeStamp}
    +
    +) -const Feedback = ({feedback}) => { - return ( -
    - {feedback.feedback} -
    {feedback.userName} on {feedback.timeStamp}
    -
    - ) -} +const Feedback = ({feedback}) => ( +
    + {feedback.feedback} +
    {feedback.userName} on {feedback.timeStamp}
    +
    +) /** * Renders the various notes/feedback for a field trip request. */ @@ -68,7 +64,6 @@ 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}"?`)) { - console.log('OK deleting') deleteFieldTripNote(request, note.id) } } diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js index 33302e468..ddb6ee7ca 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import { connect } from 'react-redux' import FieldTripDetails from './field-trip-details' @@ -10,34 +10,31 @@ const MARGIN = 15 /** * Collects the various draggable windows for the Field Trip module. */ -class FieldTripWindows extends Component { - render () { - const {callTaker} = this.props - const {fieldTrip} = callTaker - // Do not render details or list if visible is false. - if (!fieldTrip.visible) return null - return ( - <> - - - - ) - } +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) => { diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js index 942007575..c5f53259d 100644 --- a/lib/components/admin/styled.js +++ b/lib/components/admin/styled.js @@ -1,6 +1,71 @@ 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)` @@ -16,6 +81,15 @@ 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% ` From d79da54c3b6fcef16d0c2f301d185ad08dc95a70 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 4 Mar 2021 14:49:19 -0500 Subject: [PATCH 29/35] refactor(field-trip): add reactivate request feature --- lib/components/admin/field-trip-details.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 55d13e885..db2fefdee 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -47,10 +47,14 @@ class FieldTripDetails extends Component { _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null) - _onClickCancel = () => { + _onToggleStatus = () => { const {request, setRequestStatus} = this.props - if (confirm('Are you sure you want to cancel this request? Any associated trips will be deleted.')) { - setRequestStatus(request, 'cancelled') + 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') } } @@ -70,6 +74,7 @@ class FieldTripDetails extends Component { invoiceRequired, notes, schoolName, + status, submitterNotes, teacherName, ticketType, @@ -82,6 +87,7 @@ class FieldTripDetails extends Component { else operationalNotes.push(note) }) const travelDateAsMoment = moment(travelDate) + const cancelled = status === 'cancelled' return (
    } From 0de242b48f1ec0105016538d6029cb2763cdb558 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 4 Mar 2021 15:27:06 -0500 Subject: [PATCH 30/35] refactor(field-trip): improve search performance --- lib/components/admin/draggable-window.js | 2 + lib/components/admin/field-trip-details.js | 1 - lib/components/admin/field-trip-list.js | 44 +++++++++++++++------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 3fbfba51d..a43789139 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -19,6 +19,8 @@ export default class DraggableWindow extends Component { const GREY_BORDER = '#777 1.3px solid' return ( diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index db2fefdee..fa2da4f9c 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -214,7 +214,6 @@ const mapDispatchToProps = { editSubmitterNotes: fieldTripActions.editSubmitterNotes, fetchQueries: fieldTripActions.fetchQueries, setActiveFieldTrip: fieldTripActions.setActiveFieldTrip, - setFieldTripFilter: fieldTripActions.setFieldTripFilter, setRequestGroupSize: fieldTripActions.setRequestGroupSize, setRequestPaymentInfo: fieldTripActions.setRequestPaymentInfo, setRequestStatus: fieldTripActions.setRequestStatus, diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 61ddeae27..8516421db 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -58,8 +58,21 @@ class FieldTripList extends Component { this.props.setActiveFieldTrip(null) } - _onSearchChange = e => { - this.props.setFieldTripFilter({search: e.target.value}) + /** + * 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 => { @@ -81,13 +94,18 @@ class FieldTripList extends Component { // If search input is found, only include field trips with at least one // field that matches search criteria. if (search) { - isVisible = SEARCH_FIELDS.some(key => { - const value = ft[key] - let hasMatch = false - if (value) { - hasMatch = value.toLowerCase().indexOf(search.toLowerCase()) !== -1 - } - return hasMatch + // Split the search terms by whitespace and check that the request has + // values that match every term. + const searchTerms = search.toLowerCase().split(' ') + isVisible = searchTerms.every(term => { + return SEARCH_FIELDS.some(key => { + const value = (ft[key] || '').toLowerCase() + let hasMatch = false + if (value) { + hasMatch = value.indexOf(term) !== -1 + } + return hasMatch + }) }) } return isVisible @@ -107,15 +125,15 @@ class FieldTripList extends Component { From b58c8977f34d7b87ceca1e9e6c832742d2b1850c Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 4 Mar 2021 16:27:31 -0500 Subject: [PATCH 31/35] refactor(field-trip): address PR #230 comments --- lib/actions/field-trip.js | 31 ++++-- lib/components/admin/field-trip-details.js | 110 +++++++++++---------- lib/components/admin/field-trip-list.js | 61 +----------- lib/util/call-taker.js | 109 ++++++++++++++++++++ 4 files changed, 197 insertions(+), 114 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index fdcdc01f9..652ec9270 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -28,16 +28,20 @@ export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') * Fetch all field trip requests (as summaries). */ export function fetchFieldTrips () { - return function (dispatch, getState) { + return async function (dispatch, getState) { dispatch(requestingFieldTrips()) const {callTaker, otp} = getState() if (sessionIsInvalid(callTaker.session)) return const {datastoreUrl} = otp.config const {sessionId} = callTaker.session - fetch(`${datastoreUrl}/fieldtrip/getRequestsSummary?${qs.stringify({sessionId})}`) - .then(res => res.json()) - .then(fieldTrips => dispatch(receivedFieldTrips({fieldTrips}))) - .catch(err => alert(`Error fetching field trips: ${JSON.stringify(err)}`)) + 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})) } } @@ -68,7 +72,6 @@ export function addFieldTripNote (request, note) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return - console.log(callTaker.session) const {sessionId, username} = callTaker.session const queryData = new FormData() queryData.append('sessionId', sessionId) @@ -200,6 +203,20 @@ export function saveRequestTrip (request, outbound, groupPlan) { } } +/** + * @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 { @@ -225,7 +242,7 @@ function checkPlanValidity (request, groupPlan) { // FIXME More checks? E.g., origin/destination - return { isValid: true } + return { isValid: true, message: null } } // TODO: Enable saveTrip for field trip request. diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index fa2da4f9c..91d2f2af6 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -58,10 +58,66 @@ class FieldTripDetails extends Component { } } + _renderFooter = () => { + const cancelled = this.props.request.status === 'cancelled' + return ( +
    + + + Feedback link + + + Receipt link + + + +
    + ) + } + + _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, - dateFormat, deleteFieldTripNote, request, setRequestGroupSize, @@ -70,15 +126,12 @@ class FieldTripDetails extends Component { } = this.props if (!request) return null const { - id, invoiceRequired, notes, schoolName, - status, submitterNotes, teacherName, - ticketType, - travelDate + ticketType } = request const internalNotes = [] const operationalNotes = [] @@ -86,53 +139,10 @@ class FieldTripDetails extends Component { if (note.type === 'internal') internalNotes.push(note) else operationalNotes.push(note) }) - const travelDateAsMoment = moment(travelDate) - const cancelled = status === 'cancelled' return ( - - - Feedback link - - - Receipt link - - - - - } - header={ - - {schoolName} Trip (#{id}) -
    - - Travel date: {travelDateAsMoment.format(dateFormat)}{' '} - ({travelDateAsMoment.fromNow()}) - -
    -
    - } + footer={this._renderFooter()} + header={this._renderHeader()} height='375px' onClickClose={this._onCloseActiveFieldTrip} style={style} diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 8516421db..84152aa2d 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -1,4 +1,3 @@ -import moment from 'moment' import React, { Component } from 'react' import { Badge } from 'react-bootstrap' import { connect } from 'react-redux' @@ -8,36 +7,9 @@ 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' -// List of tabs used for filtering field trips. -const TABS = [ - {id: 'new', label: 'New', filter: (req) => req.status !== 'cancelled' && (!req.inboundTripStatus || !req.outboundTripStatus)}, - {id: 'planned', label: 'Planned', filter: (req) => req.status !== 'cancelled' && req.inboundTripStatus && req.outboundTripStatus}, - {id: 'cancelled', label: 'Cancelled', filter: (req) => req.status === 'cancelled'}, - {id: 'past', label: 'Past', filter: (req) => req.travelDate && moment(req.travelDate).diff(moment(), 'days') < 0}, - {id: 'all', label: 'All', filter: (req) => true} -] - -// List of fields in field trip object to which user's text search input applies. -const SEARCH_FIELDS = [ - 'address', - 'ccLastFour', - 'ccName', - 'ccType', - 'checkNumber', - 'city', - 'classpassId', - 'emailAddress', - 'endLocation', - 'grade', - 'phoneNumber', - 'schoolName', - 'startLocation', - 'submitterNotes', - 'teacherName' -] - /** * Displays a searchable list of field trip requests in a draggable window. */ @@ -80,36 +52,10 @@ class FieldTripList extends Component { } render () { - const {callTaker, style, toggleFieldTrips} = this.props + const {callTaker, style, toggleFieldTrips, visibleRequests} = this.props const {fieldTrip} = callTaker const {activeId, filter} = fieldTrip const {search} = filter - const activeTab = TABS.find(tab => tab.id === filter.tab) - const visibleRequests = fieldTrip.requests.data - .filter(ft => { - let isVisible = false - // First, filter field trip on whether it should be visible for the - // active tab. - if (activeTab) isVisible = activeTab.filter(ft) - // If search input is found, only include field trips with at least one - // field that matches search criteria. - if (search) { - // Split the search terms by whitespace and check that the request has - // values that match every term. - const searchTerms = search.toLowerCase().split(' ') - isVisible = searchTerms.every(term => { - return SEARCH_FIELDS.some(key => { - const value = (ft[key] || '').toLowerCase() - let hasMatch = false - if (value) { - hasMatch = value.indexOf(term) !== -1 - } - return hasMatch - }) - }) - } - return isVisible - }) return ( { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, - searches: state.otp.searches + searches: state.otp.searches, + visibleRequests: getVisibleRequests(state) } } diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index e1740911d..d4717b623 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -1,5 +1,6 @@ import {isTransit} from '@opentripplanner/core-utils/lib/itinerary' import {randId} from '@opentripplanner/core-utils/lib/storage' +import moment from 'moment' import {getRoutingParams} from '../actions/api' import {getTimestamp} from './state' @@ -38,6 +39,57 @@ export const PAYMENT_FIELDS = [ {label: 'Check/Money order number', fieldName: 'checkNumber'} ] +// List of tabs used for filtering field trips. +export const TABS = [ + { + id: 'new', + label: 'New', + filter: (req) => req.status !== 'cancelled' && + (!req.inboundTripStatus || !req.outboundTripStatus) + }, + { + id: 'planned', + label: 'Planned', + filter: (req) => req.status !== 'cancelled' && + req.inboundTripStatus && req.outboundTripStatus + }, + { + id: 'cancelled', + label: 'Cancelled', + filter: (req) => req.status === 'cancelled' + }, + { + id: 'past', + label: 'Past', + filter: (req) => req.travelDate && + moment(req.travelDate).diff(moment(), 'days') < 0 + }, + { + id: 'all', + label: 'All', + filter: (req) => true + } +] + +// List of fields in field trip object to which user's text search input applies. +const SEARCH_FIELDS = [ + 'address', + 'ccLastFour', + 'ccName', + 'ccType', + 'checkNumber', + 'city', + 'classpassId', + 'emailAddress', + 'endLocation', + 'grade', + 'phoneNumber', + 'schoolName', + 'startLocation', + 'submitterNotes', + 'teacherName' +] + export function constructNewCall () { return { startTime: getTimestamp(), @@ -61,6 +113,44 @@ export function sessionIsInvalid (session) { return false } +/** + * Get visible field trip requests according to the currently selected tab and + * search terms. + */ +export function getVisibleRequests (state) { + const {callTaker} = state + const {fieldTrip} = callTaker + const {filter} = fieldTrip + const {search, tab} = filter + const activeTab = TABS.find(t => t.id === tab) + return fieldTrip.requests.data.filter(request => { + let includedInTab = false + let includedInSearch = false + // First, filter field trip on whether it should be visible for the + // active tab. + if (activeTab) includedInTab = activeTab.filter(request) + // If search input is found, only include field trips with at least one + // field that matches search criteria. + if (search) { + // Split the search terms by whitespace and check that the request has + // values that match every term. + const searchTerms = search.toLowerCase().split(' ') + includedInSearch = searchTerms.every(term => { + return SEARCH_FIELDS.some(key => { + const value = (request[key] || '').toLowerCase() + let hasMatch = false + if (value) { + hasMatch = value.indexOf(term) !== -1 + } + return hasMatch + }) + }) + return includedInTab && includedInSearch + } + return includedInTab + }) +} + /** * Utility to map an OTP MOD UI search object to a Call Taker datastore query * object. @@ -106,6 +196,13 @@ export function getTripFromRequest (request, outbound = false) { return trip } +/** + * Create trip plan from plan data with itineraries. Note: this is based on + * original code in OpenTripPlanner: + * https://github.com/ibi-group/OpenTripPlanner/blob/fdf972e590b809014e3f80160aeb6dde209dd1d4/src/client/js/otp/modules/planner/TripPlan.js#L27-L38 + * + * FIXME: This still needs to be implemented for the field trip module. + */ export function createTripPlan (planData, queryParams) { const tripPlan = { earliestStartTime: null, @@ -121,6 +218,13 @@ export function createTripPlan (planData, queryParams) { return {...tripPlan, ...timeBounds} } +/** + * Calculate time bounds for all of the itineraries. Note: this is based on + * original code in OpenTripPlanner: + * https://github.com/ibi-group/OpenTripPlanner/blob/fdf972e590b809014e3f80160aeb6dde209dd1d4/src/client/js/otp/modules/planner/TripPlan.js#L53-L66 + * + * FIXME: This still needs to be implemented for the field trip module. + */ function calculateTimeBounds (itineraries) { let earliestStartTime = null let latestEndTime = null @@ -132,8 +236,13 @@ function calculateTimeBounds (itineraries) { ? itin.getEndTime() : latestEndTime }) + return {earliestStartTime, latestEndTime} } +/** + * Create itinerary. Note this is based on original code in OpenTripPlanner: + * https://github.com/ibi-group/OpenTripPlanner/blob/46e1f9ffd9a55f0c5409d25a34769cdaff2d8cbb/src/client/js/otp/modules/planner/Itinerary.js#L27-L40 + */ function createItinerary (itinData, tripPlan) { const itin = { itinData, From cd0a8fed364c696433f7846c5879bd26ff567317 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 8 Mar 2021 10:17:28 -0500 Subject: [PATCH 32/35] refactor(app-menu): preserve sessionId on start over --- lib/components/app/app-menu.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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)) From 6fd2676c6873e54c6810c570238f83c1f8184826 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 8 Mar 2021 10:26:33 -0500 Subject: [PATCH 33/35] refactor(field-trip): use object-to-formdata lib --- lib/actions/call-taker.js | 23 ++++++------ lib/actions/field-trip.js | 73 ++++++++++++++++++--------------------- package.json | 1 + yarn.lock | 5 +++ 4 files changed, 51 insertions(+), 51 deletions(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index d03bbffc7..5a48b8284 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -1,4 +1,5 @@ import { getUrlParams } from '@opentripplanner/core-utils/lib/query' +import { serialize } from 'object-to-formdata' import qs from 'qs' import { createAction } from 'redux-actions' @@ -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} ) @@ -144,13 +149,7 @@ 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} ) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 652ec9270..0e8927ee3 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -1,6 +1,7 @@ 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' @@ -73,14 +74,13 @@ export function addFieldTripNote (request, note) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return 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('requestId', request.id) + const noteData = serialize({ + sessionId, + note: {...note, userName: username}, + requestId: request.id + }) return fetch(`${datastoreUrl}/fieldtrip/addNote`, - {method: 'POST', body: queryData} + {method: 'POST', body: noteData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -98,11 +98,8 @@ export function deleteFieldTripNote (request, noteId) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('noteId', noteId) return fetch(`${datastoreUrl}/fieldtrip/deleteNote`, - {method: 'POST', body: queryData} + {method: 'POST', body: serialize({ noteId, sessionId })} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -171,12 +168,13 @@ export function editSubmitterNotes (request, submitterNotes) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('notes', submitterNotes) - queryData.append('requestId', request.id) + const noteData = serialize({ + notes: submitterNotes, + requestId: request.id, + sessionId + }) return fetch(`${datastoreUrl}/fieldtrip/editSubmitterNotes`, - {method: 'POST', body: queryData} + {method: 'POST', body: noteData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -365,7 +363,8 @@ function planInbound (request) { } /** - * Set group size for a field trip request. + * 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) { @@ -373,14 +372,13 @@ export function setRequestGroupSize (request, groupSize) { 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) + const groupSizeData = serialize({ + ...groupSize, + requestId: request.id, + sessionId + }) return fetch(`${datastoreUrl}/fieldtrip/setRequestGroupSize`, - {method: 'POST', body: queryData} + {method: 'POST', body: groupSizeData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -398,17 +396,13 @@ export function setRequestPaymentInfo (request, paymentInfo) { 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) + const paymentData = serialize({ + ...paymentInfo, + requestId: request.id, + sessionId + }) return fetch(`${datastoreUrl}/fieldtrip/setRequestPaymentInfo`, - {method: 'POST', body: queryData} + {method: 'POST', body: paymentData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -426,12 +420,13 @@ export function setRequestStatus (request, status) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('status', status) - queryData.append('requestId', request.id) + const statusData = serialize({ + requestId: request.id, + sessionId, + status + }) return fetch(`${datastoreUrl}/fieldtrip/setRequestStatus`, - {method: 'POST', body: queryData} + {method: 'POST', body: statusData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { diff --git a/package.json b/package.json index fcd696904..84fb839d7 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "moment-timezone": "^0.5.23", "object-hash": "^1.3.1", "object-path": "^0.11.5", + "object-to-formdata": "^4.1.0", "prop-types": "^15.6.0", "qs": "^6.3.0", "react": "^16.9.0", diff --git a/yarn.lock b/yarn.lock index 5b5d81889..e4bf37728 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11552,6 +11552,11 @@ object-path@^0.11.5: resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.5.tgz#d4e3cf19601a5140a55a16ad712019a9c50b577a" integrity sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg== +object-to-formdata@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-4.1.0.tgz#0d7bdd6f9e4efa8c0075a770c11ec92b7bf6c560" + integrity sha512-4Ti3VLTspWOUt5QIBl5/BpvLBnr4tbFpZ/FpXKWQFqLslvGIB3ug3jurW/k8iIpoyE6HcZhhmQ6mFiOq1tKGEQ== + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" From 94247f8fadc5ab8fd694d054ef4a78d0e5dccdb1 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 12 Mar 2021 13:37:29 -0500 Subject: [PATCH 34/35] refactor(field-trip): add search by request date --- lib/util/call-taker.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index d4717b623..f7138f281 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -136,6 +136,16 @@ export function getVisibleRequests (state) { // values that match every term. const searchTerms = search.toLowerCase().split(' ') includedInSearch = searchTerms.every(term => { + const splitBySlash = term.split('/') + if (splitBySlash.length > 1) { + // Potential date format detected in search term. + const [month, day] = splitBySlash + if (!isNaN(month) && !isNaN(day)) { + // If month and day seem to be numbers, check against request date. + const date = moment(request.travelDate) + return date.month() + 1 === +month && date.date() === +day + } + } return SEARCH_FIELDS.some(key => { const value = (request[key] || '').toLowerCase() let hasMatch = false From e8cda1bd131fb1354e32d03268bb94e2ec5583cb Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 2 Apr 2021 14:01:31 -0400 Subject: [PATCH 35/35] refactor(field-trip): remove commented out code --- lib/actions/field-trip.js | 114 -------------------------------------- 1 file changed, 114 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 0e8927ee3..69d072347 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -108,57 +108,6 @@ export function deleteFieldTripNote (request, noteId) { } } -// TODO: Enable processPlan when planning/saving trip for field trip request. -// 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. */ @@ -243,69 +192,6 @@ function checkPlanValidity (request, groupPlan) { return { isValid: true, message: null } } -// TODO: Enable saveTrip for field trip request. -// 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)))