Skip to content

Commit

Permalink
Merge pull request #1302 from opentripplanner/plan-trip-on-behalf-of
Browse files Browse the repository at this point in the history
feat(trusted-companions): Plan trip with mobility profile for self or on behalf of trusted companion
  • Loading branch information
josh-willis-arcadis authored Dec 10, 2024
2 parents 7fbe6d6 + f5f8ebd commit 7256079
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 16 deletions.
7 changes: 6 additions & 1 deletion i18n/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ actions:
confirmDeletePlace: Would you like to remove this place?
emailVerificationResent: The email verification message has been resent.
genericError: "An error was encountered: {err}"
getDependentUserInfoFailed: Error getting mobility profile information.
itineraryExistenceCheckFailed: Error checking whether your selected trip is possible.
mustAcceptTermsToSavePlace: Please accept the Terms of Use (under My Account) to save locations.
mustBeLoggedInToSavePlace: Please log in to save locations.
Expand Down Expand Up @@ -197,7 +198,6 @@ components:
BatchSearchScreen:
advancedHeader: Advanced Preferences
header: Plan Your Trip
modeOptions: Mode Options
modeSelectorLabel: Select a travel mode
moreOptions: More options
saveAndReturn: Save and return
Expand Down Expand Up @@ -339,10 +339,15 @@ components:
header: Mobility Profile
mobilityDevices: "Mobility devices: "
mobilityLimitations: "Mobility limitations: "
planTripDescription: >-
If you have a travel companion, you can choose to plan this trip
according to their mobility profile.
visionLimitations: "Vision limitations: "
dropdownLabel: "User mobility profile:"
intro: >-
Please answer a few questions to customize the trip planning experience to
your needs and preferences.
myself: Myself
title: Define Your Mobility Profile
NarrativeItinerariesHeader:
changeSortDir: Change sort direction
Expand Down
8 changes: 7 additions & 1 deletion i18n/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ actions:
Le message de vérification de votre adresse e-mail a été envoyé de
nouveau.
genericError: "Une erreur s'est produite : {err}"
getDependentUserInfoFailed: Erreur lors de l'obtention des profils de mobilité.
itineraryExistenceCheckFailed: Erreur lors de la vérification de la validité du trajet choisi.
mustAcceptTermsToSavePlace: >-
Veuillez accepter les conditions d'utilisation (dans Mon compte) pour
Expand Down Expand Up @@ -208,7 +209,6 @@ components:
BatchSearchScreen:
advancedHeader: Préférences avancées
header: Votre trajet
modeOptions: Choix du mode
modeSelectorLabel: Sélectionnez un mode de déplacement
moreOptions: Plus d'options
saveAndReturn: Enregistrer et fermer
Expand Down Expand Up @@ -356,10 +356,16 @@ components:
header: Profil mobilité
mobilityDevices: "Appareils d'aide : "
mobilityLimitations: "Handicaps moteurs : "
planTripDescription: >-
Vous pouvez rechercher des trajets adaptés au profil mobilité des
personnes que vous accompagnez. Pour ajouter des personnes
accompagnatrices, allez dans <manageLink>Préférences</manageLink>.
visionLimitations: "Handicaps visuels : "
dropdownLabel: "Profil à utiliser :"
intro: >-
Veuillez répondre a quelques questions pour personaliser vos recherches de
trajets selon vos besoins et préférences.
myself: Moi-même
title: Spécifiez votre profil de mobilité
NarrativeItinerariesHeader:
changeSortDir: Changer l'ordre de tri
Expand Down
4 changes: 3 additions & 1 deletion lib/actions/apiV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,9 @@ export function routingQuery(searchId = null, updateSearchInReducer) {
numItineraries: numItineraries || getDefaultNumItineraries(config)
}
if (config.mobilityProfile) {
baseQuery.mobilityProfile = loggedInUser?.mobilityProfile?.mobilityMode
baseQuery.mobilityProfile =
currentQuery.mobilityProfile ||
loggedInUser?.mobilityProfile?.mobilityMode
}
// Generate combinations if the modes for query are not specified in the query
// FIXME: BICYCLE_RENT does not appear in this list unless TRANSIT is also enabled.
Expand Down
24 changes: 24 additions & 0 deletions lib/actions/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const setCurrentUserMonitoredTrips = createAction(
const setCurrentUserTripRequests = createAction(
'SET_CURRENT_USER_TRIP_REQUESTS'
)
const setDependentUserInfo = createAction('SET_DEPENDENT_USER_INFO')
const setLastPhoneSmsRequest = createAction('SET_LAST_PHONE_SMS_REQUEST')
export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')
export const clearItineraryExistence = createAction('CLEAR_ITINERARY_EXISTENCE')
Expand Down Expand Up @@ -693,6 +694,29 @@ export function checkItineraryExistence(trip, intl) {
}
}

