diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 197c0baad..ea4b74aef 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -639,6 +639,10 @@ components: tripNotAvailableOnDay: Trip not available on {repeatedDay} unsavedChangesExistingTrip: You haven't saved your trip yet. If you leave, changes will be lost. unsavedChangesNewTrip: You haven't saved your new trip yet. If you leave, it will be lost. + TripCompanionsPane: + companionLabel: "Companion on this trip:" + observersLabel: "Observers watching this trip:" + primaryLabel: "Primary traveler: " TripNotificationsPane: advancedSettings: Advanced settings altRouteRecommended: An alternative route or transfer point is recommended diff --git a/i18n/fr.yml b/i18n/fr.yml index 041b2e578..6e3fa0b0e 100644 --- a/i18n/fr.yml +++ b/i18n/fr.yml @@ -358,8 +358,7 @@ components: 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 Préférences. + personnes que vous accompagnez. visionLimitations: "Handicaps visuels : " dropdownLabel: "Profil à utiliser :" intro: >- @@ -672,6 +671,10 @@ components: unsavedChangesNewTrip: >- Vous n'avez pas encore enregistré votre nouveau trajet. Si vous annulez, ce trajet sera perdu. + TripCompanionsPane: + companionLabel: "Accompagnateurs sur ce trajet :" + observersLabel: "Observateurs suivant ce trajet :" + primaryLabel: "Voyageur principal : " TripNotificationsPane: advancedSettings: Paramètres avancés altRouteRecommended: Un·e autre trajet ou correspondance est conseillé·e diff --git a/lib/actions/apiV2.js b/lib/actions/apiV2.js index 15d3f5d77..e4d5ab1a2 100644 --- a/lib/actions/apiV2.js +++ b/lib/actions/apiV2.js @@ -9,7 +9,11 @@ import clone from 'clone' import coreUtils from '@opentripplanner/core-utils' import { checkForRouteModeOverride } from '../util/config' -import { convertToPlace, getPersistenceMode } from '../util/user' +import { + convertToPlace, + getPersistenceMode, + getUserWithEmail +} from '../util/user' import { FETCH_STATUS } from '../util/constants' import { generateModeSettingValues, @@ -1026,10 +1030,10 @@ export function routingQuery(searchId = null, updateSearchInReducer) { ...currentQuery, numItineraries: numItineraries || getDefaultNumItineraries(config) } - if (config.mobilityProfile) { + if (config.mobilityProfile && loggedInUser) { baseQuery.mobilityProfile = - currentQuery.mobilityProfile || - loggedInUser?.mobilityProfile?.mobilityMode + getUserWithEmail(loggedInUser.dependentsInfo, currentQuery.forEmail) + ?.mobilityMode || 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. diff --git a/lib/components/form/advanced-settings-panel.tsx b/lib/components/form/advanced-settings-panel.tsx index bae3d224a..2510c3b88 100644 --- a/lib/components/form/advanced-settings-panel.tsx +++ b/lib/components/form/advanced-settings-panel.tsx @@ -33,8 +33,8 @@ import { AppReduxState } from '../../util/state-types' import { blue, getBaseColor } from '../util/colors' import { ComponentContext } from '../../util/contexts' import { generateModeSettingValues } from '../../util/api' +import { getDependentName } from '../../util/user' import { User } from '../user/types' -import Link from '../util/link' import { addCustomSettingLabels, @@ -178,11 +178,7 @@ const AdvancedSettingsPanel = ({ const intl = useIntl() const [closingBySave, setClosingBySave] = useState(false) const [selectedMobilityProfile, setSelectedMobilityProfile] = - useState( - currentQuery.mobilityProfile || - loggedInUser?.mobilityProfile?.mobilityMode || - '' - ) + useState(currentQuery.forEmail || loggedInUser?.email) const dependents = useMemo( () => loggedInUser?.dependents || [], [loggedInUser] @@ -263,10 +259,10 @@ const AdvancedSettingsPanel = ({ const onMobilityProfileChange = useCallback( (evt: QueryParamChangeEvent) => { - const value = evt.mobilityProfile + const value = evt.forEmail setSelectedMobilityProfile(value as string) setQueryParam({ - mobilityProfile: value + forEmail: value }) }, [setSelectedMobilityProfile, setQueryParam] @@ -309,18 +305,18 @@ const AdvancedSettingsPanel = ({ label={intl.formatMessage({ id: 'components.MobilityProfile.dropdownLabel' })} - name="mobilityProfile" + name="forEmail" onChange={onMobilityProfileChange} options={[ { text: intl.formatMessage({ id: 'components.MobilityProfile.myself' }), - value: loggedInUser.mobilityProfile?.mobilityMode || '' + value: loggedInUser?.email }, - ...(loggedInUser.dependentsInfo?.map((user) => ({ - text: user.name || user.email, - value: user.mobilityMode || '' + ...(loggedInUser?.dependentsInfo?.map((user) => ({ + text: getDependentName(user), + value: user.email })) || []) ]} value={selectedMobilityProfile} diff --git a/lib/components/user/common/dropdown-options.tsx b/lib/components/user/common/dropdown-options.tsx index 70667915b..839570cde 100644 --- a/lib/components/user/common/dropdown-options.tsx +++ b/lib/components/user/common/dropdown-options.tsx @@ -7,6 +7,7 @@ interface SelectProps { Control?: ComponentType children: ReactNode defaultValue?: string | number | boolean + disabled?: boolean label?: ReactNode name: string onChange?: ChangeEventHandler @@ -19,6 +20,7 @@ export const Select = ({ Control = FormControl, children, defaultValue, + disabled, label, name, onChange @@ -27,6 +29,7 @@ export const Select = ({ as: Control, componentClass: 'select', defaultValue, + disabled, id: name, name, onChange diff --git a/lib/components/user/mobility-profile/companion-selector.tsx b/lib/components/user/mobility-profile/companion-selector.tsx new file mode 100644 index 000000000..4b909b89b --- /dev/null +++ b/lib/components/user/mobility-profile/companion-selector.tsx @@ -0,0 +1,95 @@ +import { connect } from 'react-redux' +import React, { lazy, Suspense, useCallback } from 'react' + +import { AppReduxState } from '../../../util/state-types' +import { CompanionInfo, User } from '../types' +import StatusBadge from '../../util/status-badge' + +export interface Option { + label: string + value: CompanionInfo +} + +// @ts-expect-error: No types for react-select. +const Select = lazy(() => import('react-select')) + +function notNull(item: unknown) { + return !!item +} + +function makeOption(companion?: CompanionInfo) { + return { + label: companion?.nickname || companion?.email, + value: companion + } +} + +function isConfirmed({ status }: CompanionInfo) { + return status === 'CONFIRMED' +} + +function formatOptionLabel(option: Option) { + if (!isConfirmed(option.value)) { + return ( + <> + {option.label} + + ) + } else { + return option.label + } +} + +const CompanionSelector = ({ + disabled, + excludedUsers = [], + loggedInUser, + multi = false, + onChange, + selectedCompanions +}: { + disabled?: boolean + excludedUsers?: (CompanionInfo | undefined)[] + loggedInUser?: User + multi?: boolean + onChange: (e: Option | Option[]) => void + selectedCompanions?: (CompanionInfo | undefined)[] +}): JSX.Element => { + const companionOptions = (loggedInUser?.relatedUsers || []) + .filter(notNull) + .filter(isConfirmed) + .map(makeOption) + const companionValues = multi + ? selectedCompanions?.filter(notNull).map(makeOption) + : selectedCompanions?.[0] + ? makeOption(selectedCompanions[0]) + : null + + const isOptionDisabled = useCallback( + (option: Option) => excludedUsers.includes(option?.value), + [excludedUsers] + ) + + return ( + ...}> + } @@ -132,6 +134,7 @@ class TripNotificationsPane extends Component {
  • } diff --git a/lib/components/user/monitored-trip/trip-status.js b/lib/components/user/monitored-trip/trip-status.js index 27ffcfb8f..03bf2b30a 100644 --- a/lib/components/user/monitored-trip/trip-status.js +++ b/lib/components/user/monitored-trip/trip-status.js @@ -70,6 +70,7 @@ function MonitoredTripAlerts({ alerts }) { */ function TripStatus({ confirmAndDeleteUserMonitoredTrip, + isReadOnly, planNewTripFromMonitoredTrip, renderData, togglePauseTrip, @@ -89,37 +90,39 @@ function TripStatus({ )} {/* Footer buttons */} - - {renderData.shouldRenderToggleSnoozeTripButton && ( - - )} - {renderData.shouldRenderTogglePauseTripButton && ( - - )} - {renderData.shouldRenderDeleteTripButton && ( - } - /> - )} - {renderData.shouldRenderPlanNewTripButton && ( - } - /> - )} - + {!isReadOnly && ( + + {renderData.shouldRenderToggleSnoozeTripButton && ( + + )} + {renderData.shouldRenderTogglePauseTripButton && ( + + )} + {renderData.shouldRenderDeleteTripButton && ( + } + /> + )} + {renderData.shouldRenderPlanNewTripButton && ( + } + /> + )} + + )} ) } diff --git a/lib/components/user/monitored-trip/trip-summary-pane.tsx b/lib/components/user/monitored-trip/trip-summary-pane.tsx index 3e1ccef51..5ce8b904b 100644 --- a/lib/components/user/monitored-trip/trip-summary-pane.tsx +++ b/lib/components/user/monitored-trip/trip-summary-pane.tsx @@ -114,6 +114,7 @@ const ToggleNotificationButton = styled.button` const TripSummaryPane = ({ from, handleTogglePauseMonitoring, + isReadOnly, monitoredTrip, pendingRequest, to @@ -224,20 +225,24 @@ const TripSummaryPane = ({ values={{ leadTimeInMinutes }} /> )} -
    - - {pendingRequest === 'pause' ? ( - /* Make loader fit */ - - ) : monitoredTrip.isActive ? ( - - ) : ( - - )} - + {!isReadOnly && ( + <> +
    + + {pendingRequest === 'pause' ? ( + /* Make loader fit */ + + ) : monitoredTrip.isActive ? ( + + ) : ( + + )} + + + )} diff --git a/lib/components/user/stacked-panes-with-save.tsx b/lib/components/user/stacked-panes-with-save.tsx index 596132ba4..5304a01ff 100644 --- a/lib/components/user/stacked-panes-with-save.tsx +++ b/lib/components/user/stacked-panes-with-save.tsx @@ -8,6 +8,7 @@ import StackedPanes, { Props as StackedPanesProps } from './stacked-panes' interface Props extends StackedPanesProps { extraButton?: ButtonType + isReadOnly?: boolean onCancel: () => void } @@ -18,6 +19,7 @@ interface Props extends StackedPanesProps { */ const StackedPanesWithSave = ({ extraButton, + isReadOnly, onCancel, panes, title @@ -49,21 +51,25 @@ const StackedPanesWithSave = ({ ) }} - extraButton={extraButton} - okayButton={{ - disabled: buttonClicked === 'okay', - onClick: () => { - // Some browsers need this to happen after the formik action finishes firing - setTimeout(() => setButtonClicked('okay'), 10) - }, - text: - buttonClicked === 'okay' ? ( - - ) : ( - - ), - type: 'submit' - }} + extraButton={isReadOnly ? undefined : extraButton} + okayButton={ + isReadOnly + ? undefined + : { + disabled: buttonClicked === 'okay', + onClick: () => { + // Some browsers need this to happen after the formik action finishes firing + setTimeout(() => setButtonClicked('okay'), 10) + }, + text: + buttonClicked === 'okay' ? ( + + ) : ( + + ), + type: 'submit' + } + } /> ) diff --git a/lib/components/user/types.ts b/lib/components/user/types.ts index be05872cd..6babd41f7 100644 --- a/lib/components/user/types.ts +++ b/lib/components/user/types.ts @@ -81,6 +81,7 @@ export interface JourneyState { export type MonitoredTrip = Record & { arrivalVarianceMinutesThreshold: number + companion?: CompanionInfo departureVarianceMinutesThreshold: number excludeFederalHolidays?: boolean id: string @@ -89,8 +90,11 @@ export type MonitoredTrip = Record & { itineraryExistence?: ItineraryExistence journeyState?: JourneyState leadTimeInMinutes: number + observers?: CompanionInfo[] otp2QueryParams: Record + primary?: DependentInfo queryParams: Record + secondary?: CompanionInfo tripName: string userId: string } @@ -98,6 +102,7 @@ export type MonitoredTrip = Record & { export interface MonitoredTripProps { from?: Place handleTogglePauseMonitoring?: () => void + isReadOnly?: boolean monitoredTrip: MonitoredTrip pendingRequest?: boolean | string to?: Place diff --git a/lib/util/user.js b/lib/util/user.js index ec571227b..e909be808 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -264,3 +264,13 @@ export function getPlaceMainText(place, intl) { ? toSentenceCase(getFormattedPlaces(place.type, intl)) : place.name || place.address } + +/** Helper for matching the email field. */ +export function getUserWithEmail(users, email) { + return users?.find((user) => user.email === email) +} + +/** Helper for displaying a dependent user name with fallback on email. */ +export function getDependentName(dependent) { + return dependent?.name || dependent?.email +}