From 79ff90e68072c51623357849067d1a675fdf9e36 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Thu, 1 Jan 2026 15:01:20 -0500 Subject: [PATCH 1/2] Map enable/disable, fix admin add rides flow Custom locations! No longer takes like five clicks to add a location on the admin side! --- .../RequestRideModal/RequestRideInfo.tsx | 25 ++- .../RideDetails/RideLocations.module.css | 8 + .../components/RideDetails/RideLocations.tsx | 193 +++++++++--------- .../RiderComponents/RequestRideDialog.tsx | 30 ++- frontend/src/config/googleMaps.ts | 5 + 5 files changed, 135 insertions(+), 126 deletions(-) create mode 100644 frontend/src/config/googleMaps.ts diff --git a/frontend/src/components/RequestRideModal/RequestRideInfo.tsx b/frontend/src/components/RequestRideModal/RequestRideInfo.tsx index adb127942..e00cd1a4b 100644 --- a/frontend/src/components/RequestRideModal/RequestRideInfo.tsx +++ b/frontend/src/components/RequestRideModal/RequestRideInfo.tsx @@ -11,6 +11,7 @@ import { RideModalType } from './types'; import { checkBounds, isTimeValid } from '../../util/index'; import { useLocations } from '../../context/LocationsContext'; import RequestRideMap from '../RiderComponents/RequestRideMap'; +import { ENABLE_ADD_RIDE_MAPS } from '../../config/googleMaps'; type RequestRideInfoProps = { ride?: Ride; @@ -443,18 +444,20 @@ const RequestRideInfo: React.FC = ({ {/* Map for location selection */} -
- -
- + {ENABLE_ADD_RIDE_MAPS && ( +
+ +
+ +
-
+ )} {watchDropoffCustom === 'Other' ? (
diff --git a/frontend/src/components/RideDetails/RideLocations.module.css b/frontend/src/components/RideDetails/RideLocations.module.css index f0323a7ec..15f3b1f2c 100644 --- a/frontend/src/components/RideDetails/RideLocations.module.css +++ b/frontend/src/components/RideDetails/RideLocations.module.css @@ -2,6 +2,14 @@ max-width: 1000px; } +.locationsGridNoMap { + display: grid; + grid-template-columns: 1fr; + gap: 24px; + margin-bottom: 24px; + align-items: start; +} + .locationsGrid { display: grid; grid-template-columns: 1fr 1fr; diff --git a/frontend/src/components/RideDetails/RideLocations.tsx b/frontend/src/components/RideDetails/RideLocations.tsx index 9c99ae8ff..9ce9819b4 100644 --- a/frontend/src/components/RideDetails/RideLocations.tsx +++ b/frontend/src/components/RideDetails/RideLocations.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { Typography, IconButton, Chip, Box, Button } from '@mui/material'; +import { Typography, IconButton, Chip, Box, Button, TextField } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import DirectionsCarIcon from '@mui/icons-material/DirectionsCar'; import TimelapseIcon from '@mui/icons-material/Timelapse'; @@ -20,6 +20,7 @@ import { useRideEdit } from './RideEditContext'; import { useLocations } from '../../context/LocationsContext'; import { SearchableType } from '../../utils/searchConfig'; import SearchPopup from './SearchPopup'; +import { ENABLE_ADD_RIDE_MAPS } from '../../config/googleMaps'; import styles from './RideLocations.module.css'; interface RideLocationsProps { @@ -33,9 +34,6 @@ interface LocationBlockProps { isPickup?: boolean; isChanging?: boolean; onChangeClick?: () => void; - onDropdownClick?: () => void; - onConfirm?: () => void; - onCancel?: () => void; canEdit?: boolean; dropdownButtonRef?: React.RefObject; } @@ -47,9 +45,6 @@ const LocationBlock: React.FC = ({ isPickup = false, isChanging = false, onChangeClick, - onDropdownClick, - onConfirm, - onCancel, canEdit = false, dropdownButtonRef, }) => { @@ -122,9 +117,10 @@ const LocationBlock: React.FC = ({ )} - {!isChanging && canEdit && ( + {canEdit && ( )} - {isChanging && ( -
- -
- - - - - - -
-
- )}
@@ -462,50 +427,62 @@ const RideLocations: React.FC = () => { const [changingLocation, setChangingLocation] = useState< 'pickup' | 'dropoff' | null >(null); - const [tempLocation, setTempLocation] = useState(null); + const [customAddress, setCustomAddress] = useState(''); const [locationSelectorOpen, setLocationSelectorOpen] = useState(false); const pickupButtonRef = useRef(null); const dropoffButtonRef = useRef(null); const handleStartChanging = (locationType: 'pickup' | 'dropoff') => { + setChangingLocation(locationType); const currentLocation = locationType === 'pickup' ? ride.startLocation : ride.endLocation; - setChangingLocation(locationType); - setTempLocation(currentLocation); + setCustomAddress(currentLocation.address || ''); + setLocationSelectorOpen(true); }; - const handleLocationSelect = (location: Location) => { - setTempLocation(location); - setLocationSelectorOpen(false); + const handleCustomAddressChange = ( + event: React.ChangeEvent + ) => { + const address = event.target.value; + setCustomAddress(address); + + if (!changingLocation) return; + + const field = + changingLocation === 'pickup' ? 'startLocation' : 'endLocation'; + const currentLocation = + changingLocation === 'pickup' ? ride.startLocation : ride.endLocation; + + const updatedLocation: Location = { + ...currentLocation, + address, + name: address || currentLocation.name, + }; + + updateRideField(field, updatedLocation); }; - const handleConfirmChange = () => { - if (changingLocation && tempLocation) { + const handleLocationSelect = (location: Location) => { + if (changingLocation) { const field = changingLocation === 'pickup' ? 'startLocation' : 'endLocation'; - updateRideField(field, tempLocation); + updateRideField(field, location); } - handleCancelChange(); - }; - - const handleCancelChange = () => { - setChangingLocation(null); - setTempLocation(null); setLocationSelectorOpen(false); + setChangingLocation(null); }; const handleMapLocationSelect = (location: Location) => { - setTempLocation(location); - }; - - const handleDropdownClick = () => { - setLocationSelectorOpen(!locationSelectorOpen); + if (changingLocation) { + const field = + changingLocation === 'pickup' ? 'startLocation' : 'endLocation'; + updateRideField(field, location); + setChangingLocation(null); + setLocationSelectorOpen(false); + } }; const getDisplayLocation = (locationType: 'pickup' | 'dropoff') => { - if (changingLocation === locationType && tempLocation) { - return tempLocation; - } return locationType === 'pickup' ? ride.startLocation : ride.endLocation; }; @@ -515,7 +492,7 @@ const RideLocations: React.FC = () => { return (
-
+
{/* Left side - Address blocks */}
= () => { isPickup={true} isChanging={changingLocation === 'pickup'} onChangeClick={() => handleStartChanging('pickup')} - onDropdownClick={handleDropdownClick} - onConfirm={handleConfirmChange} - onCancel={handleCancelChange} canEdit={isEditing && canEdit} dropdownButtonRef={pickupButtonRef} /> @@ -538,41 +512,61 @@ const RideLocations: React.FC = () => { isPickup={false} isChanging={changingLocation === 'dropoff'} onChangeClick={() => handleStartChanging('dropoff')} - onDropdownClick={handleDropdownClick} - onConfirm={handleConfirmChange} - onCancel={handleCancelChange} canEdit={isEditing && canEdit} dropdownButtonRef={dropoffButtonRef} /> + + {/* Custom location text input (similar in spirit to RequestRide modal) */} + {changingLocation && isEditing && canEdit && ( + + + Or enter a custom location + + + + )}
{/* Right side - Map */} + {ENABLE_ADD_RIDE_MAPS && (
- { - // Don't show temp selected location - if (location.id === tempLocation?.id) return false; - - // Don't allow selecting the other location (pickup/dropoff) to prevent duplicates - if (changingLocation === 'pickup') { - // When changing pickup, don't show current dropoff location - return location.id !== ride.endLocation.id; - } else { - // When changing dropoff, don't show current pickup location - return location.id !== ride.startLocation.id; - } - }) - : [] - } - onLocationSelect={handleMapLocationSelect} - changingLocationType={changingLocation} - /> + { + // Don't allow selecting the other location (pickup/dropoff) to prevent duplicates + if (changingLocation === 'pickup') { + // When changing pickup, don't show current dropoff location + return location.id !== ride.endLocation.id; + } else { + // When changing dropoff, don't show current pickup location + return location.id !== ride.startLocation.id; + } + }) + : [] + } + onLocationSelect={handleMapLocationSelect} + changingLocationType={changingLocation} + />
+ )}
{/* Location Selection Popup */} @@ -584,9 +578,6 @@ const RideLocations: React.FC = () => { items={(() => { // Filter out the temporarily selected location and the other location type return locations.filter((location) => { - // Don't show temp selected location - if (location.id === tempLocation?.id) return false; - // Don't allow selecting the other location (pickup/dropoff) to prevent duplicates if (changingLocation === 'pickup') { // When changing pickup, don't show current dropoff location @@ -604,7 +595,13 @@ const RideLocations: React.FC = () => { changingLocation === 'pickup' ? 'Pickup' : 'Dropoff' } Location`} placeholder="Search locations..." - selectedItems={tempLocation ? [tempLocation] : []} + selectedItems={ + changingLocation === 'pickup' + ? [ride.startLocation] + : changingLocation === 'dropoff' + ? [ride.endLocation] + : [] + } anchorEl={getCurrentButtonRef().current} /> )} diff --git a/frontend/src/components/RiderComponents/RequestRideDialog.tsx b/frontend/src/components/RiderComponents/RequestRideDialog.tsx index 04f90fed6..9068dbc5a 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -25,13 +25,13 @@ import { DatePicker, TimePicker, } from '@mui/x-date-pickers'; -import { APIProvider } from '@vis.gl/react-google-maps'; import RequestRideMap from './RequestRideMap'; import styles from './requestridedialog.module.css'; import { Ride, Location, Tag } from 'types'; import RequestRidePlacesSearch from './RequestRidePlacesSearch'; import axios from '../../util/axios'; import { error } from 'console'; +import { ENABLE_ADD_RIDE_MAPS } from '../../config/googleMaps'; type RepeatOption = 'none' | 'daily' | 'weekly' | 'custom'; @@ -554,12 +554,7 @@ const RequestRideDialog: React.FC = ({ {!ride ? 'Request a Ride' : 'Edit Ride'} - {/* Wrap everything in a single APIProvider */} - -
+
{/* Selection Progress Indicator */} @@ -892,17 +887,18 @@ const RequestRideDialog: React.FC = ({
-
- -
+ {ENABLE_ADD_RIDE_MAPS && ( +
+ +
+ )}
- diff --git a/frontend/src/config/googleMaps.ts b/frontend/src/config/googleMaps.ts new file mode 100644 index 000000000..ad5080312 --- /dev/null +++ b/frontend/src/config/googleMaps.ts @@ -0,0 +1,5 @@ +// Global feature flag for Google Maps usage in add-ride flows. +// Set this to true to re-enable Google Maps views on add-ride pages/modals. +export const ENABLE_ADD_RIDE_MAPS = false; + + From 5aca74a6d82d3e6d89c493cb662541682c87f4ff Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Wed, 7 Jan 2026 17:41:47 -0500 Subject: [PATCH 2/2] Fix custom locations --- .../RideDetails/RideEditContext.tsx | 102 +++++++++++++++--- .../components/RideDetails/RideLocations.tsx | 3 + 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/RideDetails/RideEditContext.tsx b/frontend/src/components/RideDetails/RideEditContext.tsx index 3a47a9167..d265cde9b 100644 --- a/frontend/src/components/RideDetails/RideEditContext.tsx +++ b/frontend/src/components/RideDetails/RideEditContext.tsx @@ -6,7 +6,7 @@ import React, { useEffect, ReactNode, } from 'react'; -import { RideType, SchedulingState } from '../../types'; +import { RideType, SchedulingState, Location, Tag } from '../../types'; import axios from '../../util/axios'; import { canEditRide, UserRole } from '../../util/rideValidation'; import { @@ -128,9 +128,19 @@ export const RideEditProvider: React.FC = ({ // For new rides, check if required fields are filled if (isNewRide(editedRide)) { - const hasRequiredFields = - editedRide.startLocation.id !== '' && editedRide.endLocation.id !== ''; - return hasRequiredFields; + // Require that both pickup and dropoff have a concrete address. + // This supports both saved locations (with IDs) and truly custom + // free‑text locations created in RideLocations. + const hasPickupLocation = + !!editedRide.startLocation && + !!editedRide.startLocation.address && + editedRide.startLocation.address.trim().length > 0; + const hasDropoffLocation = + !!editedRide.endLocation && + !!editedRide.endLocation.address && + editedRide.endLocation.address.trim().length > 0; + + return hasPickupLocation && hasDropoffLocation; } if (!originalRide) { @@ -167,26 +177,88 @@ export const RideEditProvider: React.FC = ({ // For new rides, we need different validation and payload if (isNewRide(editedRide)) { - // Validate required fields for new ride - if ( - !editedRide.startLocation.id || - !editedRide.endLocation.id || - !editedRide.startTime || - !editedRide.endTime - ) { - console.error('Missing required fields for new ride'); + // Validate required fields for new ride. + // Allow custom locations that only have an address (no ID). + const hasPickupLocation = + !!editedRide.startLocation && + !!editedRide.startLocation.address && + editedRide.startLocation.address.trim().length > 0; + const hasDropoffLocation = + !!editedRide.endLocation && + !!editedRide.endLocation.address && + editedRide.endLocation.address.trim().length > 0; + + if (!hasPickupLocation || !hasDropoffLocation) { + console.error('Missing required pickup or dropoff location for new ride'); + return false; + } + + if (!editedRide.startTime || !editedRide.endTime) { + console.error('Missing required time fields for new ride'); return false; } try { - // Prepare payload for new ride creation + // Helper to ensure a location is backed by a real Location row, + // similar to RequestRideDialog's createCustomLocation flow. + const ensurePersistedLocation = async ( + loc: Location + ): Promise => { + // If this location already has an ID, assume it's a real Location. + if (loc.id && loc.id.trim().length > 0) { + return loc; + } + + // Create a new custom Location in the backend, mirroring + // RequestRideDialog's /api/locations/custom behavior. + const name = + (loc.name || loc.address || 'Custom Location').trim(); + + const payload: Partial = { + name, + shortName: loc.shortName || '', + // For custom locations, we follow the existing pattern and + // don't require a resolvable address/coordinates. + address: '', + info: loc.info || '', + tag: Tag.CUSTOM, + lat: 0, + lng: 0, + }; + + const response = await axios.post('/api/locations/custom', payload); + const created: Location = response.data.data || response.data; + return created; + }; + + let startLocationToUse = editedRide.startLocation as Location; + let endLocationToUse = editedRide.endLocation as Location; + + // If either location looks like a "new" custom one (no ID), + // create a persisted custom Location first. + if ( + startLocationToUse && + (!startLocationToUse.id || startLocationToUse.id.trim().length === 0) + ) { + startLocationToUse = await ensurePersistedLocation(startLocationToUse); + } + + if ( + endLocationToUse && + (!endLocationToUse.id || endLocationToUse.id.trim().length === 0) + ) { + endLocationToUse = await ensurePersistedLocation(endLocationToUse); + } + + // Prepare payload for new ride creation. Send location IDs like + // the rider Schedule flow does; the backend will populate them. const createPayload = { type: editedRide.type, schedulingState: editedRide.schedulingState, startTime: editedRide.startTime, endTime: editedRide.endTime, - startLocation: editedRide.startLocation, - endLocation: editedRide.endLocation, + startLocation: startLocationToUse.id, + endLocation: endLocationToUse.id, riders: editedRide.riders || [], driver: editedRide.driver || null, isRecurring: false, diff --git a/frontend/src/components/RideDetails/RideLocations.tsx b/frontend/src/components/RideDetails/RideLocations.tsx index 20c45f6a6..7aee53537 100644 --- a/frontend/src/components/RideDetails/RideLocations.tsx +++ b/frontend/src/components/RideDetails/RideLocations.tsx @@ -526,6 +526,9 @@ const RideLocations: React.FC = () => { ...currentLocation, address, name: address || currentLocation.name, + tag: Tag.CUSTOM, + lat: 0, + lng: 0, }; updateRideField(field, updatedLocation);