export function getDependentUserInfo(userIds, intl) {
return async function (dispatch, getState) {
const state = getState()
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(state)
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/getdependentmobilityprofile?dependentuserids=${userIds}`

const { data, status } = await secureFetch(
requestUrl,
accessToken,
apiKey,
'GET'
)

if (status === 'success' && data) {
dispatch(setDependentUserInfo(data))
} else {
alert(
intl.formatMessage({ id: 'actions.user.getDependentUserInfoFailed' })
)
}
}
}

/**
* Plans a new trip for the current date given the query parameters in the given
* monitored trip
Expand Down
115 changes: 103 additions & 12 deletions lib/components/form/advanced-settings-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
import {
addSettingsToButton,
AdvancedModeSubsettingsContainer,
DropdownSelector,
ModeSettingRenderer,
populateSettingWithValue
} from '@opentripplanner/trip-form'
import { ArrowLeft } from '@styled-icons/fa-solid/ArrowLeft'
import { Check } from '@styled-icons/boxicons-regular'
import { connect } from 'react-redux'
import { decodeQueryParams, DelimitedArrayParam } from 'serialize-query-params'
import { FormattedMessage, useIntl } from 'react-intl'
import { FormattedMessage, IntlShape, useIntl } from 'react-intl'
import { invisibleCss } from '@opentripplanner/trip-form/lib/MetroModeSelector'
import {
ModeButtonDefinition,
ModeSetting,
ModeSettingValues
} from '@opentripplanner/types'
import React, { RefObject, useCallback, useContext, useState } from 'react'
import { QueryParamChangeEvent } from '@opentripplanner/trip-form/lib/types'
import React, {
RefObject,
useCallback,
useContext,
useEffect,
useMemo,
useState
} from 'react'
import styled from 'styled-components'

import * as formActions from '../../actions/form'
import * as userActions from '../../actions/user'
import { AppReduxState } from '../../util/state-types'
import { blue, getBaseColor } from '../util/colors'
import { ComponentContext } from '../../util/contexts'
import { generateModeSettingValues } from '../../util/api'
import { User } from '../user/types'
import Link from '../util/link'

import {
addCustomSettingLabels,
Expand Down Expand Up @@ -69,10 +81,18 @@ const HeaderContainer = styled.div`
height: 30px;
`

const Subheader = styled.h2`
const InvisibleSubheader = styled.h2`
${invisibleCss}
`

const VisibleSubheader = styled.h2`
display: block;
font-size: 18px;
font-weight: 700;
height: auto;
margin: 1em 0;
position: static;
width: auto;
`
const ReturnToTripPlanButton = styled.button`
align-items: center;
background-color: var(--main-base-color, ${blue[900]});
Expand Down Expand Up @@ -111,13 +131,27 @@ const DtSelectorContainer = styled.div`
}
`

const MobilityProfileContainer = styled.div`
margin: 60px 0 60px 5px;
`

const MobilityProfileDropdown = styled(DropdownSelector)`
margin: 20px 0px;
label {
padding-left: 0;
}
`

const AdvancedSettingsPanel = ({
autoPlan,
closeAdvancedSettings,
currentQuery,
enabledModeButtons,
getDependentUserInfo,
handlePlanTrip,
innerRef,
loggedInUser,
mobilityProfile,
modeButtonOptions,
modeSettingDefinitions,
modeSettingValues,
Expand All @@ -129,20 +163,40 @@ const AdvancedSettingsPanel = ({
closeAdvancedSettings: () => void
currentQuery: any
enabledModeButtons: string[]
getDependentUserInfo: (userIds: string[], intl: IntlShape) => void
handlePlanTrip: () => void
innerRef: RefObject<HTMLDivElement>
loggedInUser?: User
mobilityProfile: boolean
modeButtonOptions: ModeButtonDefinition[]
modeSettingDefinitions: ModeSetting[]
modeSettingValues: ModeSettingValues
saveAndReturnButton?: boolean
setCloseAdvancedSettingsWithDelay: () => void
setQueryParam: (evt: any) => void
}): JSX.Element => {
const intl = useIntl()
const [closingBySave, setClosingBySave] = useState(false)
const [selectedMobilityProfile, setSelectedMobilityProfile] =
useState<string>(
currentQuery.mobilityProfile ||
loggedInUser?.mobilityProfile?.mobilityMode ||
''
)
const dependents = useMemo(
() => loggedInUser?.dependents || [],
[loggedInUser]
)

useEffect(() => {
if (mobilityProfile && dependents.length > 0) {
getDependentUserInfo(dependents, intl)
}
}, [dependents, getDependentUserInfo, intl, mobilityProfile])

const baseColor = getBaseColor()
const accentColor = baseColor || blue[900]

const intl = useIntl()
const closeButtonText = intl.formatMessage({
id: 'components.BatchSearchScreen.saveAndReturn'
})
Expand Down Expand Up @@ -184,7 +238,6 @@ const AdvancedSettingsPanel = ({
)
)


const tripFormErrors = tripPlannerValidationErrors(currentQuery, intl)

const closePanel = useCallback(() => {
Expand All @@ -208,6 +261,16 @@ const AdvancedSettingsPanel = ({
closePanel()
}, [closePanel, setCloseAdvancedSettingsWithDelay])

const onMobilityProfileChange = useCallback(
(evt: QueryParamChangeEvent) => {
const value = evt.mobilityProfile
setSelectedMobilityProfile(value as string)
setQueryParam({
mobilityProfile: value
})
},
[setSelectedMobilityProfile, setQueryParam]
)
return (
<PanelOverlay className="advanced-settings" ref={innerRef}>
<HeaderContainer>
Expand All @@ -228,17 +291,43 @@ const AdvancedSettingsPanel = ({
</DtSelectorContainer>
{processedGlobalSettings.length > 0 && (
<>
<Subheader>
<InvisibleSubheader>
<FormattedMessage id="components.BatchSearchScreen.tripOptions" />
</Subheader>
</InvisibleSubheader>
<GlobalSettingsContainer className="global-settings-container">
{globalSettingsComponents}
</GlobalSettingsContainer>
</>
)}
<Subheader>
<FormattedMessage id="components.BatchSearchScreen.modeOptions" />
</Subheader>
{loggedInUser?.dependentsInfo?.length && (
<MobilityProfileContainer>
<VisibleSubheader>
<FormattedMessage id="components.MobilityProfile.MobilityPane.header" />
</VisibleSubheader>
<FormattedMessage id="components.MobilityProfile.MobilityPane.planTripDescription" />
<MobilityProfileDropdown
label={intl.formatMessage({
id: 'components.MobilityProfile.dropdownLabel'
})}
name="mobilityProfile"
onChange={onMobilityProfileChange}
options={[
{
text: intl.formatMessage({
id: 'components.MobilityProfile.myself'
}),
value: loggedInUser.mobilityProfile?.mobilityMode || ''
},
...(loggedInUser.dependentsInfo?.map((user) => ({
text: user.name || user.email,
value: user.mobilityMode || ''
})) || [])
]}
value={selectedMobilityProfile}
/>
</MobilityProfileContainer>
)}

<AdvancedModeSubsettingsContainer
accentColor={accentColor}
fillModeIcons
Expand Down Expand Up @@ -268,7 +357,6 @@ const AdvancedSettingsPanel = ({
</PanelOverlay>
)
}

const queryParamConfig = { modeButtons: DelimitedArrayParam }

const mapStateToProps = (state: AppReduxState) => {
Expand All @@ -293,6 +381,8 @@ const mapStateToProps = (state: AppReduxState) => {
})?.modeButtons?.filter((mb): mb is string => mb !== null) ||
modes?.initialState?.enabledModeButtons ||
[],
loggedInUser: state.user.loggedInUser,
mobilityProfile: state.otp.config?.mobilityProfile || false,
modeButtonOptions: modes?.modeButtons || [],
modeSettingDefinitions: state.otp?.modeSettingDefinitions || [],
modeSettingValues,
Expand All @@ -301,6 +391,7 @@ const mapStateToProps = (state: AppReduxState) => {
}

const mapDispatchToProps = {
getDependentUserInfo: userActions.getDependentUserInfo,
setQueryParam: formActions.setQueryParam,
updateQueryTimeIfLeavingNow: formActions.updateQueryTimeIfLeavingNow
}
Expand Down
4 changes: 3 additions & 1 deletion lib/components/user/monitored-trip/saved-trip-screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ class SavedTripScreen extends Component {
render() {
const {
disableSingleItineraryDays,
hasMobilityProfile,
isCreating,
itinerary,
loggedInUser,
Expand Down Expand Up @@ -241,7 +242,7 @@ class SavedTripScreen extends Component {
<Form noValidate>
<SavedTripEditor
{...props}
hasMobilityProfile={Boolean(loggedInUser.mobilityProfile)}
hasMobilityProfile={hasMobilityProfile}
isCreating={isCreating}
notificationChannel={loggedInUser.notificationChannel}
onCancel={
Expand Down Expand Up @@ -283,6 +284,7 @@ const mapStateToProps = (state, ownProps) => {
const { disableSingleItineraryDays } = state.otp.config
return {
disableSingleItineraryDays,
hasMobilityProfile: state.otp.config.mobilityProfile,
homeTimezone: state.otp.config.homeTimezone,
isCreating: tripId === 'new',
itinerary: itineraries[activeItinerary],
Expand Down
11 changes: 11 additions & 0 deletions lib/components/user/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,26 @@ export interface MobilityProfile {
}

export interface CompanionInfo {
acceptKey?: string
email: string
nickname?: string
status?: 'PENDING' | 'CONFIRMED' | 'INVALID'
}

export interface DependentInfo {
email: string
mobilityMode: string
name?: string
userId: string
}

/**
* Type definition for an OTP-middleware (OTP-personas) user.
*/
export interface User {
accessibilityRoutingByDefault?: boolean
dependents?: string[]
dependentsInfo?: DependentInfo[]
// email always exists per Auth0.
email: string
hasConsentedToTerms?: boolean
Expand Down
Loading

0 comments on commit 7256079

Please sign in to comment.