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/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.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 4e72a01d1..7aee53537 100644 --- a/frontend/src/components/RideDetails/RideLocations.tsx +++ b/frontend/src/components/RideDetails/RideLocations.tsx @@ -5,7 +5,7 @@ import React, { useRef, useMemo, } 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'; @@ -26,6 +26,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 { @@ -39,9 +40,6 @@ interface LocationBlockProps { isPickup?: boolean; isChanging?: boolean; onChangeClick?: () => void; - onDropdownClick?: () => void; - onConfirm?: () => void; - onCancel?: () => void; canEdit?: boolean; dropdownButtonRef?: React.RefObject; } @@ -53,9 +51,6 @@ const LocationBlock: React.FC = ({ isPickup = false, isChanging = false, onChangeClick, - onDropdownClick, - onConfirm, - onCancel, canEdit = false, dropdownButtonRef, }) => { @@ -128,9 +123,10 @@ const LocationBlock: React.FC = ({ )} - {!isChanging && canEdit && ( + {canEdit && ( )} - {isChanging && ( -
- -
- - - - - - -
-
- )}
@@ -531,50 +496,65 @@ 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, + tag: Tag.CUSTOM, + lat: 0, + lng: 0, + }; + + 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; }; @@ -584,7 +564,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} /> @@ -607,41 +584,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 */} @@ -653,9 +650,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 @@ -673,7 +667,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 941450a41..fb0ae1acf 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -26,7 +26,6 @@ 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'; @@ -404,10 +403,6 @@ const RequestRideDialog: React.FC = ({ {!ride ? 'Request a Ride' : 'Edit Ride'} -
@@ -857,7 +852,6 @@ const RequestRideDialog: React.FC = ({ )}
- 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; + +