From d034e77164d6761c5c4185b43cd3438f7588ac31 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Sat, 25 Oct 2025 13:42:18 -0400 Subject: [PATCH 01/12] Initial toast impl --- .eslintrc.js | 2 +- .../confirmationtoast.module.css | 2 +- .../CreateOrEditRideModal.tsx | 7 +- .../RideDetails/RideEditContext.tsx | 24 ++++- .../components/RideDetails/RideOverview.tsx | 89 +++++-------------- .../RiderComponents/RequestRideDialog.tsx | 2 +- frontend/src/pages/Driver/Rides.tsx | 4 +- frontend/src/pages/Rider/Schedule.tsx | 20 +++-- 8 files changed, 66 insertions(+), 84 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index df02896ea..6aec181ff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,7 +23,7 @@ module.exports = { ecmaVersion: 6, sourceType: 'module', }, - plugins: ['promise', '@typescript-eslint', 'import', 'react', 'react-hooks'], + plugins: ['promise', '@typescript-eslint', 'import', 'react', 'react-hooks', 'jest'], rules: { 'import/extensions': [ 'error', diff --git a/frontend/src/components/ConfirmationToast/confirmationtoast.module.css b/frontend/src/components/ConfirmationToast/confirmationtoast.module.css index 9c874ff31..c36db2862 100644 --- a/frontend/src/components/ConfirmationToast/confirmationtoast.module.css +++ b/frontend/src/components/ConfirmationToast/confirmationtoast.module.css @@ -7,7 +7,7 @@ padding: 0.75rem 1rem; box-shadow: 4px 6px 30px 5px rgba(0, 0, 0, 0.15); border-radius: 0.625rem; - z-index: 999; + z-index: 2000; display: flex; align-items: center; } diff --git a/frontend/src/components/RequestRideModal/CreateOrEditRideModal.tsx b/frontend/src/components/RequestRideModal/CreateOrEditRideModal.tsx index 9769f4a84..67f09dbf6 100644 --- a/frontend/src/components/RequestRideModal/CreateOrEditRideModal.tsx +++ b/frontend/src/components/RequestRideModal/CreateOrEditRideModal.tsx @@ -10,6 +10,7 @@ import RequestRideInfo from './RequestRideInfo'; import { RideModalType } from './types'; import { format_date } from '../../util/index'; import axios from '../../util/axios'; +import { useToast, ToastStatus } from '../../context/toastContext'; type CreateOrEditRideModalProps = { isOpen: boolean; @@ -52,6 +53,7 @@ const CreateOrEditRideModal = ({ const methods = useForm({ defaultValues }); const { id } = useContext(AuthContext); + const { showToast } = useToast(); const closeModal = () => { methods.clearErrors(); @@ -97,8 +99,9 @@ const CreateOrEditRideModal = ({ let rideData: ObjectType; if (recurring || whenRepeat) { // For now, block recurring rides as they're not fully implemented - alert( - 'Recurring rides are not yet supported. Please create a single ride instead.' + showToast( + 'Recurring rides are not yet supported. Please create a single ride instead.', + ToastStatus.ERROR ); return; } else { diff --git a/frontend/src/components/RideDetails/RideEditContext.tsx b/frontend/src/components/RideDetails/RideEditContext.tsx index 3a47a9167..05e7e99e0 100644 --- a/frontend/src/components/RideDetails/RideEditContext.tsx +++ b/frontend/src/components/RideDetails/RideEditContext.tsx @@ -16,6 +16,7 @@ import { } from '../../util/modelFixtures'; import { validateRideTimes } from './TimeValidation'; import { useRides } from '../../context/RidesContext'; +import { useToast, ToastStatus } from '../../context/toastContext'; interface RideEditContextType { isEditing: boolean; @@ -50,6 +51,7 @@ export const RideEditProvider: React.FC = ({ initialEditingState = false, }) => { const { updateRideInfo } = useRides(); + const { showToast } = useToast(); // For new rides, automatically start in editing mode const shouldStartEditing = initialEditingState || isNewRide(ride); @@ -161,6 +163,9 @@ export const RideEditProvider: React.FC = ({ if (!timeValidation.isValid) { console.error('Time validation failed:', timeValidation.errors); + const firstErr = + timeValidation.errors[0]?.message || 'Invalid time values'; + showToast(firstErr, ToastStatus.ERROR); return false; } } @@ -175,6 +180,10 @@ export const RideEditProvider: React.FC = ({ !editedRide.endTime ) { console.error('Missing required fields for new ride'); + showToast( + 'Please select pickup, dropoff, start time, and end time.', + ToastStatus.ERROR + ); return false; } @@ -202,8 +211,13 @@ export const RideEditProvider: React.FC = ({ stopEditing(); return true; - } catch (error) { + } catch (error: any) { console.error('Failed to create new ride:', error); + const msg = + error?.response?.data?.message || + error?.response?.data?.err || + 'Failed to create ride.'; + showToast(msg, ToastStatus.ERROR); return false; } } else { @@ -253,8 +267,13 @@ export const RideEditProvider: React.FC = ({ stopEditing(); return true; - } catch (error) { + } catch (error: any) { console.error('Failed to save ride changes:', error); + const msg = + error?.response?.data?.message || + error?.response?.data?.err || + 'Failed to save changes.'; + showToast(msg, ToastStatus.ERROR); return false; } } @@ -266,6 +285,7 @@ export const RideEditProvider: React.FC = ({ onRideUpdated, stopEditing, updateRideInfo, + showToast, ]); const contextValue: RideEditContextType = { diff --git a/frontend/src/components/RideDetails/RideOverview.tsx b/frontend/src/components/RideDetails/RideOverview.tsx index 7dbfb55bd..856a81733 100644 --- a/frontend/src/components/RideDetails/RideOverview.tsx +++ b/frontend/src/components/RideDetails/RideOverview.tsx @@ -37,8 +37,8 @@ import { useRideEdit } from './RideEditContext'; import RecurrenceDisplay from './RecurrenceDisplay'; import RiderList from './RiderList'; import { isNewRide } from '../../util/modelFixtures'; -import { validateRideTimes } from './TimeValidation'; import styles from './RideOverview.module.css'; +import { useToast, ToastStatus } from '../../context/toastContext'; interface RideOverviewProps { userRole: 'rider' | 'driver' | 'admin'; @@ -205,7 +205,7 @@ const RideOverview: React.FC = ({ userRole }) => { const ride = editedRide!; const temporalType = getTemporalType(ride); const showRecurrence = userRole !== 'driver'; // Hide recurrence for drivers - + const { showToast } = useToast(); const formatDateTime = (dateTimeString: string) => { const date = new Date(dateTimeString); return { @@ -240,15 +240,20 @@ const RideOverview: React.FC = ({ userRole }) => { const currentEndTime = dayjs(ride.endTime); // Ensure end time is after start time - if ( - updatedStartTime.isAfter(currentEndTime) || - updatedStartTime.isSame(currentEndTime) - ) { - const newEndTime = updatedStartTime.add(30, 'minute'); - updateRideField('endTime', newEndTime.toISOString()); + try { + if ( + updatedStartTime.isAfter(currentEndTime) || + updatedStartTime.isSame(currentEndTime) + ) { + const newEndTime = updatedStartTime.add(30, 'minute'); + updateRideField('endTime', newEndTime.toISOString()); + } + + updateRideField('startTime', updatedStartTime.toISOString()); + } catch (error) { + console.error('Failed to update ride time:', error); + showToast(`Error updating ride time: ${error}`, ToastStatus.ERROR); } - - updateRideField('startTime', updatedStartTime.toISOString()); }; const handleStartTimeChange = (newTime: Dayjs | null) => { @@ -388,34 +393,9 @@ const RideOverview: React.FC = ({ userRole }) => { // For editing existing rides, allow past times; for new rides, don't const allowPastTimes = !isNewRide(ride); - const validation = validateRideTimes( - ride.startTime, - ride.endTime, - { - allowPastTimes, - maxDurationHours: 24, - minDurationMinutes: 5, - } - ); - - // Check for specific error types - const startTimePastError = validation.errors.find( - (e) => e.type === 'start_time_past' - ); - const endTimeBeforeStartError = validation.errors.find( - (e) => - e.type === 'end_time_before_start' || - e.type === 'same_time' - ); - const durationError = validation.errors.find( - (e) => e.type === 'too_long_duration' - ); - - const hasStartTimeError = - startTimePastError !== undefined; - const hasEndTimeError = - endTimeBeforeStartError !== undefined || - durationError !== undefined; + // Inline date/time validation removed; handled on Save + const hasStartTimeError = false; + const hasEndTimeError = false; return ( <> @@ -454,20 +434,7 @@ const RideOverview: React.FC = ({ userRole }) => { }} /> {/* Start time specific error - directly below start time field */} - {startTimePastError && ( - - {startTimePastError.message} - - )} + {/* Field-level error messaging removed; errors surface on Save */} @@ -506,23 +473,7 @@ const RideOverview: React.FC = ({ userRole }) => { }} /> {/* End time specific errors - directly below end time field */} - {(endTimeBeforeStartError || durationError) && ( - - { - (endTimeBeforeStartError || durationError) - ?.message - } - - )} + {/* Field-level error messaging removed; errors surface on Save */} diff --git a/frontend/src/components/RiderComponents/RequestRideDialog.tsx b/frontend/src/components/RiderComponents/RequestRideDialog.tsx index 04f90fed6..3c7c7ecd8 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -50,7 +50,7 @@ type SelectionState = 'pickup' | 'dropoff' | 'complete'; interface RequestRideDialogProps { open: boolean; onClose: () => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: FormData) => Promise | boolean | void; supportedLocations: Location[]; ride?: Ride; } diff --git a/frontend/src/pages/Driver/Rides.tsx b/frontend/src/pages/Driver/Rides.tsx index f08370e0b..dd5206a25 100644 --- a/frontend/src/pages/Driver/Rides.tsx +++ b/frontend/src/pages/Driver/Rides.tsx @@ -29,6 +29,7 @@ import NoRidesView from '../../components/NoRidesView/NoRidesView'; import ContactInfoModal from '../../components/ContactInfoModal/ContactInfoModal'; import { APIProvider, useMapsLibrary } from '@vis.gl/react-google-maps'; import UpdateStatusModal from '../../components/UpdateStatusModal/UpdateStatusModal'; +import { useToast, ToastStatus } from '../../context/toastContext'; const getStatusColor = ( status: Status @@ -403,6 +404,7 @@ const Rides = () => { useRides(); const { curDate } = useDate(); const authContext = useContext(AuthContext); + const { showToast } = useToast(); const [updating, setUpdating] = useState(false); const [currentRideId, setCurrentRideId] = useState(null); const [allDriverRides, setAllDriverRides] = useState([]); @@ -487,7 +489,7 @@ const Rides = () => { const errorMessage = error.response?.data?.message || 'Could not update ride status. Please try again.'; - alert(`Error: ${errorMessage}`); + showToast(`Error: ${errorMessage}`, ToastStatus.ERROR); } finally { setUpdating(false); } diff --git a/frontend/src/pages/Rider/Schedule.tsx b/frontend/src/pages/Rider/Schedule.tsx index d2eda13f2..5cdb64105 100644 --- a/frontend/src/pages/Rider/Schedule.tsx +++ b/frontend/src/pages/Rider/Schedule.tsx @@ -15,6 +15,7 @@ import buttonStyles from '../../components/ResponsiveRideCard.module.css'; import { NavigateBefore, NavigateNext } from '@mui/icons-material'; import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { useToast, ToastStatus } from '../../context/toastContext'; type DayRideCollection = [string, Ride[]][]; @@ -63,6 +64,7 @@ const partitionRides = (rides: Ride[]): DayRideCollection => { const Schedule: React.FC = () => { const { user, id } = useContext(AuthContext); + const { showToast } = useToast(); const { locations } = useLocations(); const { refreshRides, refreshRidesByUser } = useRides(); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -151,17 +153,18 @@ const Schedule: React.FC = () => { // For now, block any recurring rides if (formData.repeatType !== 'none') { - alert( - 'Recurring rides are not yet supported. Please create a single ride.' + showToast( + 'Recurring rides are not yet supported. Please create a single ride.', + ToastStatus.ERROR ); - return; + return false; } try { // Build ISO datetimes if (!formData.date || !formData.time) { - alert('Please select both date and time.'); - return; + showToast('Please select both date and time.', ToastStatus.ERROR); + return false; } const dateStr = formData.date.toISOString().split('T')[0]; @@ -187,9 +190,12 @@ const Schedule: React.FC = () => { // Refresh rides after successful creation await refreshRides(); console.log('Ride created successfully'); - } catch (error) { + return true; + } catch (error: any) { console.error('Failed to create ride:', error); - alert('Failed to create ride. Please try again.'); + const msg = error?.response?.data?.err || 'Please try again.'; + showToast('Failed to create ride: ' + msg, ToastStatus.ERROR); + return false; } }; From 048af8013ed5b2996699502884f3ee6418ac83be Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Thu, 4 Dec 2025 23:53:09 -0500 Subject: [PATCH 02/12] Add back instant error checking --- .../RideDetails/RideEditContext.tsx | 5 -- .../components/RideDetails/RideOverview.tsx | 51 +++++++++++++++++-- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/RideDetails/RideEditContext.tsx b/frontend/src/components/RideDetails/RideEditContext.tsx index 05e7e99e0..184afbe9b 100644 --- a/frontend/src/components/RideDetails/RideEditContext.tsx +++ b/frontend/src/components/RideDetails/RideEditContext.tsx @@ -227,10 +227,8 @@ export const RideEditProvider: React.FC = ({ } try { - // Prepare the update payload - only include changed fields (like the previous implementation) const updatePayload: any = {}; - // Only include changed fields - include riders for single rider updates const fieldsToCheck: (keyof RideType)[] = [ 'startTime', 'endTime', @@ -257,10 +255,7 @@ export const RideEditProvider: React.FC = ({ updatePayload.$REMOVE = ['driver']; } - // Use optimistic update from RidesContext await updateRideInfo(ride.id, updatePayload); - - // Update the context with the optimistically updated ride if (onRideUpdated) { onRideUpdated({ ...editedRide } as RideType); } diff --git a/frontend/src/components/RideDetails/RideOverview.tsx b/frontend/src/components/RideDetails/RideOverview.tsx index 856a81733..0f8ff40a3 100644 --- a/frontend/src/components/RideDetails/RideOverview.tsx +++ b/frontend/src/components/RideDetails/RideOverview.tsx @@ -37,6 +37,7 @@ import { useRideEdit } from './RideEditContext'; import RecurrenceDisplay from './RecurrenceDisplay'; import RiderList from './RiderList'; import { isNewRide } from '../../util/modelFixtures'; +import { validateRideTimes } from './TimeValidation'; import styles from './RideOverview.module.css'; import { useToast, ToastStatus } from '../../context/toastContext'; @@ -393,10 +394,35 @@ const RideOverview: React.FC = ({ userRole }) => { // For editing existing rides, allow past times; for new rides, don't const allowPastTimes = !isNewRide(ride); - // Inline date/time validation removed; handled on Save - const hasStartTimeError = false; - const hasEndTimeError = false; + const validation = validateRideTimes( + ride.startTime, + ride.endTime, + { + allowPastTimes, + maxDurationHours: 24, + minDurationMinutes: 5, + } + ); + // Check for specific error types + const startTimePastError = validation.errors.find( + (e) => e.type === 'start_time_past' + ); + const endTimeBeforeStartError = validation.errors.find( + (e) => + e.type === 'end_time_before_start' || + e.type === 'same_time' + ); + const durationError = validation.errors.find( + (e) => e.type === 'too_long_duration' + ); + + const hasStartTimeError = + startTimePastError !== undefined; + const hasEndTimeError = + endTimeBeforeStartError !== undefined || + durationError !== undefined; + return ( <>
@@ -472,8 +498,23 @@ const RideOverview: React.FC = ({ userRole }) => { }, }} /> - {/* End time specific errors - directly below end time field */} - {/* Field-level error messaging removed; errors surface on Save */} + {(endTimeBeforeStartError || durationError) && ( + + { + (endTimeBeforeStartError || durationError) + ?.message + } + + )}
From dd0af19f43824b0c694ffbca21c8e44c74355338 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Sun, 2 Nov 2025 16:21:44 -0500 Subject: [PATCH 03/12] Using modal throughout app --- frontend/src/App.tsx | 9 ++- .../components/AuthManager/AuthManager.tsx | 3 + .../EmployeeCards/EmployeeCards.tsx | 1 - .../EmployeeModal/EmployeeModal.tsx | 14 +++- .../Locations/LocationFormModal.tsx | 10 +++ .../src/components/Locations/PlacesSearch.tsx | 3 + .../src/components/Modal/modal.module.css | 4 +- .../OptimisticDemo/OptimisticDemo.tsx | 3 + .../components/RideDetails/RideActions.tsx | 19 +++-- .../RideDetails/RideEditContext.tsx | 13 ++-- .../components/RideDetails/RideLocations.tsx | 3 + .../components/RideDetails/RideOverview.tsx | 4 +- .../src/components/RideDetails/RidePeople.tsx | 4 + .../RiderComponents/RequestRideDialog.tsx | 25 ++++-- .../RiderComponents/RequestRideMap.tsx | 3 + .../RequestRidePlacesSearch.tsx | 3 + .../src/components/UserDetail/ActionsCard.tsx | 3 + .../UserDetail/hooks/useUserDetailData.ts | 9 +++ frontend/src/context/EmployeesContext.tsx | 10 +++ frontend/src/context/RidersContext.tsx | 7 ++ frontend/src/context/RidesContext.tsx | 15 +++- frontend/src/context/errorModal.tsx | 78 +++++++++++++++++++ frontend/src/hooks/useOptimisticRiders.ts | 5 ++ frontend/src/index.tsx | 2 + frontend/src/pages/Driver/Rides.tsx | 5 +- frontend/src/pages/Rider/Schedule.tsx | 9 ++- frontend/src/serviceWorker.ts | 9 +++ 27 files changed, 235 insertions(+), 38 deletions(-) create mode 100644 frontend/src/context/errorModal.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 264675d23..63b978996 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { ToastProvider } from './context/toastContext'; import AuthManager from './components/AuthManager/AuthManager'; +import { ErrorModalProvider } from './context/errorModal'; import './styles/App.css'; import { setAuthToken } from './util/axios'; @@ -18,9 +19,11 @@ const App = () => { return ( - - - + + + + + ); }; diff --git a/frontend/src/components/AuthManager/AuthManager.tsx b/frontend/src/components/AuthManager/AuthManager.tsx index d85a797e0..fa34e3ee5 100644 --- a/frontend/src/components/AuthManager/AuthManager.tsx +++ b/frontend/src/components/AuthManager/AuthManager.tsx @@ -29,6 +29,7 @@ import { createPortal } from 'react-dom'; import CryptoJS from 'crypto-js'; import axios, { setAuthToken } from '../../util/axios'; import UnregisteredUserPage from '../Onboarding/UnregisteredUserPage'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; const secretKey = `${process.env.REACT_APP_ENCRYPTION_KEY!}`; @@ -67,6 +68,7 @@ const AuthManager = () => { setUnregisteredUser(null); logout(); }; + const { showError } = useErrorModal(); useEffect(() => { const token = jwtValue(); @@ -209,6 +211,7 @@ const AuthManager = () => { } } catch (error) { console.error('Error decrypting JWT:', error); + showError(`Error decrypting JWT: ${formatErrorMessage(error)}`, 'Authentication Error'); } return ''; } diff --git a/frontend/src/components/EmployeeCards/EmployeeCards.tsx b/frontend/src/components/EmployeeCards/EmployeeCards.tsx index 0a1627955..6692d1995 100644 --- a/frontend/src/components/EmployeeCards/EmployeeCards.tsx +++ b/frontend/src/components/EmployeeCards/EmployeeCards.tsx @@ -11,7 +11,6 @@ const formatPhone = (phoneNumber: string | undefined) => { const secondPart = phoneNumber.substring(6, 10); return `${areaCode}-${firstPart}-${secondPart}`; } else { - console.error('Undefined PhoneNumber'); return ''; } }; diff --git a/frontend/src/components/EmployeeModal/EmployeeModal.tsx b/frontend/src/components/EmployeeModal/EmployeeModal.tsx index d298e2dea..1ae3454d8 100644 --- a/frontend/src/components/EmployeeModal/EmployeeModal.tsx +++ b/frontend/src/components/EmployeeModal/EmployeeModal.tsx @@ -12,6 +12,7 @@ import styles from './employeemodal.module.css'; import { useEmployees } from '../../context/EmployeesContext'; import { useToast, ToastStatus } from '../../context/toastContext'; import axios from '../../util/axios'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; type AdminData = { type: string[]; @@ -76,6 +77,7 @@ const EmployeeModal = ({ setIsOpen, }: EmployeeModalProps) => { const { showToast } = useToast(); + const { showError } = useErrorModal(); const { updateAdminInfo, updateDriverInfo, @@ -157,6 +159,7 @@ const EmployeeModal = ({ }); } catch (error) { console.error('Error uploading photo:', error); + showError(`Error uploading photo: ${formatErrorMessage(error)}`, 'Employees Error'); throw new Error('Failed to upload employee photo. Please try again.'); } } @@ -366,9 +369,9 @@ const EmployeeModal = ({ setIsUploadingImage(true); await uploadEmployeePhoto(id, targetTable, imageBase64); } catch (uploadError) { - showToast( - 'Employee created but photo upload failed. You can try uploading the photo again later.', - ToastStatus.ERROR + showError( + `Employee created but photo upload failed: ${formatErrorMessage(uploadError)}. You can try uploading the photo again later.`, + 'Employees Error' ); // Don't throw here - we want the employee creation to succeed even if photo upload fails } finally { @@ -379,7 +382,10 @@ const EmployeeModal = ({ // Note: No need to manually refresh - optimistic updates handle this automatically showToast(`Employee information processed`, ToastStatus.SUCCESS); } catch (error) { - showToast('An error occurred: ', ToastStatus.ERROR); + showError( + `An error occurred while saving employee: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } finally { closeModal(); } diff --git a/frontend/src/components/Locations/LocationFormModal.tsx b/frontend/src/components/Locations/LocationFormModal.tsx index 732633096..f41b38f2d 100644 --- a/frontend/src/components/Locations/LocationFormModal.tsx +++ b/frontend/src/components/Locations/LocationFormModal.tsx @@ -23,6 +23,7 @@ import GeocoderService from './GeocoderService'; import { Location, Tag } from 'types'; import styles from './locations.module.css'; import LocationImagesUpload, { LocationImage } from './LocationImagesUpload'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; const CAMPUS_OPTIONS = [ { value: Tag.NORTH, label: 'North Campus' }, @@ -67,6 +68,7 @@ export const LocationFormModal: React.FC = ({ const [loadingAddr, setLoadingAddr] = useState(false); const [error, setError] = useState(null); const [locationImages, setLocationImages] = useState([]); + const { showError } = useErrorModal(); useEffect(() => { if (!open) return; @@ -98,6 +100,10 @@ export const LocationFormModal: React.FC = ({ } catch (e) { setError("Couldn't retrieve address for this location"); console.error(e); + showError( + `Couldn't retrieve address for this location: ${formatErrorMessage(e)}`, + 'Locations Error' + ); } finally { setLoadingAddr(false); } @@ -114,6 +120,10 @@ export const LocationFormModal: React.FC = ({ } catch (e) { setError("Couldn't find coordinates for this address"); console.error(e); + showError( + `Couldn't find coordinates for this address: ${formatErrorMessage(e)}`, + 'Locations Error' + ); } finally { setLoadingAddr(false); } diff --git a/frontend/src/components/Locations/PlacesSearch.tsx b/frontend/src/components/Locations/PlacesSearch.tsx index 7cf7ce41a..a96a0861b 100644 --- a/frontend/src/components/Locations/PlacesSearch.tsx +++ b/frontend/src/components/Locations/PlacesSearch.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react'; import { TextField, Paper, CircularProgress } from '@mui/material'; import { useMap, Map } from '@vis.gl/react-google-maps'; import styles from './locations.module.css'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface PlacesSearchProps { onAddressSelect: (address: string, lat: number, lng: number) => void; @@ -18,6 +19,7 @@ const PlacesSearch = ({ const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const map = useMap(); + const { showError } = useErrorModal(); const searchPlace = useCallback( async (query: string) => { @@ -54,6 +56,7 @@ const PlacesSearch = ({ setIsLoading(false); setError('Error searching for address'); setResults([]); + showError(`Error searching for address: ${formatErrorMessage(error)}`, 'Address Search Error'); } }, [map] diff --git a/frontend/src/components/Modal/modal.module.css b/frontend/src/components/Modal/modal.module.css index 925434ba5..edc0da70f 100644 --- a/frontend/src/components/Modal/modal.module.css +++ b/frontend/src/components/Modal/modal.module.css @@ -6,7 +6,7 @@ left: 0; height: 100%; width: 100%; - z-index: 1000; + z-index: 10000; } .modal { @@ -17,7 +17,7 @@ background-color: white; padding: 2rem 2.25rem; border-radius: 1rem; - z-index: 1010; + z-index: 10010; } .title { diff --git a/frontend/src/components/OptimisticDemo/OptimisticDemo.tsx b/frontend/src/components/OptimisticDemo/OptimisticDemo.tsx index 9884d1bfe..87d9eea8e 100644 --- a/frontend/src/components/OptimisticDemo/OptimisticDemo.tsx +++ b/frontend/src/components/OptimisticDemo/OptimisticDemo.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { useRiders } from '../../context/RidersContext'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; const OptimisticDemo: React.FC = () => { const { riders, isOptimistic, pendingOperations, updateRiderActive, error } = useRiders(); + const { showError } = useErrorModal(); const handleDemoToggle = async () => { if (riders.length > 0) { @@ -12,6 +14,7 @@ const OptimisticDemo: React.FC = () => { await updateRiderActive(firstRider.id, !firstRider.active); } catch (err) { console.error('Demo failed:', err); + showError(`Demo failed: ${formatErrorMessage(err)}`, 'Demo Error'); } } }; diff --git a/frontend/src/components/RideDetails/RideActions.tsx b/frontend/src/components/RideDetails/RideActions.tsx index 8de9d13ce..7e5c2f5cb 100644 --- a/frontend/src/components/RideDetails/RideActions.tsx +++ b/frontend/src/components/RideDetails/RideActions.tsx @@ -41,6 +41,7 @@ import { useRides } from '../../context/RidesContext'; import { useDate } from '../../context/date'; import { isNewRide } from '../../util/modelFixtures'; import axios from '../../util/axios'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface RideActionsProps { userRole: UserRole; @@ -85,6 +86,7 @@ const RideActions: React.FC = ({ const { showToast } = useToast(); const { refreshRides } = useRides(); const { curDate } = useDate(); + const { showError } = useErrorModal(); const [updateStatusOpen, setUpdateStatusOpen] = useState(false); const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false); @@ -109,6 +111,7 @@ const RideActions: React.FC = ({ setSelectedStatus(null); } catch (error) { console.error('Failed to update status:', error); + showError(`Failed to update status: ${formatErrorMessage(error)}`, 'Rides Error'); } finally { setUpdating(false); } @@ -144,7 +147,7 @@ const RideActions: React.FC = ({ showToast('Ride Cancelled', ToastStatus.SUCCESS); } catch (error) { console.error('Failed to cancel ride:', error); - showToast('Failed to cancel ride', ToastStatus.ERROR); + showError(`Failed to cancel ride: ${formatErrorMessage(error)}`, 'Rides Error'); } }; @@ -179,17 +182,17 @@ const RideActions: React.FC = ({ onClose(); // Close modal after creating new ride } } else { - const message = isNewRide(ride) - ? 'Failed to create ride' - : 'Failed to save ride'; - showToast(message, ToastStatus.ERROR); + // const message = isNewRide(ride) + // ? 'Failed to create ride' + // : 'Failed to save ride'; + // showToast(message, ToastStatus.ERROR); } } catch (error) { console.error('Error saving ride:', error); const message = isNewRide(ride) - ? 'Failed to create ride' - : 'Failed to save ride'; - showToast(message, ToastStatus.ERROR); + ? 'Failed to create ride: ' + formatErrorMessage(error) + : 'Failed to save ride: ' + formatErrorMessage(error); + showError(message, 'Rides Error'); } finally { setSaving(false); } diff --git a/frontend/src/components/RideDetails/RideEditContext.tsx b/frontend/src/components/RideDetails/RideEditContext.tsx index 184afbe9b..a13e6c3d9 100644 --- a/frontend/src/components/RideDetails/RideEditContext.tsx +++ b/frontend/src/components/RideDetails/RideEditContext.tsx @@ -17,6 +17,7 @@ import { import { validateRideTimes } from './TimeValidation'; import { useRides } from '../../context/RidesContext'; import { useToast, ToastStatus } from '../../context/toastContext'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface RideEditContextType { isEditing: boolean; @@ -52,6 +53,7 @@ export const RideEditProvider: React.FC = ({ }) => { const { updateRideInfo } = useRides(); const { showToast } = useToast(); + const { showError } = useErrorModal(); // For new rides, automatically start in editing mode const shouldStartEditing = initialEditingState || isNewRide(ride); @@ -165,7 +167,7 @@ export const RideEditProvider: React.FC = ({ console.error('Time validation failed:', timeValidation.errors); const firstErr = timeValidation.errors[0]?.message || 'Invalid time values'; - showToast(firstErr, ToastStatus.ERROR); + showError(firstErr, 'Ride Edit Error'); return false; } } @@ -180,10 +182,7 @@ export const RideEditProvider: React.FC = ({ !editedRide.endTime ) { console.error('Missing required fields for new ride'); - showToast( - 'Please select pickup, dropoff, start time, and end time.', - ToastStatus.ERROR - ); + showError('Please select pickup, dropoff, start time, and end time.', 'Ride Edit Error'); return false; } @@ -217,7 +216,7 @@ export const RideEditProvider: React.FC = ({ error?.response?.data?.message || error?.response?.data?.err || 'Failed to create ride.'; - showToast(msg, ToastStatus.ERROR); + showError(msg, 'Rides Error'); return false; } } else { @@ -268,7 +267,7 @@ export const RideEditProvider: React.FC = ({ error?.response?.data?.message || error?.response?.data?.err || 'Failed to save changes.'; - showToast(msg, ToastStatus.ERROR); + showError(msg, 'Rides Error'); return false; } } diff --git a/frontend/src/components/RideDetails/RideLocations.tsx b/frontend/src/components/RideDetails/RideLocations.tsx index 9c99ae8ff..46e296894 100644 --- a/frontend/src/components/RideDetails/RideLocations.tsx +++ b/frontend/src/components/RideDetails/RideLocations.tsx @@ -21,6 +21,7 @@ import { useLocations } from '../../context/LocationsContext'; import { SearchableType } from '../../utils/searchConfig'; import SearchPopup from './SearchPopup'; import styles from './RideLocations.module.css'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface RideLocationsProps { // No props needed - gets ride from context @@ -214,6 +215,7 @@ const RideMap: React.FC = ({ duration: string; } | null>(null); const mapsLibrary = useMapsLibrary('routes'); + const { showError } = useErrorModal(); const fetchAndDrawRoute = useCallback(async () => { if (!window.google || !map || !startLocation || !endLocation) { @@ -277,6 +279,7 @@ const RideMap: React.FC = ({ } } catch (error) { console.error('Error fetching route:', error); + showError(`Error fetching route: ${formatErrorMessage(error)}`, 'Maps Error'); if (polylineRef.current) { polylineRef.current.setMap(null); polylineRef.current = null; diff --git a/frontend/src/components/RideDetails/RideOverview.tsx b/frontend/src/components/RideDetails/RideOverview.tsx index 0f8ff40a3..720c0688e 100644 --- a/frontend/src/components/RideDetails/RideOverview.tsx +++ b/frontend/src/components/RideDetails/RideOverview.tsx @@ -40,6 +40,7 @@ import { isNewRide } from '../../util/modelFixtures'; import { validateRideTimes } from './TimeValidation'; import styles from './RideOverview.module.css'; import { useToast, ToastStatus } from '../../context/toastContext'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface RideOverviewProps { userRole: 'rider' | 'driver' | 'admin'; @@ -207,6 +208,7 @@ const RideOverview: React.FC = ({ userRole }) => { const temporalType = getTemporalType(ride); const showRecurrence = userRole !== 'driver'; // Hide recurrence for drivers const { showToast } = useToast(); + const { showError } = useErrorModal(); const formatDateTime = (dateTimeString: string) => { const date = new Date(dateTimeString); return { @@ -253,7 +255,7 @@ const RideOverview: React.FC = ({ userRole }) => { updateRideField('startTime', updatedStartTime.toISOString()); } catch (error) { console.error('Failed to update ride time:', error); - showToast(`Error updating ride time: ${error}`, ToastStatus.ERROR); + showError(`Error updating ride time: ${formatErrorMessage(error)}`, 'Ride Edit Error'); } }; diff --git a/frontend/src/components/RideDetails/RidePeople.tsx b/frontend/src/components/RideDetails/RidePeople.tsx index 7473aeeca..4906a569c 100644 --- a/frontend/src/components/RideDetails/RidePeople.tsx +++ b/frontend/src/components/RideDetails/RidePeople.tsx @@ -21,6 +21,7 @@ import RiderList from './RiderList'; import SearchPopup from './SearchPopup'; import { SearchableType } from '../../utils/searchConfig'; import styles from './RidePeople.module.css'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface RidePeopleProps { userRole: 'rider' | 'driver' | 'admin'; @@ -111,6 +112,7 @@ const PersonCard: React.FC = ({ const RidePeople: React.FC = ({ userRole }) => { const { editedRide, isEditing, updateRideField } = useRideEdit(); const { getAvailableRiders } = useRides(); + const { showError } = useErrorModal(); const ride = editedRide!; const [drivers, setDrivers] = useState([]); @@ -181,6 +183,7 @@ const RidePeople: React.FC = ({ userRole }) => { console.error('Failed to fetch available drivers:', error); setDriversError('Failed to load available drivers'); setDrivers([]); + showError(`Failed to fetch available drivers: ${formatErrorMessage(error)}`, 'Employees Error'); } finally { setLoadingDrivers(false); } @@ -208,6 +211,7 @@ const RidePeople: React.FC = ({ userRole }) => { console.error('Failed to fetch riders:', error); setRidersError('Failed to load riders'); setRiders([]); + showError(`Failed to fetch riders: ${formatErrorMessage(error)}`, 'Riders Error'); } finally { setLoadingRiders(false); } diff --git a/frontend/src/components/RiderComponents/RequestRideDialog.tsx b/frontend/src/components/RiderComponents/RequestRideDialog.tsx index 3c7c7ecd8..ccf707e33 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -32,6 +32,9 @@ import { Ride, Location, Tag } from 'types'; import RequestRidePlacesSearch from './RequestRidePlacesSearch'; import axios from '../../util/axios'; import { error } from 'console'; +import { useLocations } from '../../context/LocationsContext'; +import { useToast, ToastStatus } from '../../context/toastContext'; +import { formatErrorMessage } from '../../context/errorModal'; type RepeatOption = 'none' | 'daily' | 'weekly' | 'custom'; @@ -110,6 +113,8 @@ const RequestRideDialog: React.FC = ({ //official locations with other added const supportLocsWithOther = [Other, ...supportedLocations]; + const { locations } = useLocations(); + const { showToast } = useToast(); const [formData, setFormData] = useState({ pickupLocation: null, dropoffLocation: null, @@ -503,18 +508,22 @@ const RequestRideDialog: React.FC = ({ setCustomDropoff(false); throw new Error('Start and end location are too simiilar'); } - - onSubmit({ + + const result = await onSubmit({ ...formData, pickupLocation: finalPickup, dropoffLocation: finalDropoff, }); - setCustomPickup(false); - setCustomDropoff(false); - onClose(); - } catch (err) { - console.error('Error submitting ride:', err); - alert('Failed to submit ride. Please try again.'); + + if (result !== false) { + onClose(); + showToast('Changes saved successfully', ToastStatus.SUCCESS); + } else { + showToast('Failed to save changes', ToastStatus.ERROR); + } + } catch(e) { + console.error('Error submitting ride:', e); + showToast('Failed to save changes: ' + formatErrorMessage(e), ToastStatus.ERROR); } }; diff --git a/frontend/src/components/RiderComponents/RequestRideMap.tsx b/frontend/src/components/RiderComponents/RequestRideMap.tsx index c29225665..5a3064282 100644 --- a/frontend/src/components/RiderComponents/RequestRideMap.tsx +++ b/frontend/src/components/RiderComponents/RequestRideMap.tsx @@ -13,6 +13,7 @@ import type { Marker } from '@googlemaps/markerclusterer'; import styles from './requestridedialog.module.css'; // Removed unused imports import { Location } from '../../types'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface RequestRideMapProps { pickupLocation: Location | null; @@ -38,6 +39,7 @@ const RequestRideMap: React.FC = ({ onDropoffSelect, }) => { const map = useMap(); + const { showError } = useErrorModal(); const polylineRef = useRef(null); const clusterer = useRef(null); const markers = useRef>({}); @@ -110,6 +112,7 @@ const RequestRideMap: React.FC = ({ } } catch (error) { console.error('Error fetching route:', error); + showError(`Error fetching route: ${formatErrorMessage(error)}`, 'Maps Error'); if (polylineRef.current) { polylineRef.current.setMap(null); polylineRef.current = null; diff --git a/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx b/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx index f55761c68..dccaee7bf 100644 --- a/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx +++ b/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback, useEffect, useRef } from 'react'; import { TextField, Paper, CircularProgress } from '@mui/material'; import styles from './requestridedialog.module.css'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface RequestRidePlacesSearchProps { onAddressSelect: (address: string, lat: number, lng: number) => void; @@ -29,6 +30,7 @@ const RequestRidePlacesSearch: React.FC = ({ }); } }, []); + const { showError } = useErrorModal(); const searchPlace = useCallback(async (query: string) => { console.log('Searching for:', query); @@ -76,6 +78,7 @@ const RequestRidePlacesSearch: React.FC = ({ setIsLoading(false); setError('Error searching for address'); setResults([]); + showError(`Error searching for address: ${formatErrorMessage(error)}`, 'Address Search Error'); } }, []); diff --git a/frontend/src/components/UserDetail/ActionsCard.tsx b/frontend/src/components/UserDetail/ActionsCard.tsx index 2207e49db..20e4fcff1 100644 --- a/frontend/src/components/UserDetail/ActionsCard.tsx +++ b/frontend/src/components/UserDetail/ActionsCard.tsx @@ -12,6 +12,7 @@ import Toast from '../ConfirmationToast/ConfirmationToast'; import { useRiders } from '../../context/RidersContext'; import { ToastStatus, useToast } from '../../context/toastContext'; import styles from './UserDetailCards.module.css'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; interface ActionsCardProps { user: Employee | Rider; @@ -32,6 +33,7 @@ const ActionsCard: React.FC = ({ const { updateRiderActive } = useRiders(); const { toastType } = useToast(); + const { showError } = useErrorModal(); // Auto-dismiss toast after 3 seconds useEffect(() => { @@ -92,6 +94,7 @@ const ActionsCard: React.FC = ({ // triggers the useUserDetailData hook to update the local user state } catch (error) { console.error('Error updating rider status:', error); + showError(`Error updating rider status: ${formatErrorMessage(error)}`, 'Rider Error'); setToastMessage( `Failed to ${newActiveStatus ? 'activate' : 'deactivate'} rider.` ); diff --git a/frontend/src/components/UserDetail/hooks/useUserDetailData.ts b/frontend/src/components/UserDetail/hooks/useUserDetailData.ts index 2d7cb65c6..73739db55 100644 --- a/frontend/src/components/UserDetail/hooks/useUserDetailData.ts +++ b/frontend/src/components/UserDetail/hooks/useUserDetailData.ts @@ -5,6 +5,7 @@ import { DriverType } from '../../../../../server/src/models/driver'; import { useRiders } from '../../../context/RidersContext'; import { useEmployees } from '../../../context/EmployeesContext'; import axios from '../../../util/axios'; +import { showGlobalError, formatErrorMessage } from '../../../context/errorModal'; interface UserDetailData { user: Employee | Rider | null; @@ -149,6 +150,10 @@ const useUserDetailData = ( } catch (err) { console.error('Error fetching employee data:', err); setError('Failed to fetch employee data'); + showGlobalError( + `Failed to fetch employee data: ${formatErrorMessage(err)}`, + 'Employees Error' + ); setLoading(false); } } @@ -192,6 +197,10 @@ const useUserDetailData = ( } catch (err) { setError('Failed to fetch rider data'); console.error('Error fetching rider data:', err); + showGlobalError( + `Failed to fetch rider data: ${formatErrorMessage(err)}`, + 'Riders Error' + ); setLoading(false); } }; diff --git a/frontend/src/context/EmployeesContext.tsx b/frontend/src/context/EmployeesContext.tsx index cb6a3e605..71afcdbad 100644 --- a/frontend/src/context/EmployeesContext.tsx +++ b/frontend/src/context/EmployeesContext.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useRef, useState } from 'react'; import { Admin, Driver, Employee } from '../types'; import axios from '../util/axios'; +import { useErrorModal, formatErrorMessage } from './errorModal'; type employeesState = { drivers: Array; @@ -67,6 +68,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { const [admins, setAdmins] = useState>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { showError } = useErrorModal(); const refreshDrivers = useCallback(async () => { try { @@ -80,6 +82,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { } catch (error) { console.error('Failed to fetch drivers:', error); setError(error as Error); + showError(`Failed to fetch drivers: ${formatErrorMessage(error)}`, 'Employees Error'); } }, []); @@ -95,6 +98,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { } catch (error) { console.error('Failed to fetch admins:', error); setError(error as Error); + showError(`Failed to fetch admins: ${formatErrorMessage(error)}`, 'Employees Error'); } }, []); @@ -127,6 +131,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to update driver info:', error); setDrivers(originalDrivers); setError(error as Error); + showError(`Failed to update driver info: ${formatErrorMessage(error)}`, 'Employees Error'); throw error; } }, @@ -158,6 +163,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to create driver:', error); setDrivers((prevDrivers) => prevDrivers.filter((d) => d.id !== tempId)); setError(error as Error); + showError(`Failed to create driver: ${formatErrorMessage(error)}`, 'Employees Error'); throw error; } }, []); @@ -178,6 +184,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to delete driver:', error); setDrivers(originalDrivers); setError(error as Error); + showError(`Failed to delete driver: ${formatErrorMessage(error)}`, 'Employees Error'); throw error; } }, @@ -213,6 +220,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to update admin info:', error); setAdmins(originalAdmins); setError(error as Error); + showError(`Failed to update admin info: ${formatErrorMessage(error)}`, 'Employees Error'); throw error; } }, @@ -242,6 +250,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to create admin:', error); setAdmins((prevAdmins) => prevAdmins.filter((a) => a.id !== tempId)); setError(error as Error); + showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); throw error; } }, []); @@ -262,6 +271,7 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to delete admin:', error); setAdmins(originalAdmins); setError(error as Error); + showError(`Failed to delete admin: ${formatErrorMessage(error)}`, 'Employees Error'); throw error; } }, diff --git a/frontend/src/context/RidersContext.tsx b/frontend/src/context/RidersContext.tsx index f249068aa..d7e947746 100644 --- a/frontend/src/context/RidersContext.tsx +++ b/frontend/src/context/RidersContext.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState, useRef } from 'react'; import { Rider } from '../types'; import axios from '../util/axios'; +import { useErrorModal, formatErrorMessage } from './errorModal'; type ridersState = { riders: Array; @@ -42,6 +43,7 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { const componentMounted = useRef(true); const [riders, setRiders] = useState>([]); const [loading, setLoading] = useState(true); + const { showError } = useErrorModal(); const refreshRiders = useCallback(async () => { setLoading(true); @@ -61,6 +63,7 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { } } catch (error) { console.error('Failed to fetch riders:', error); + showError(`Failed to fetch riders: ${formatErrorMessage(error)}`, 'Riders Error'); } finally { if (componentMounted.current) { setLoading(false); @@ -84,6 +87,7 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to update rider active status:', error); await refreshRiders(); // Refresh to get server state + showError(`Failed to update rider active status: ${formatErrorMessage(error)}`, 'Riders Error'); throw error; } }, @@ -115,6 +119,7 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to update rider info:', error); setRiders(originalRiders); + showError(`Failed to update rider info: ${formatErrorMessage(error)}`, 'Riders Error'); throw error; } }, @@ -141,6 +146,7 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to create rider:', error); setRiders((prevRiders) => prevRiders.filter((r) => r.id !== tempId)); + showError(`Failed to create rider: ${formatErrorMessage(error)}`, 'Riders Error'); throw error; } }, []); @@ -160,6 +166,7 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to delete rider:', error); setRiders(originalRiders); + showError(`Failed to delete rider: ${formatErrorMessage(error)}`, 'Riders Error'); throw error; } }, diff --git a/frontend/src/context/RidesContext.tsx b/frontend/src/context/RidesContext.tsx index b059952e6..1708afd88 100644 --- a/frontend/src/context/RidesContext.tsx +++ b/frontend/src/context/RidesContext.tsx @@ -3,6 +3,7 @@ import { Ride, SchedulingState, Status, Rider } from '../types'; import { useDate } from './date'; import { format_date } from '../util/index'; import axios from '../util/axios'; +import { useErrorModal, formatErrorMessage } from './errorModal'; type ridesState = { unscheduledRides: Ride[]; @@ -68,6 +69,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const { curDate } = useDate(); + const { showError } = useErrorModal(); const refreshRides = useCallback(async () => { const formattedDate = format_date(curDate); @@ -93,6 +95,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } catch (error) { console.error('Error refreshing rides:', error); setError(error as Error); + showError(`Error refreshing rides: ${formatErrorMessage(error)}`, 'Rides Error'); } finally { setLoading(false); } @@ -202,6 +205,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { updateRideInLists(rideId, () => originalRide); } setError(error as Error); + showError(`Failed to update ride status: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, @@ -250,6 +254,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { ); } setError(error as Error); + showError(`Failed to update ride scheduling: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, @@ -279,6 +284,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { updateRideInLists(rideId, () => originalRide); } setError(error as Error); + showError(`Failed to assign driver: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, @@ -299,7 +305,9 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { // We'll just use it for the optimistic update logic } catch (error) { console.error('Failed to fetch ride from server:', error); - throw new Error('Ride not found'); + const msg = 'Ride not found'; + showError(`${msg}: ${formatErrorMessage(error)}`, 'Rides Error'); + throw new Error(msg); } } @@ -363,6 +371,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { ); } setError(error as Error); + showError(`Failed to update ride info: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, @@ -405,6 +414,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { setUnscheduledRides((prev) => prev.filter((r) => r.id !== tempId)); } setError(error as Error); + showError(`Failed to create ride: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, []); @@ -444,6 +454,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } } setError(error as Error); + showError(`Failed to delete ride: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, @@ -486,6 +497,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } } setError(error as Error); + showError(`Failed to cancel ride: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, @@ -545,6 +557,7 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } catch (error) { console.error('Failed to get available riders:', error); setError(error as Error); + showError(`Failed to get available riders: ${formatErrorMessage(error)}`, 'Rides Error'); throw error; } }, diff --git a/frontend/src/context/errorModal.tsx b/frontend/src/context/errorModal.tsx new file mode 100644 index 000000000..f5e61c7fa --- /dev/null +++ b/frontend/src/context/errorModal.tsx @@ -0,0 +1,78 @@ +import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import Modal from '../components/Modal/Modal'; + +type ErrorModalState = { + isOpen: boolean; + title?: string; + message?: React.ReactNode; +}; + +type ErrorModalContextValue = { + showError: (message: React.ReactNode, title?: string) => void; + hideError: () => void; +}; + +const ErrorModalContext = createContext(undefined); + +let externalShowError: ((message: React.ReactNode, title?: string) => void) | null = null; + +export const showGlobalError = (message: React.ReactNode, title?: string) => { + if (externalShowError) { + externalShowError(message, title); + } +}; + +export const useErrorModal = (): ErrorModalContextValue => { + const ctx = useContext(ErrorModalContext); + if (!ctx) { + throw new Error('useErrorModal must be used within an ErrorModalProvider'); + } + return ctx; +}; + +type ProviderProps = { + children: React.ReactNode; + defaultTitle?: string; +}; + +export const ErrorModalProvider = ({ children, defaultTitle = 'Something went wrong' }: ProviderProps) => { + const [state, setState] = useState({ isOpen: false }); + + const hideError = useCallback(() => { + setState({ isOpen: false }); + }, []); + + const showError = useCallback((message: React.ReactNode, title?: string) => { + setState({ isOpen: true, title: title || defaultTitle, message }); + }, [defaultTitle]); + + // Expose for non-React modules + externalShowError = showError; + + const value = useMemo(() => ({ showError, hideError }), [showError, hideError]); + + return ( + + {children} + +
{state.message}
+
+
+ ); +}; + +export const formatErrorMessage = (err: unknown): string => { + if (typeof err === 'string') return err; + if (err && typeof err === 'object') { + const anyErr: any = err as any; + if (anyErr?.response?.data?.message) return anyErr.response.data.message; + if (anyErr?.message) return anyErr.message as string; + } + try { + return JSON.stringify(err); + } catch { + return 'An unexpected error occurred.'; + } +}; + + diff --git a/frontend/src/hooks/useOptimisticRiders.ts b/frontend/src/hooks/useOptimisticRiders.ts index 9ecb365ad..aa18e18e3 100644 --- a/frontend/src/hooks/useOptimisticRiders.ts +++ b/frontend/src/hooks/useOptimisticRiders.ts @@ -5,6 +5,7 @@ import { OptimisticUpdateOptions, } from './useOptimisticUpdate'; import axios from '../util/axios'; +import { showGlobalError, formatErrorMessage } from '../context/errorModal'; export interface RiderOperations { updateRiderActive: (riderId: string, active: boolean) => Promise; @@ -230,6 +231,10 @@ export function useOptimisticRiders(initialRiders: Rider[]) { optimisticState.updateServerData(serverRiders); } catch (error) { console.error('Failed to refresh riders from server:', error); + showGlobalError( + `Failed to refresh riders from server: ${formatErrorMessage(error)}`, + 'Riders Error' + ); throw error; } }, [optimisticState]); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 04acdf1f1..c4ef1f35f 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; +import { showGlobalError } from './context/errorModal'; import * as serviceWorker from './serviceWorker'; const container = document.getElementById('root'); @@ -9,6 +10,7 @@ if (container) { root.render(); } else { console.error('Failed to find the root element'); + showGlobalError('Failed to initialize the app. Please refresh the page.', 'Initialization Error'); } serviceWorker.register(); diff --git a/frontend/src/pages/Driver/Rides.tsx b/frontend/src/pages/Driver/Rides.tsx index dd5206a25..bbc0edbd9 100644 --- a/frontend/src/pages/Driver/Rides.tsx +++ b/frontend/src/pages/Driver/Rides.tsx @@ -30,6 +30,7 @@ import ContactInfoModal from '../../components/ContactInfoModal/ContactInfoModal import { APIProvider, useMapsLibrary } from '@vis.gl/react-google-maps'; import UpdateStatusModal from '../../components/UpdateStatusModal/UpdateStatusModal'; import { useToast, ToastStatus } from '../../context/toastContext'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; const getStatusColor = ( status: Status @@ -405,6 +406,7 @@ const Rides = () => { const { curDate } = useDate(); const authContext = useContext(AuthContext); const { showToast } = useToast(); + const { showError } = useErrorModal(); const [updating, setUpdating] = useState(false); const [currentRideId, setCurrentRideId] = useState(null); const [allDriverRides, setAllDriverRides] = useState([]); @@ -420,6 +422,7 @@ const Rides = () => { setAllDriverRides(rides); } catch (error) { console.error('Failed to fetch driver rides:', error); + showError(`Failed to fetch driver rides: ${formatErrorMessage(error)}`, 'Rides Error'); } finally { setLoadingRides(false); } @@ -489,7 +492,7 @@ const Rides = () => { const errorMessage = error.response?.data?.message || 'Could not update ride status. Please try again.'; - showToast(`Error: ${errorMessage}`, ToastStatus.ERROR); + showError(`Error: ${errorMessage}`, 'Rides Error'); } finally { setUpdating(false); } diff --git a/frontend/src/pages/Rider/Schedule.tsx b/frontend/src/pages/Rider/Schedule.tsx index 5cdb64105..3b9c271c3 100644 --- a/frontend/src/pages/Rider/Schedule.tsx +++ b/frontend/src/pages/Rider/Schedule.tsx @@ -16,6 +16,7 @@ import { NavigateBefore, NavigateNext } from '@mui/icons-material'; import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { useToast, ToastStatus } from '../../context/toastContext'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; type DayRideCollection = [string, Ride[]][]; @@ -70,6 +71,7 @@ const Schedule: React.FC = () => { const [isDialogOpen, setIsDialogOpen] = useState(false); const [allRiderRides, setAllRiderRides] = useState([]); const [loadingRides, setLoadingRides] = useState(false); + const { showError } = useErrorModal(); const [editingRide, setEditingRide] = useState(null); @@ -127,6 +129,7 @@ const Schedule: React.FC = () => { setAllRiderRides(rides); } catch (error) { console.error('Failed to fetch rider rides:', error); + showError(`Failed to fetch rider rides: ${formatErrorMessage(error)}`, 'Rides Error'); } finally { setLoadingRides(false); } @@ -175,7 +178,7 @@ const Schedule: React.FC = () => { new Date(startISO).getTime() + 30 * 60 * 1000 ).toISOString(); - await axios.post('/api/rides', { + const result = await axios.post('/api/rides', { // Send location IDs (matching Admin flow) startLocation: formData.pickupLocation.id, endLocation: formData.dropoffLocation.id, @@ -187,6 +190,8 @@ const Schedule: React.FC = () => { schedulingState: 'unscheduled', }); + console.log('Result:', result); + // Refresh rides after successful creation await refreshRides(); console.log('Ride created successfully'); @@ -194,7 +199,7 @@ const Schedule: React.FC = () => { } catch (error: any) { console.error('Failed to create ride:', error); const msg = error?.response?.data?.err || 'Please try again.'; - showToast('Failed to create ride: ' + msg, ToastStatus.ERROR); + showError('Failed to create ride: ' + msg, 'Rides Error'); return false; } }; diff --git a/frontend/src/serviceWorker.ts b/frontend/src/serviceWorker.ts index 3a8dee7f0..9fcec6970 100644 --- a/frontend/src/serviceWorker.ts +++ b/frontend/src/serviceWorker.ts @@ -2,6 +2,7 @@ // register() is not called by default. import axios from './util/axios'; +import { showGlobalError, formatErrorMessage } from './context/errorModal'; // This lets the app load faster on subsequent visits in production, and gives // it offline capabilities. However, it also means that developers (and users) @@ -106,6 +107,10 @@ function registerValidSW(swUrl: string, config?: Config) { }) .catch((error) => { console.error('Error during service worker registration:', error); + showGlobalError( + `Failed to register service worker: ${formatErrorMessage(error)}. Some features may not be available offline.`, + 'Service Worker Error' + ); }); } @@ -146,6 +151,10 @@ export function unregister() { }) .catch((error) => { console.error(error.message); + showGlobalError( + `Failed to unregister service worker: ${formatErrorMessage(error)}`, + 'Service Worker Error' + ); }); } } From 8504163e305f51ecb097abae8b818dcb7752e507 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Fri, 5 Dec 2025 00:36:42 -0500 Subject: [PATCH 04/12] Added more modal uses --- .../EmployeeModal/EmployeeModal.tsx | 90 +++++++++++++------ .../components/ExportButton/ExportButton.tsx | 8 +- .../Locations/LocationFormModal.tsx | 5 +- frontend/src/components/Modal/RiderModal.tsx | 5 +- .../components/RideDetails/RideActions.tsx | 8 +- 5 files changed, 84 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/EmployeeModal/EmployeeModal.tsx b/frontend/src/components/EmployeeModal/EmployeeModal.tsx index 1ae3454d8..993e2474f 100644 --- a/frontend/src/components/EmployeeModal/EmployeeModal.tsx +++ b/frontend/src/components/EmployeeModal/EmployeeModal.tsx @@ -185,7 +185,9 @@ const EmployeeModal = ({ break; case '/api/admins': // Use optimistic create from context - await createAdmin(extractAdminData(employeeData)); + await createAdmin(extractAdminData(employeeData)).catch((error) => { + showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); + }); res = employeeData; // The context will handle server response and ID assignment break; default: @@ -236,9 +238,13 @@ const EmployeeModal = ({ async function deleteEmployee(id: string, endpoint: string): Promise { // Use optimistic delete from context if (endpoint === '/api/admins') { - await deleteAdmin(id); + await deleteAdmin(id).catch((error) => { + showError(`Failed to delete admin: ${formatErrorMessage(error)}`, 'Employees Error'); + }); } else if (endpoint === '/api/drivers') { - await deleteDriver(id); + await deleteDriver(id).catch((error) => { + showError(`Failed to delete driver: ${formatErrorMessage(error)}`, 'Employees Error'); + }); } } @@ -265,42 +271,74 @@ const EmployeeModal = ({ // If no employee exists, create one using a primary role. if (!currentId || currentId === '') { if (hasAdmin) { - employeeData.id = ( - await createEmployee(employeeData, '/api/admins') - ).id; - showToast( - `Created a new employee with the admin role`, - ToastStatus.SUCCESS - ); + try { + employeeData.id = ( + await createEmployee(employeeData, '/api/admins') + ).id; + showToast( + `Created a new employee with the admin role`, + ToastStatus.SUCCESS + ); + } catch (error) { + showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); + } } if (hasDriver) { - employeeData.id = ( - await createEmployee(employeeData, '/api/drivers') - ).id; - showToast( - `Created a new employee with the driver role`, - ToastStatus.SUCCESS - ); + try { + employeeData.id = ( + await createEmployee(employeeData, '/api/drivers') + ).id; + showToast( + `Created a new employee with the driver role`, + ToastStatus.SUCCESS + ); + } catch (error) { + showError(`Failed to create driver: ${formatErrorMessage(error)}`, 'Employees Error'); + } } } else { if (hasAdmin) { if (employeeData.admin) { - await updateEmployee(employeeData, '/api/admins'); + try { + await updateEmployee(employeeData, '/api/admins'); + } catch (error) { + showError(`Failed to update admin: ${formatErrorMessage(error)}`, 'Employees Error'); + } } else { - await createEmployee(employeeData, '/api/admins'); + try { + await createEmployee(employeeData, '/api/admins'); + } catch (error) { + showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); + } } } else if (employeeData.admin) { - await deleteEmployee(employeeData.id, '/api/admins'); + try { + await deleteEmployee(employeeData.id, '/api/admins'); + } catch (error) { + showError(`Failed to delete admin: ${formatErrorMessage(error)}`, 'Employees Error'); + } } if (hasDriver) { if (employeeData.driver) { - await updateEmployee(employeeData, '/api/drivers'); + try { + await updateEmployee(employeeData, '/api/drivers'); + } catch (error) { + showError(`Failed to update driver: ${formatErrorMessage(error)}`, 'Employees Error'); + } } else { - await createEmployee(employeeData, '/api/drivers'); + try { + await createEmployee(employeeData, '/api/drivers'); + } catch (error) { + showError(`Failed to create driver: ${formatErrorMessage(error)}`, 'Employees Error'); + } } } else if (employeeData.driver) { - await deleteEmployee(employeeData.id, '/api/drivers'); + try { + await deleteEmployee(employeeData.id, '/api/drivers'); + } catch (error) { + showError(`Failed to delete driver: ${formatErrorMessage(error)}`, 'Employees Error'); + } } } let id = employeeData.id; @@ -367,7 +405,9 @@ const EmployeeModal = ({ : 'Drivers'; try { setIsUploadingImage(true); - await uploadEmployeePhoto(id, targetTable, imageBase64); + await uploadEmployeePhoto(id, targetTable, imageBase64).catch((error) => { + showError(`Failed to upload photo: ${formatErrorMessage(error)}`, 'Employees Error'); + }); } catch (uploadError) { showError( `Employee created but photo upload failed: ${formatErrorMessage(uploadError)}. You can try uploading the photo again later.`, @@ -380,7 +420,7 @@ const EmployeeModal = ({ } // Note: No need to manually refresh - optimistic updates handle this automatically - showToast(`Employee information processed`, ToastStatus.SUCCESS); + showToast('Employee information processed', ToastStatus.SUCCESS); } catch (error) { showError( `An error occurred while saving employee: ${formatErrorMessage(error)}`, diff --git a/frontend/src/components/ExportButton/ExportButton.tsx b/frontend/src/components/ExportButton/ExportButton.tsx index d94715399..e841919fe 100644 --- a/frontend/src/components/ExportButton/ExportButton.tsx +++ b/frontend/src/components/ExportButton/ExportButton.tsx @@ -5,6 +5,8 @@ import { Button } from '../FormElements/FormElements'; import styles from './exportButton.module.css'; import { ToastStatus, useToast } from '../../context/toastContext'; import axios from '../../util/axios'; +import { formatErrorMessage } from '../../context/errorModal'; +import { useErrorModal } from '../../context/errorModal'; type clickHandler = { toastMsg: string; @@ -21,6 +23,7 @@ const ExportButton = ({ }: clickHandler) => { const [downloadData, setDownloadData] = useState(''); const { showToast } = useToast(); + const { showError } = useErrorModal(); const csvLink = useRef< CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement } >(null); @@ -42,7 +45,10 @@ const ExportButton = ({ csvLink.current.link.click(); } }) - .then(() => showToast(toastMsg, ToastStatus.SUCCESS)); + .then(() => showToast(toastMsg, ToastStatus.SUCCESS)) + .catch((error) => { + showError(`Failed to download data: ${formatErrorMessage(error)}`, 'Export Error'); + }); }; return ( diff --git a/frontend/src/components/Locations/LocationFormModal.tsx b/frontend/src/components/Locations/LocationFormModal.tsx index f41b38f2d..f352cb5f1 100644 --- a/frontend/src/components/Locations/LocationFormModal.tsx +++ b/frontend/src/components/Locations/LocationFormModal.tsx @@ -24,6 +24,7 @@ import { Location, Tag } from 'types'; import styles from './locations.module.css'; import LocationImagesUpload, { LocationImage } from './LocationImagesUpload'; import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; +import { useToast, ToastStatus } from '../../context/toastContext'; const CAMPUS_OPTIONS = [ { value: Tag.NORTH, label: 'North Campus' }, @@ -69,7 +70,8 @@ export const LocationFormModal: React.FC = ({ const [error, setError] = useState(null); const [locationImages, setLocationImages] = useState([]); const { showError } = useErrorModal(); - + const { showToast } = useToast(); + useEffect(() => { if (!open) return; @@ -157,6 +159,7 @@ export const LocationFormModal: React.FC = ({ imagesList: locationImages, }; onSubmit(updatedLocation); + showToast('Location saved successfully', ToastStatus.SUCCESS); onClose(); }; diff --git a/frontend/src/components/Modal/RiderModal.tsx b/frontend/src/components/Modal/RiderModal.tsx index fd132edbc..bc0f56242 100644 --- a/frontend/src/components/Modal/RiderModal.tsx +++ b/frontend/src/components/Modal/RiderModal.tsx @@ -8,6 +8,7 @@ import { edit, trash, trashbig, red_trash } from '../../icons/other/index'; import AuthContext from '../../context/auth'; import { ToastStatus, useToast } from '../../context/toastContext'; import axios from '../../util/axios'; +import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; type RiderModalProps = { existingRider?: Rider; @@ -27,7 +28,7 @@ const RiderModal = ({ const [isSubmitted, setIsSubmitted] = useState(false); const { showToast } = useToast(); const { refreshRiders } = useRiders(); - + const { showError } = useErrorModal(); const closeModal = () => setIsOpen(false); const saveDataThen = (next: () => void) => (data: ObjectType) => { @@ -55,6 +56,8 @@ const RiderModal = ({ if (isRiderWeb) { refreshUser(); } + }).catch((error) => { + showError(`Failed to save student: ${formatErrorMessage(error)}`, 'Students Error'); }); setIsSubmitted(false); } diff --git a/frontend/src/components/RideDetails/RideActions.tsx b/frontend/src/components/RideDetails/RideActions.tsx index 7e5c2f5cb..e99431827 100644 --- a/frontend/src/components/RideDetails/RideActions.tsx +++ b/frontend/src/components/RideDetails/RideActions.tsx @@ -182,10 +182,10 @@ const RideActions: React.FC = ({ onClose(); // Close modal after creating new ride } } else { - // const message = isNewRide(ride) - // ? 'Failed to create ride' - // : 'Failed to save ride'; - // showToast(message, ToastStatus.ERROR); + const message = isNewRide(ride) + ? 'Failed to create ride' + : 'Failed to save ride'; + showError(message, 'Rides Error'); } } catch (error) { console.error('Error saving ride:', error); From 9996935744ff684a8c33cc4404d61ef919ff1556 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Fri, 5 Dec 2025 00:38:38 -0500 Subject: [PATCH 05/12] Small changes --- frontend/src/components/RideDetails/RideActions.tsx | 1 + frontend/src/components/RideDetails/RideOverview.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/RideDetails/RideActions.tsx b/frontend/src/components/RideDetails/RideActions.tsx index e99431827..1c3302c6a 100644 --- a/frontend/src/components/RideDetails/RideActions.tsx +++ b/frontend/src/components/RideDetails/RideActions.tsx @@ -204,6 +204,7 @@ const RideActions: React.FC = ({ const handleReport = () => { // In a real app, open report issue dialog + showError('Contact admin to report issues with a ride.', 'Report Feature Not Available'); }; const renderRiderActions = () => { diff --git a/frontend/src/components/RideDetails/RideOverview.tsx b/frontend/src/components/RideDetails/RideOverview.tsx index 720c0688e..dd84a8ae6 100644 --- a/frontend/src/components/RideDetails/RideOverview.tsx +++ b/frontend/src/components/RideDetails/RideOverview.tsx @@ -687,7 +687,7 @@ const RideOverview: React.FC = ({ userRole }) => { color="textSecondary" sx={{ fontStyle: 'italic' }} > - Note: Full recurrence functionality coming soon + Full recurrence functionality coming soon )} From b7fd46d1fd9e2e50e1badb544906a1d6ba0449ce Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Fri, 5 Dec 2025 00:49:06 -0500 Subject: [PATCH 06/12] Fix time validation Add CULift requirements + lower arbitrary minimum ride length --- .../RideDetails/RideEditContext.tsx | 2 +- .../components/RideDetails/RideOverview.tsx | 2 +- .../components/RideDetails/TimeValidation.tsx | 38 ++++++++++++++++++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/RideDetails/RideEditContext.tsx b/frontend/src/components/RideDetails/RideEditContext.tsx index a13e6c3d9..bcb2fdb4b 100644 --- a/frontend/src/components/RideDetails/RideEditContext.tsx +++ b/frontend/src/components/RideDetails/RideEditContext.tsx @@ -159,7 +159,7 @@ export const RideEditProvider: React.FC = ({ { allowPastTimes: !isNewRide(editedRide), maxDurationHours: 24, - minDurationMinutes: 5, + minDurationMinutes: 1, } ); diff --git a/frontend/src/components/RideDetails/RideOverview.tsx b/frontend/src/components/RideDetails/RideOverview.tsx index dd84a8ae6..2fd20c531 100644 --- a/frontend/src/components/RideDetails/RideOverview.tsx +++ b/frontend/src/components/RideDetails/RideOverview.tsx @@ -402,7 +402,7 @@ const RideOverview: React.FC = ({ userRole }) => { { allowPastTimes, maxDurationHours: 24, - minDurationMinutes: 5, + minDurationMinutes: 1, } ); diff --git a/frontend/src/components/RideDetails/TimeValidation.tsx b/frontend/src/components/RideDetails/TimeValidation.tsx index e81925f87..45541a94a 100644 --- a/frontend/src/components/RideDetails/TimeValidation.tsx +++ b/frontend/src/components/RideDetails/TimeValidation.tsx @@ -6,7 +6,9 @@ export interface TimeValidationError { | 'end_time_before_start' | 'same_time' | 'too_long_duration' - | 'invalid_time'; + | 'invalid_time' + | 'weekend_occurrence' + | 'scheduling_deadline_passed'; message: string; } @@ -27,7 +29,7 @@ export const validateRideTimes = ( const { allowPastTimes = false, maxDurationHours = 24, - minDurationMinutes = 5, + minDurationMinutes = 1, } = options; const errors: TimeValidationError[] = []; @@ -78,6 +80,38 @@ export const validateRideTimes = ( }); } + // Check weekends + if (start.day() === 0 || start.day() === 6 || end.day() === 0 || end.day() === 6) { + errors.push({ + type: 'weekend_occurrence', + message: 'Ride cannot occur on a weekend', + }); + } + + // Check that rides must be scheduled between 7:45am and 10:00 pm + if (start.isBefore(now.hour(7).minute(45)) || end.isAfter(now.hour(22))) { + errors.push({ + type: 'invalid_time', + message: 'Ride must be scheduled between 7:45am and 10:00 pm', + }); + } + + // Check that rides must be scheduled by 10am the previous business day + const rideStartTime = dayjs(startTime); + + let previousBusinessDay = rideStartTime.subtract(1, 'day'); + while (previousBusinessDay.day() === 0 || previousBusinessDay.day() === 6) { + previousBusinessDay = previousBusinessDay.subtract(1, 'day'); + } + const deadline = previousBusinessDay.set('hour', 10).set('minute', 0).set('second', 0); + + if (now.isAfter(deadline)) { + errors.push({ + type: 'scheduling_deadline_passed', + message: `Ride must be scheduled by 10am on ${previousBusinessDay.format('dddd')} (${previousBusinessDay.format('MM/DD/YYYY')})`, + }); + } + return { isValid: errors.length === 0, errors, From 77df0f6537d98c61433fb54bfc1de24119d62711 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Sun, 21 Dec 2025 23:29:12 -0500 Subject: [PATCH 07/12] Add validation to ride editing --- frontend/src/util/rideValidation.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/util/rideValidation.ts b/frontend/src/util/rideValidation.ts index 508ea185f..e3c13aa9b 100644 --- a/frontend/src/util/rideValidation.ts +++ b/frontend/src/util/rideValidation.ts @@ -52,6 +52,19 @@ export function isRideCompleted(ride: RideType): boolean { * @returns True if the user can edit the ride */ export function canEditRide(ride: RideType, userRole: UserRole): boolean { + // Past or completed rides cannot be edited + if (isRidePast(ride) || isRideCompleted(ride) || isRideActive(ride)) { + return false; + } + + // Time-based check for past/active ride + const now = new Date(); + const rideStartTime = new Date(ride.startTime); + + if (now > rideStartTime) { + return false; + } + // Drivers cannot edit rides if (userRole === 'driver') { return false; From e749e531fbc071b53807eff2006ba5d175e070eb Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Mon, 22 Dec 2025 00:22:52 -0500 Subject: [PATCH 08/12] Add validation to ride creation Also add more descriptive error messages --- .../components/RideDetails/RideActions.tsx | 6 +-- .../RideDetails/RideEditContext.tsx | 28 ++++++------ .../components/RideDetails/TimeValidation.tsx | 18 +++++--- .../RiderComponents/RequestRideDialog.tsx | 44 +++++++++++++++---- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/RideDetails/RideActions.tsx b/frontend/src/components/RideDetails/RideActions.tsx index 1c3302c6a..289b9cb01 100644 --- a/frontend/src/components/RideDetails/RideActions.tsx +++ b/frontend/src/components/RideDetails/RideActions.tsx @@ -164,7 +164,7 @@ const RideActions: React.FC = ({ const handleSave = async () => { setSaving(true); try { - const success = await saveChanges(); + const [success, errMessage] = await saveChanges(); if (success) { const message = isNewRide(ride) ? 'Ride created successfully' @@ -183,8 +183,8 @@ const RideActions: React.FC = ({ } } else { const message = isNewRide(ride) - ? 'Failed to create ride' - : 'Failed to save ride'; + ? 'Failed to create ride due to the following errors: ' + errMessage + '.' + : 'Failed to save ride due to the following errors: ' + errMessage + '.'; showError(message, 'Rides Error'); } } catch (error) { diff --git a/frontend/src/components/RideDetails/RideEditContext.tsx b/frontend/src/components/RideDetails/RideEditContext.tsx index bcb2fdb4b..cd54bb749 100644 --- a/frontend/src/components/RideDetails/RideEditContext.tsx +++ b/frontend/src/components/RideDetails/RideEditContext.tsx @@ -12,12 +12,11 @@ import { canEditRide, UserRole } from '../../util/rideValidation'; import { isNewRide, hasRideChanges, - getRideChanges, } from '../../util/modelFixtures'; import { validateRideTimes } from './TimeValidation'; import { useRides } from '../../context/RidesContext'; -import { useToast, ToastStatus } from '../../context/toastContext'; -import { useErrorModal, formatErrorMessage } from '../../context/errorModal'; +import { useToast } from '../../context/toastContext'; +import { useErrorModal } from '../../context/errorModal'; interface RideEditContextType { isEditing: boolean; @@ -26,7 +25,7 @@ interface RideEditContextType { startEditing: () => void; stopEditing: () => void; updateRideField: (field: keyof RideType, value: any) => void; - saveChanges: () => Promise; + saveChanges: () => Promise<[boolean, string]>; hasChanges: boolean; canEdit: boolean; userRole: UserRole; @@ -146,9 +145,9 @@ export const RideEditProvider: React.FC = ({ return hasChangesResult; }, [editedRide, originalRide]); - const saveChanges = useCallback(async (): Promise => { + const saveChanges = useCallback(async (): Promise<[boolean, string]> => { if (!editedRide) { - return false; + return [false, 'No ride to save']; } // Validate ride times @@ -165,10 +164,11 @@ export const RideEditProvider: React.FC = ({ if (!timeValidation.isValid) { console.error('Time validation failed:', timeValidation.errors); + const errMessages = timeValidation.errors.map((err) => err.message).join(', '); const firstErr = timeValidation.errors[0]?.message || 'Invalid time values'; - showError(firstErr, 'Ride Edit Error'); - return false; + showError(errMessages || firstErr, 'Ride Edit Error'); + return [false, errMessages || firstErr]; } } @@ -183,7 +183,7 @@ export const RideEditProvider: React.FC = ({ ) { console.error('Missing required fields for new ride'); showError('Please select pickup, dropoff, start time, and end time.', 'Ride Edit Error'); - return false; + return [false, 'Missing required fields for new ride']; } try { @@ -209,7 +209,7 @@ export const RideEditProvider: React.FC = ({ } stopEditing(); - return true; + return [true, '']; } catch (error: any) { console.error('Failed to create new ride:', error); const msg = @@ -217,12 +217,12 @@ export const RideEditProvider: React.FC = ({ error?.response?.data?.err || 'Failed to create ride.'; showError(msg, 'Rides Error'); - return false; + return [false, msg]; } } else { // Existing ride update logic if (!originalRide || !hasChanges()) { - return false; + return [false, 'No changes to save']; } try { @@ -260,7 +260,7 @@ export const RideEditProvider: React.FC = ({ } stopEditing(); - return true; + return [true, '']; } catch (error: any) { console.error('Failed to save ride changes:', error); const msg = @@ -268,7 +268,7 @@ export const RideEditProvider: React.FC = ({ error?.response?.data?.err || 'Failed to save changes.'; showError(msg, 'Rides Error'); - return false; + return [false, msg]; } } }, [ diff --git a/frontend/src/components/RideDetails/TimeValidation.tsx b/frontend/src/components/RideDetails/TimeValidation.tsx index 45541a94a..b3e4915d1 100644 --- a/frontend/src/components/RideDetails/TimeValidation.tsx +++ b/frontend/src/components/RideDetails/TimeValidation.tsx @@ -89,17 +89,23 @@ export const validateRideTimes = ( } // Check that rides must be scheduled between 7:45am and 10:00 pm - if (start.isBefore(now.hour(7).minute(45)) || end.isAfter(now.hour(22))) { + if (start.isBefore(start.hour(7).minute(45)) || end.isAfter(end.hour(22))) { errors.push({ type: 'invalid_time', message: 'Ride must be scheduled between 7:45am and 10:00 pm', }); } - // Check that rides must be scheduled by 10am the previous business day - const rideStartTime = dayjs(startTime); - - let previousBusinessDay = rideStartTime.subtract(1, 'day'); + // Check that ride times are scheduled in five-minute intervals + if (start.minute() % 5 !== 0 || end.minute() % 5 !== 0) { + errors.push({ + type: 'invalid_time', + message: 'Start and end time must be in five-minute intervals', + }); + } + + // Check that rides must be scheduled by 10am the previous business day + let previousBusinessDay = start.subtract(1, 'day'); while (previousBusinessDay.day() === 0 || previousBusinessDay.day() === 6) { previousBusinessDay = previousBusinessDay.subtract(1, 'day'); } @@ -108,7 +114,7 @@ export const validateRideTimes = ( if (now.isAfter(deadline)) { errors.push({ type: 'scheduling_deadline_passed', - message: `Ride must be scheduled by 10am on ${previousBusinessDay.format('dddd')} (${previousBusinessDay.format('MM/DD/YYYY')})`, + message: `Ride must be scheduled by 10am on previous day (${previousBusinessDay.format('dddd')} ${previousBusinessDay.format('MM/DD/YYYY')})`, }); } diff --git a/frontend/src/components/RiderComponents/RequestRideDialog.tsx b/frontend/src/components/RiderComponents/RequestRideDialog.tsx index ccf707e33..24680dbde 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -26,15 +26,17 @@ import { TimePicker, } from '@mui/x-date-pickers'; import { APIProvider } from '@vis.gl/react-google-maps'; +import dayjs from 'dayjs'; 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 { useLocations } from '../../context/LocationsContext'; import { useToast, ToastStatus } from '../../context/toastContext'; import { formatErrorMessage } from '../../context/errorModal'; +import { validateRideTimes } from 'components/RideDetails/TimeValidation'; +import { useErrorModal } from '../../context/errorModal'; type RepeatOption = 'none' | 'daily' | 'weekly' | 'custom'; @@ -115,6 +117,7 @@ const RequestRideDialog: React.FC = ({ const { locations } = useLocations(); const { showToast } = useToast(); + const { showError } = useErrorModal(); const [formData, setFormData] = useState({ pickupLocation: null, dropoffLocation: null, @@ -470,6 +473,7 @@ const RequestRideDialog: React.FC = ({ try { let finalPickup = formData.pickupLocation; let finalDropoff = formData.dropoffLocation; + let result: boolean | void = false; // only create custom pickup if it's "Other" if (finalPickup?.name === Other.name) { @@ -508,13 +512,37 @@ const RequestRideDialog: React.FC = ({ setCustomDropoff(false); throw new Error('Start and end location are too simiilar'); } - - const result = await onSubmit({ - ...formData, - pickupLocation: finalPickup, - dropoffLocation: finalDropoff, - }); - + + const datetime = dayjs(formData.time) + .set('date', formData.date!.getDate()) + .set('month', formData.date!.getMonth()) + .set('year', formData.date!.getFullYear()); + + const timeValidation = validateRideTimes( + datetime, // start time + datetime.add(30, 'minute'), // end time + { + allowPastTimes: false, + maxDurationHours: 24, + minDurationMinutes: 5, + } + ); + + if (!timeValidation.isValid) { + console.error('Time validation failed:', timeValidation.errors); + const errMessages = timeValidation.errors.map((err) => err.message).join(', '); + const firstErr = + timeValidation.errors[0]?.message || 'Invalid time values'; + showError("Could not create ride due to following time validation issues: " + (errMessages || firstErr), 'Time Validation Error'); + result = false; + } + else { + result = await onSubmit({ + ...formData, + pickupLocation: finalPickup, + dropoffLocation: finalDropoff, + }); + } if (result !== false) { onClose(); showToast('Changes saved successfully', ToastStatus.SUCCESS); From 3af5cad7b364c81a23f9b282fcdfa6d53b66769f Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Fri, 26 Dec 2025 01:59:30 -0500 Subject: [PATCH 09/12] Add end date validation Rides can't be scheduled with users whose end date has passed --- server/src/router/ride.ts | 121 +++++++++++++++++++++++++++++++++++- server/tests/driver.test.ts | 4 +- server/tests/rider.test.ts | 4 +- 3 files changed, 122 insertions(+), 7 deletions(-) diff --git a/server/src/router/ride.ts b/server/src/router/ride.ts index b053e9947..fefd83eb7 100644 --- a/server/src/router/ride.ts +++ b/server/src/router/ride.ts @@ -9,7 +9,7 @@ import { Ride, Status, Type, RideType, SchedulingState } from '../models/ride'; import { Tag, LocationType } from '../models/location'; import { validateUser, daysUntilWeekday } from '../util'; import { DriverType } from '../models/driver'; -import { RiderType } from '../models/rider'; +import { Rider, RiderType } from '../models/rider'; import { notify } from '../util/notification'; import { Change } from '../util/types'; import { UserType } from '../models/subscription'; @@ -17,6 +17,53 @@ import { UserType } from '../models/subscription'; const router = express.Router(); const tableName = 'Rides'; +/** + * Ensure that all riders attached to a ride are still eligible based on their endDate. + * - A rider must be active (if the flag exists) + * - The ride's start date (in America/New_York) must be on or before the rider's endDate + */ +async function ensureRidersEligibleForRide( + riderIds: string[], + rideStartTimeIso: string +) { + if (!riderIds.length) return; + + const uniqueIds = Array.from(new Set(riderIds)); + const keys = uniqueIds.map((id) => ({ id })); + + const docs = (await Rider.batchGet(keys)) as any[]; + if (!docs || !docs.length) { + throw new Error('No riders found for this ride.'); + } + + const rideDateNy = moment + .tz(rideStartTimeIso, 'America/New_York') + .format('YYYY-MM-DD'); + + for (const doc of docs) { + if (!doc) continue; + const riderJson = doc.toJSON ? doc.toJSON() : doc; + const { id, email, active, endDate } = riderJson as { + id: string; + email: string; + active?: boolean; + endDate?: string; + }; + + if (active === false) { + throw new Error( + `Rider with email ${email} is not active and cannot be scheduled for rides.` + ); + } + + if (endDate && endDate < rideDateNy) { + throw new Error( + `Rider with email ${email} has an end date of ${endDate} and cannot be scheduled for a ride on ${rideDateNy}. Please contact admin to extend your end date` + ); + } + } +} + // Debug endpoint to get current user's JWT token router.get('/debug/token', validateUser('User'), (req, res) => { const token = req.headers.authorization?.replace('Bearer ', ''); @@ -296,7 +343,7 @@ router.get('/diagnose', async (_req, res) => { }); // Create a new ride -router.post('/', validateUser('User'), (req, res) => { +router.post('/', validateUser('User'), async (req, res) => { const { body } = req; const { startLocation, @@ -348,6 +395,33 @@ router.post('/', validateUser('User'), (req, res) => { return; } + // Enforce rider end dates: all riders on this ride must still be eligible. + // Accept both legacy single rider and new riders array formats. + const riderIdsForValidation: string[] = []; + if (body.riders && Array.isArray(body.riders) && body.riders.length > 0) { + body.riders.forEach((r: any) => { + if (typeof r === 'string') riderIdsForValidation.push(r); + else if (r && typeof r.id === 'string') riderIdsForValidation.push(r.id); + }); + } else if (body.rider) { + if (typeof body.rider === 'string') { + riderIdsForValidation.push(body.rider); + } else if (body.rider.id) { + riderIdsForValidation.push(body.rider.id); + } + } + + try { + await ensureRidersEligibleForRide(riderIdsForValidation, body.startTime); + } catch (error: any) { + res.status(400).send({ + err: + error?.message || + 'One or more riders are not eligible to be scheduled for this ride.', + }); + return; + } + // Determine scheduling state based on driver assignment const hasDriver = body.driver ? true : false; const schedulingState = @@ -437,7 +511,7 @@ router.put('/:id', validateUser('User'), (req, res) => { } //Check if id matches or user is admin - db.getById(res, Ride, id, tableName, (ride: RideType) => { + db.getById(res, Ride, id, tableName, async (ride: RideType) => { const { riders, driver } = ride; const userIsRider = riders && riders.some((rider) => rider.id === res.locals.user.id); @@ -447,6 +521,47 @@ router.put('/:id', validateUser('User'), (req, res) => { userIsRider || (driver && res.locals.user.id === driver.id) ) { + // Before applying the update, ensure all riders will still be eligible + // at the (possibly updated) startTime. + const updatedStartTime = body.startTime || ride.startTime; + + const riderIdsForValidation: string[] = []; + if (body.riders && Array.isArray(body.riders)) { + body.riders.forEach((r: any) => { + if (typeof r === 'string') riderIdsForValidation.push(r); + else if (r && typeof r.id === 'string') + riderIdsForValidation.push(r.id); + }); + } else if (body.rider) { + if (typeof body.rider === 'string') { + riderIdsForValidation.push(body.rider); + } else if (body.rider.id) { + riderIdsForValidation.push(body.rider.id); + } + } else if (riders && Array.isArray(riders)) { + riders.forEach((r: any) => { + if (!r) return; + if (typeof r === 'string') riderIdsForValidation.push(r); + else if (r.id) riderIdsForValidation.push(r.id); + }); + } else if ((ride as any).rider && (ride as any).rider.id) { + riderIdsForValidation.push((ride as any).rider.id); + } + + try { + await ensureRidersEligibleForRide( + riderIdsForValidation, + updatedStartTime + ); + } catch (error: any) { + res.status(400).send({ + err: + error?.message || + 'One or more riders are not eligible to be scheduled for this ride.', + }); + return; + } + db.update(res, Ride, { id }, body, tableName, async (doc) => { const ride = doc; const { userType } = res.locals.user; diff --git a/server/tests/driver.test.ts b/server/tests/driver.test.ts index e5b5bf0b3..bbe25cf9f 100644 --- a/server/tests/driver.test.ts +++ b/server/tests/driver.test.ts @@ -33,7 +33,7 @@ const testRider: Omit = { accessibility: [Accessibility.CRUTCHES, Accessibility.ASSISTANT], description: '', joinDate: '2023-03-09', - endDate: '2024-03-09', + endDate: '2099-03-09', address: '36 Colonial Ln, Ithaca, NY 14850', favoriteLocations: ['Test-Location 1'], organization: Organization.REDRUNNER, @@ -50,7 +50,7 @@ const testStatRider: RiderType = { accessibility: [Accessibility.ASSISTANT], description: '', joinDate: '2023-03-09', - endDate: '2024-03-09', + endDate: '2099-03-09', address: '36 Colonial Ln, Ithaca, NY 14850', favoriteLocations: ['1'], organization: Organization.REDRUNNER, diff --git a/server/tests/rider.test.ts b/server/tests/rider.test.ts index 06b1e2805..cc9c82d68 100644 --- a/server/tests/rider.test.ts +++ b/server/tests/rider.test.ts @@ -92,7 +92,7 @@ const testRiders = [ accessibility: 'Crutches', description: '', joinDate: '2023-03-09', - endDate: '2024-03-09', + endDate: '2099-03-09', address: '36 Colonial Ln, Ithaca, NY 14850', favoriteLocations: ['1'], organization: Organization.REDRUNNER, @@ -109,7 +109,7 @@ const testRiders = [ accessibility: 'Crutches', description: 'needs help', joinDate: '2023-03-09', - endDate: '2024-03-09', + endDate: '2099-03-09', address: '37 Colonial Ln, Ithaca, NY 14850', favoriteLocations: ['2'], organization: Organization.CULIFT, From 4c491e899e612047d28e2d6029c31b59a0667857 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Wed, 8 Apr 2026 21:27:23 -0400 Subject: [PATCH 10/12] lint --- .../components/AuthManager/AuthManager.tsx | 5 +- .../EmployeeModal/EmployeeModal.tsx | 75 +++++++++++++++---- .../components/ExportButton/ExportButton.tsx | 5 +- .../Locations/LocationFormModal.tsx | 2 +- .../src/components/Locations/PlacesSearch.tsx | 5 +- frontend/src/components/Modal/RiderModal.tsx | 32 ++++---- .../components/RideDetails/RideActions.tsx | 18 ++++- .../RideDetails/RideEditContext.tsx | 14 ++-- .../components/RideDetails/RideLocations.tsx | 5 +- .../components/RideDetails/RideOverview.tsx | 7 +- .../src/components/RideDetails/RidePeople.tsx | 5 +- .../components/RideDetails/TimeValidation.tsx | 20 +++-- .../RiderComponents/RequestRideDialog.tsx | 12 ++- .../RiderComponents/RequestRideMap.tsx | 5 +- .../RequestRidePlacesSearch.tsx | 5 +- .../src/components/UserDetail/ActionsCard.tsx | 5 +- .../UserDetail/hooks/useUserDetailData.ts | 5 +- frontend/src/context/EmployeesContext.tsx | 40 ++++++++-- frontend/src/context/RidersContext.tsx | 25 +++++-- frontend/src/context/RidesContext.tsx | 51 ++++++++++--- frontend/src/context/errorModal.tsx | 43 ++++++++--- frontend/src/index.tsx | 5 +- frontend/src/pages/Driver/Rides.tsx | 5 +- frontend/src/serviceWorker.ts | 4 +- 24 files changed, 297 insertions(+), 101 deletions(-) diff --git a/frontend/src/components/AuthManager/AuthManager.tsx b/frontend/src/components/AuthManager/AuthManager.tsx index 579018911..2890b383b 100644 --- a/frontend/src/components/AuthManager/AuthManager.tsx +++ b/frontend/src/components/AuthManager/AuthManager.tsx @@ -215,7 +215,10 @@ const AuthManager = () => { } } catch (error) { console.error('Error decrypting JWT:', error); - showError(`Error decrypting JWT: ${formatErrorMessage(error)}`, 'Authentication Error'); + showError( + `Error decrypting JWT: ${formatErrorMessage(error)}`, + 'Authentication Error' + ); } return ''; } diff --git a/frontend/src/components/EmployeeModal/EmployeeModal.tsx b/frontend/src/components/EmployeeModal/EmployeeModal.tsx index 4483fd418..d8cd1fc39 100644 --- a/frontend/src/components/EmployeeModal/EmployeeModal.tsx +++ b/frontend/src/components/EmployeeModal/EmployeeModal.tsx @@ -160,7 +160,10 @@ const EmployeeModal = ({ }); } catch (error) { console.error('Error uploading photo:', error); - showError(`Error uploading photo: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Error uploading photo: ${formatErrorMessage(error)}`, + 'Employees Error' + ); throw new Error('Failed to upload employee photo. Please try again.'); } } @@ -187,7 +190,10 @@ const EmployeeModal = ({ case '/api/admins': // Use optimistic create from context await createAdmin(extractAdminData(employeeData)).catch((error) => { - showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to create admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); }); res = employeeData; // The context will handle server response and ID assignment break; @@ -240,11 +246,17 @@ const EmployeeModal = ({ // Use optimistic delete from context if (endpoint === '/api/admins') { await deleteAdmin(id).catch((error) => { - showError(`Failed to delete admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to delete admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); }); } else if (endpoint === '/api/drivers') { await deleteDriver(id).catch((error) => { - showError(`Failed to delete driver: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to delete driver: ${formatErrorMessage(error)}`, + 'Employees Error' + ); }); } } @@ -281,7 +293,10 @@ const EmployeeModal = ({ ToastStatus.SUCCESS ); } catch (error) { - showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to create admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } if (hasDriver) { @@ -294,7 +309,10 @@ const EmployeeModal = ({ ToastStatus.SUCCESS ); } catch (error) { - showError(`Failed to create driver: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to create driver: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } } else { @@ -303,20 +321,29 @@ const EmployeeModal = ({ try { await updateEmployee(employeeData, '/api/admins'); } catch (error) { - showError(`Failed to update admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to update admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } else { try { await createEmployee(employeeData, '/api/admins'); } catch (error) { - showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to create admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } } else if (employeeData.admin) { try { await deleteEmployee(employeeData.id, '/api/admins'); } catch (error) { - showError(`Failed to delete admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to delete admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } @@ -325,20 +352,29 @@ const EmployeeModal = ({ try { await updateEmployee(employeeData, '/api/drivers'); } catch (error) { - showError(`Failed to update driver: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to update driver: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } else { try { await createEmployee(employeeData, '/api/drivers'); } catch (error) { - showError(`Failed to create driver: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to create driver: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } } else if (employeeData.driver) { try { await deleteEmployee(employeeData.id, '/api/drivers'); } catch (error) { - showError(`Failed to delete driver: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to delete driver: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } } } @@ -406,12 +442,19 @@ const EmployeeModal = ({ : 'Drivers'; try { setIsUploadingImage(true); - await uploadEmployeePhoto(id, targetTable, imageBase64).catch((error) => { - showError(`Failed to upload photo: ${formatErrorMessage(error)}`, 'Employees Error'); - }); + await uploadEmployeePhoto(id, targetTable, imageBase64).catch( + (error) => { + showError( + `Failed to upload photo: ${formatErrorMessage(error)}`, + 'Employees Error' + ); + } + ); } catch (uploadError) { showError( - `Employee created but photo upload failed: ${formatErrorMessage(uploadError)}. You can try uploading the photo again later.`, + `Employee created but photo upload failed: ${formatErrorMessage( + uploadError + )}. You can try uploading the photo again later.`, 'Employees Error' ); // Don't throw here - we want the employee creation to succeed even if photo upload fails diff --git a/frontend/src/components/ExportButton/ExportButton.tsx b/frontend/src/components/ExportButton/ExportButton.tsx index e841919fe..bf1e9fd79 100644 --- a/frontend/src/components/ExportButton/ExportButton.tsx +++ b/frontend/src/components/ExportButton/ExportButton.tsx @@ -47,7 +47,10 @@ const ExportButton = ({ }) .then(() => showToast(toastMsg, ToastStatus.SUCCESS)) .catch((error) => { - showError(`Failed to download data: ${formatErrorMessage(error)}`, 'Export Error'); + showError( + `Failed to download data: ${formatErrorMessage(error)}`, + 'Export Error' + ); }); }; diff --git a/frontend/src/components/Locations/LocationFormModal.tsx b/frontend/src/components/Locations/LocationFormModal.tsx index 28f49232f..c1b12fb54 100644 --- a/frontend/src/components/Locations/LocationFormModal.tsx +++ b/frontend/src/components/Locations/LocationFormModal.tsx @@ -72,7 +72,7 @@ export const LocationFormModal: React.FC = ({ const [locationImages, setLocationImages] = useState([]); const { showError } = useErrorModal(); const { showToast } = useToast(); - + useEffect(() => { if (!open) return; diff --git a/frontend/src/components/Locations/PlacesSearch.tsx b/frontend/src/components/Locations/PlacesSearch.tsx index a96a0861b..1e95b0616 100644 --- a/frontend/src/components/Locations/PlacesSearch.tsx +++ b/frontend/src/components/Locations/PlacesSearch.tsx @@ -56,7 +56,10 @@ const PlacesSearch = ({ setIsLoading(false); setError('Error searching for address'); setResults([]); - showError(`Error searching for address: ${formatErrorMessage(error)}`, 'Address Search Error'); + showError( + `Error searching for address: ${formatErrorMessage(error)}`, + 'Address Search Error' + ); } }, [map] diff --git a/frontend/src/components/Modal/RiderModal.tsx b/frontend/src/components/Modal/RiderModal.tsx index aa24555ef..0aa1f39da 100644 --- a/frontend/src/components/Modal/RiderModal.tsx +++ b/frontend/src/components/Modal/RiderModal.tsx @@ -45,21 +45,23 @@ const RiderModal = ({ useEffect(() => { if (isSubmitted) { const method = existingRider ? axios.put : axios.post; - method( - `/api/riders/${!existingRider ? '' : existingRider.id}`, - formData - ).then(() => { - refreshRiders(); - showToast( - `The student has been ${!existingRider ? 'added' : 'edited'}`, - ToastStatus.SUCCESS - ); - if (isRiderWeb) { - refreshUser(); - } - }).catch((error) => { - showError(`Failed to save student: ${formatErrorMessage(error)}`, 'Students Error'); - }); + method(`/api/riders/${!existingRider ? '' : existingRider.id}`, formData) + .then(() => { + refreshRiders(); + showToast( + `The student has been ${!existingRider ? 'added' : 'edited'}`, + ToastStatus.SUCCESS + ); + if (isRiderWeb) { + refreshUser(); + } + }) + .catch((error) => { + showError( + `Failed to save student: ${formatErrorMessage(error)}`, + 'Students Error' + ); + }); setIsSubmitted(false); } }, [ diff --git a/frontend/src/components/RideDetails/RideActions.tsx b/frontend/src/components/RideDetails/RideActions.tsx index e7627a356..2f5339a7d 100644 --- a/frontend/src/components/RideDetails/RideActions.tsx +++ b/frontend/src/components/RideDetails/RideActions.tsx @@ -86,7 +86,10 @@ const RideActions: React.FC = ({ await refreshRides(); } catch (error) { console.error('Failed to update status:', error); - showError(`Failed to update status: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to update status: ${formatErrorMessage(error)}`, + 'Rides Error' + ); } finally { setUpdating(false); } @@ -122,7 +125,10 @@ const RideActions: React.FC = ({ showToast('Ride Cancelled', ToastStatus.SUCCESS); } catch (error) { console.error('Failed to cancel ride:', error); - showError(`Failed to cancel ride: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to cancel ride: ${formatErrorMessage(error)}`, + 'Rides Error' + ); } }; @@ -158,8 +164,12 @@ const RideActions: React.FC = ({ } } else { const message = isNewRide(ride) - ? 'Failed to create ride due to the following errors: ' + errMessage + '.' - : 'Failed to save ride due to the following errors: ' + errMessage + '.'; + ? 'Failed to create ride due to the following errors: ' + + errMessage + + '.' + : 'Failed to save ride due to the following errors: ' + + errMessage + + '.'; showError(message, 'Rides Error'); } } catch (error) { diff --git a/frontend/src/components/RideDetails/RideEditContext.tsx b/frontend/src/components/RideDetails/RideEditContext.tsx index 326586117..95a38a1ae 100644 --- a/frontend/src/components/RideDetails/RideEditContext.tsx +++ b/frontend/src/components/RideDetails/RideEditContext.tsx @@ -10,10 +10,7 @@ import { SchedulingState } from '../../types'; import { RideType } from '@carriage-web/shared/types/ride'; import axios from '../../util/axios'; import { canEditRide, UserRole } from '../../util/rideValidation'; -import { - isNewRide, - hasRideChanges, -} from '../../util/modelFixtures'; +import { isNewRide, hasRideChanges } from '../../util/modelFixtures'; import { validateRideTimes } from './TimeValidation'; import { useRides } from '../../context/RidesContext'; import { useToast } from '../../context/toastContext'; @@ -165,7 +162,9 @@ export const RideEditProvider: React.FC = ({ if (!timeValidation.isValid) { console.error('Time validation failed:', timeValidation.errors); - const errMessages = timeValidation.errors.map((err) => err.message).join(', '); + const errMessages = timeValidation.errors + .map((err) => err.message) + .join(', '); const firstErr = timeValidation.errors[0]?.message || 'Invalid time values'; showError(errMessages || firstErr, 'Ride Edit Error'); @@ -183,7 +182,10 @@ export const RideEditProvider: React.FC = ({ !editedRide.endTime ) { console.error('Missing required fields for new ride'); - showError('Please select pickup, dropoff, start time, and end time.', 'Ride Edit Error'); + showError( + 'Please select pickup, dropoff, start time, and end time.', + 'Ride Edit Error' + ); return [false, 'Missing required fields for new ride']; } diff --git a/frontend/src/components/RideDetails/RideLocations.tsx b/frontend/src/components/RideDetails/RideLocations.tsx index fdfc90a31..87ed71938 100644 --- a/frontend/src/components/RideDetails/RideLocations.tsx +++ b/frontend/src/components/RideDetails/RideLocations.tsx @@ -311,7 +311,10 @@ const RideMap: React.FC = ({ } } catch (error) { console.error('Error fetching route:', error); - showError(`Error fetching route: ${formatErrorMessage(error)}`, 'Maps Error'); + showError( + `Error fetching route: ${formatErrorMessage(error)}`, + 'Maps Error' + ); if (polylineRef.current) { polylineRef.current.setMap(null); polylineRef.current = null; diff --git a/frontend/src/components/RideDetails/RideOverview.tsx b/frontend/src/components/RideDetails/RideOverview.tsx index 01e1654bb..98a67a9b1 100644 --- a/frontend/src/components/RideDetails/RideOverview.tsx +++ b/frontend/src/components/RideDetails/RideOverview.tsx @@ -251,7 +251,10 @@ const RideOverview: React.FC = ({ userRole }) => { updateRideField('startTime', updatedStartTime.toISOString()); } catch (error) { console.error('Failed to update ride time:', error); - showError(`Error updating ride time: ${formatErrorMessage(error)}`, 'Ride Edit Error'); + showError( + `Error updating ride time: ${formatErrorMessage(error)}`, + 'Ride Edit Error' + ); } }; @@ -420,7 +423,7 @@ const RideOverview: React.FC = ({ userRole }) => { const hasEndTimeError = endTimeBeforeStartError !== undefined || durationError !== undefined; - + return ( <>
diff --git a/frontend/src/components/RideDetails/RidePeople.tsx b/frontend/src/components/RideDetails/RidePeople.tsx index bc7ff8098..c6bd2256a 100644 --- a/frontend/src/components/RideDetails/RidePeople.tsx +++ b/frontend/src/components/RideDetails/RidePeople.tsx @@ -191,7 +191,10 @@ const RidePeople: React.FC = ({ userRole }) => { console.error('Failed to fetch riders:', error); setRidersError('Failed to load riders'); setRiders([]); - showError(`Failed to fetch riders: ${formatErrorMessage(error)}`, 'Riders Error'); + showError( + `Failed to fetch riders: ${formatErrorMessage(error)}`, + 'Riders Error' + ); } finally { setLoadingRiders(false); } diff --git a/frontend/src/components/RideDetails/TimeValidation.tsx b/frontend/src/components/RideDetails/TimeValidation.tsx index b3e4915d1..19298ec5d 100644 --- a/frontend/src/components/RideDetails/TimeValidation.tsx +++ b/frontend/src/components/RideDetails/TimeValidation.tsx @@ -81,7 +81,12 @@ export const validateRideTimes = ( } // Check weekends - if (start.day() === 0 || start.day() === 6 || end.day() === 0 || end.day() === 6) { + if ( + start.day() === 0 || + start.day() === 6 || + end.day() === 0 || + end.day() === 6 + ) { errors.push({ type: 'weekend_occurrence', message: 'Ride cannot occur on a weekend', @@ -104,17 +109,22 @@ export const validateRideTimes = ( }); } - // Check that rides must be scheduled by 10am the previous business day + // Check that rides must be scheduled by 10am the previous business day let previousBusinessDay = start.subtract(1, 'day'); while (previousBusinessDay.day() === 0 || previousBusinessDay.day() === 6) { previousBusinessDay = previousBusinessDay.subtract(1, 'day'); } - const deadline = previousBusinessDay.set('hour', 10).set('minute', 0).set('second', 0); - + const deadline = previousBusinessDay + .set('hour', 10) + .set('minute', 0) + .set('second', 0); + if (now.isAfter(deadline)) { errors.push({ type: 'scheduling_deadline_passed', - message: `Ride must be scheduled by 10am on previous day (${previousBusinessDay.format('dddd')} ${previousBusinessDay.format('MM/DD/YYYY')})`, + message: `Ride must be scheduled by 10am on previous day (${previousBusinessDay.format( + 'dddd' + )} ${previousBusinessDay.format('MM/DD/YYYY')})`, }); } diff --git a/frontend/src/components/RiderComponents/RequestRideDialog.tsx b/frontend/src/components/RiderComponents/RequestRideDialog.tsx index fd1040ffb..fd8ece84d 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -533,7 +533,6 @@ const RequestRideDialog: React.FC = ({ finalDropoff = await createCustomLocation(customDropoffName); } - const datetime = dayjs(formData.time) .set('date', formData.date!.getDate()) .set('month', formData.date!.getMonth()) @@ -551,11 +550,13 @@ const RequestRideDialog: React.FC = ({ if (!timeValidation.isValid) { console.error('Time validation failed:', timeValidation.errors); - const errMessages = timeValidation.errors.map((err) => err.message).join(', '); + const errMessages = timeValidation.errors + .map((err) => err.message) + .join(', '); const firstErr = timeValidation.errors[0]?.message || 'Invalid time values'; showError( - "Could not create ride due to following time validation issues: " + + 'Could not create ride due to following time validation issues: ' + (errMessages || firstErr), 'Time Validation Error' ); @@ -580,7 +581,10 @@ const RequestRideDialog: React.FC = ({ } } catch (e) { console.error('Error submitting ride:', e); - showToast('Failed to save changes: ' + formatErrorMessage(e), ToastStatus.ERROR); + showToast( + 'Failed to save changes: ' + formatErrorMessage(e), + ToastStatus.ERROR + ); } }; diff --git a/frontend/src/components/RiderComponents/RequestRideMap.tsx b/frontend/src/components/RiderComponents/RequestRideMap.tsx index 4263ae0c4..7f8c3b665 100644 --- a/frontend/src/components/RiderComponents/RequestRideMap.tsx +++ b/frontend/src/components/RiderComponents/RequestRideMap.tsx @@ -114,7 +114,10 @@ const RequestRideMap: React.FC = ({ } } catch (error) { console.error('Error fetching route:', error); - showError(`Error fetching route: ${formatErrorMessage(error)}`, 'Maps Error'); + showError( + `Error fetching route: ${formatErrorMessage(error)}`, + 'Maps Error' + ); if (polylineRef.current) { polylineRef.current.setMap(null); polylineRef.current = null; diff --git a/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx b/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx index dccaee7bf..9b0e0d333 100644 --- a/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx +++ b/frontend/src/components/RiderComponents/RequestRidePlacesSearch.tsx @@ -78,7 +78,10 @@ const RequestRidePlacesSearch: React.FC = ({ setIsLoading(false); setError('Error searching for address'); setResults([]); - showError(`Error searching for address: ${formatErrorMessage(error)}`, 'Address Search Error'); + showError( + `Error searching for address: ${formatErrorMessage(error)}`, + 'Address Search Error' + ); } }, []); diff --git a/frontend/src/components/UserDetail/ActionsCard.tsx b/frontend/src/components/UserDetail/ActionsCard.tsx index ae3bc1406..fade63dc6 100644 --- a/frontend/src/components/UserDetail/ActionsCard.tsx +++ b/frontend/src/components/UserDetail/ActionsCard.tsx @@ -95,7 +95,10 @@ const ActionsCard: React.FC = ({ // triggers the useUserDetailData hook to update the local user state } catch (error) { console.error('Error updating rider status:', error); - showError(`Error updating rider status: ${formatErrorMessage(error)}`, 'Rider Error'); + showError( + `Error updating rider status: ${formatErrorMessage(error)}`, + 'Rider Error' + ); setToastMessage( `Failed to ${newActiveStatus ? 'activate' : 'deactivate'} rider.` ); diff --git a/frontend/src/components/UserDetail/hooks/useUserDetailData.ts b/frontend/src/components/UserDetail/hooks/useUserDetailData.ts index 24fd99529..7d27b4a8c 100644 --- a/frontend/src/components/UserDetail/hooks/useUserDetailData.ts +++ b/frontend/src/components/UserDetail/hooks/useUserDetailData.ts @@ -7,7 +7,10 @@ import { DriverType } from '@carriage-web/shared/types/driver'; import { useRiders } from '../../../context/RidersContext'; import { useEmployees } from '../../../context/EmployeesContext'; import axios from '../../../util/axios'; -import { showGlobalError, formatErrorMessage } from '../../../context/errorModal'; +import { + showGlobalError, + formatErrorMessage, +} from '../../../context/errorModal'; interface UserDetailData { user: Employee | RiderType | null; diff --git a/frontend/src/context/EmployeesContext.tsx b/frontend/src/context/EmployeesContext.tsx index 426fca20d..98b828a63 100644 --- a/frontend/src/context/EmployeesContext.tsx +++ b/frontend/src/context/EmployeesContext.tsx @@ -87,7 +87,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { } catch (error) { console.error('Failed to fetch drivers:', error); setError(error as Error); - showError(`Failed to fetch drivers: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to fetch drivers: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } }, []); @@ -103,7 +106,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { } catch (error) { console.error('Failed to fetch admins:', error); setError(error as Error); - showError(`Failed to fetch admins: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to fetch admins: ${formatErrorMessage(error)}`, + 'Employees Error' + ); } }, []); @@ -136,7 +142,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to update driver info:', error); setDrivers(originalDrivers); setError(error as Error); - showError(`Failed to update driver info: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to update driver info: ${formatErrorMessage(error)}`, + 'Employees Error' + ); throw error; } }, @@ -168,7 +177,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to create driver:', error); setDrivers((prevDrivers) => prevDrivers.filter((d) => d.id !== tempId)); setError(error as Error); - showError(`Failed to create driver: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to create driver: ${formatErrorMessage(error)}`, + 'Employees Error' + ); throw error; } }, []); @@ -189,7 +201,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to delete driver:', error); setDrivers(originalDrivers); setError(error as Error); - showError(`Failed to delete driver: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to delete driver: ${formatErrorMessage(error)}`, + 'Employees Error' + ); throw error; } }, @@ -225,7 +240,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to update admin info:', error); setAdmins(originalAdmins); setError(error as Error); - showError(`Failed to update admin info: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to update admin info: ${formatErrorMessage(error)}`, + 'Employees Error' + ); throw error; } }, @@ -255,7 +273,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to create admin:', error); setAdmins((prevAdmins) => prevAdmins.filter((a) => a.id !== tempId)); setError(error as Error); - showError(`Failed to create admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to create admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); throw error; } }, []); @@ -276,7 +297,10 @@ export const EmployeesProvider = ({ children }: EmployeesProviderProps) => { console.error('Failed to delete admin:', error); setAdmins(originalAdmins); setError(error as Error); - showError(`Failed to delete admin: ${formatErrorMessage(error)}`, 'Employees Error'); + showError( + `Failed to delete admin: ${formatErrorMessage(error)}`, + 'Employees Error' + ); throw error; } }, diff --git a/frontend/src/context/RidersContext.tsx b/frontend/src/context/RidersContext.tsx index 9598fc755..7202b18b5 100644 --- a/frontend/src/context/RidersContext.tsx +++ b/frontend/src/context/RidersContext.tsx @@ -66,7 +66,10 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { } } catch (error) { console.error('Failed to fetch riders:', error); - showError(`Failed to fetch riders: ${formatErrorMessage(error)}`, 'Riders Error'); + showError( + `Failed to fetch riders: ${formatErrorMessage(error)}`, + 'Riders Error' + ); } finally { if (componentMounted.current) { setLoading(false); @@ -90,7 +93,10 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to update rider active status:', error); await refreshRiders(); // Refresh to get server state - showError(`Failed to update rider active status: ${formatErrorMessage(error)}`, 'Riders Error'); + showError( + `Failed to update rider active status: ${formatErrorMessage(error)}`, + 'Riders Error' + ); throw error; } }, @@ -122,7 +128,10 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to update rider info:', error); setRiders(originalRiders); - showError(`Failed to update rider info: ${formatErrorMessage(error)}`, 'Riders Error'); + showError( + `Failed to update rider info: ${formatErrorMessage(error)}`, + 'Riders Error' + ); throw error; } }, @@ -149,7 +158,10 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to create rider:', error); setRiders((prevRiders) => prevRiders.filter((r) => r.id !== tempId)); - showError(`Failed to create rider: ${formatErrorMessage(error)}`, 'Riders Error'); + showError( + `Failed to create rider: ${formatErrorMessage(error)}`, + 'Riders Error' + ); throw error; } }, []); @@ -169,7 +181,10 @@ export const RidersProvider = ({ children }: RidersProviderProps) => { // Rollback on error console.error('Failed to delete rider:', error); setRiders(originalRiders); - showError(`Failed to delete rider: ${formatErrorMessage(error)}`, 'Riders Error'); + showError( + `Failed to delete rider: ${formatErrorMessage(error)}`, + 'Riders Error' + ); throw error; } }, diff --git a/frontend/src/context/RidesContext.tsx b/frontend/src/context/RidesContext.tsx index fa15ff1ba..ce02bfe99 100644 --- a/frontend/src/context/RidesContext.tsx +++ b/frontend/src/context/RidesContext.tsx @@ -112,7 +112,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } catch (error) { console.error('Error refreshing rides:', error); setError(error as Error); - showError(`Error refreshing rides: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Error refreshing rides: ${formatErrorMessage(error)}`, + 'Rides Error' + ); } finally { setLoading(false); } @@ -247,7 +250,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { updateRideInLists(rideId, () => originalRide); } setError(error as Error); - showError(`Failed to update ride status: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to update ride status: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, @@ -296,7 +302,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { ); } setError(error as Error); - showError(`Failed to update ride scheduling: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to update ride scheduling: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, @@ -326,7 +335,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { updateRideInLists(rideId, () => originalRide); } setError(error as Error); - showError(`Failed to assign driver: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to assign driver: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, @@ -347,9 +359,9 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { // We'll just use it for the optimistic update logic } catch (error) { console.error('Failed to fetch ride from server:', error); - const msg = 'Ride not found'; - showError(`${msg}: ${formatErrorMessage(error)}`, 'Rides Error'); - throw new Error(msg); + const msg = 'Ride not found'; + showError(`${msg}: ${formatErrorMessage(error)}`, 'Rides Error'); + throw new Error(msg); } } @@ -413,7 +425,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { ); } setError(error as Error); - showError(`Failed to update ride info: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to update ride info: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, @@ -456,7 +471,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { setUnscheduledRides((prev) => prev.filter((r) => r.id !== tempId)); } setError(error as Error); - showError(`Failed to create ride: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to create ride: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, []); @@ -496,7 +514,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } } setError(error as Error); - showError(`Failed to delete ride: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to delete ride: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, @@ -539,7 +560,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } } setError(error as Error); - showError(`Failed to cancel ride: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to cancel ride: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, @@ -599,7 +623,10 @@ export const RidesProvider = ({ children }: RidesProviderProps) => { } catch (error) { console.error('Failed to get available riders:', error); setError(error as Error); - showError(`Failed to get available riders: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to get available riders: ${formatErrorMessage(error)}`, + 'Rides Error' + ); throw error; } }, diff --git a/frontend/src/context/errorModal.tsx b/frontend/src/context/errorModal.tsx index f5e61c7fa..dd54f9d48 100644 --- a/frontend/src/context/errorModal.tsx +++ b/frontend/src/context/errorModal.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; import Modal from '../components/Modal/Modal'; type ErrorModalState = { @@ -12,9 +18,13 @@ type ErrorModalContextValue = { hideError: () => void; }; -const ErrorModalContext = createContext(undefined); +const ErrorModalContext = createContext( + undefined +); -let externalShowError: ((message: React.ReactNode, title?: string) => void) | null = null; +let externalShowError: + | ((message: React.ReactNode, title?: string) => void) + | null = null; export const showGlobalError = (message: React.ReactNode, title?: string) => { if (externalShowError) { @@ -35,26 +45,39 @@ type ProviderProps = { defaultTitle?: string; }; -export const ErrorModalProvider = ({ children, defaultTitle = 'Something went wrong' }: ProviderProps) => { +export const ErrorModalProvider = ({ + children, + defaultTitle = 'Something went wrong', +}: ProviderProps) => { const [state, setState] = useState({ isOpen: false }); const hideError = useCallback(() => { setState({ isOpen: false }); }, []); - const showError = useCallback((message: React.ReactNode, title?: string) => { - setState({ isOpen: true, title: title || defaultTitle, message }); - }, [defaultTitle]); + const showError = useCallback( + (message: React.ReactNode, title?: string) => { + setState({ isOpen: true, title: title || defaultTitle, message }); + }, + [defaultTitle] + ); // Expose for non-React modules externalShowError = showError; - const value = useMemo(() => ({ showError, hideError }), [showError, hideError]); + const value = useMemo( + () => ({ showError, hideError }), + [showError, hideError] + ); return ( {children} - +
{state.message}
@@ -74,5 +97,3 @@ export const formatErrorMessage = (err: unknown): string => { return 'An unexpected error occurred.'; } }; - - diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index c4ef1f35f..fdb48b036 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -10,7 +10,10 @@ if (container) { root.render(); } else { console.error('Failed to find the root element'); - showGlobalError('Failed to initialize the app. Please refresh the page.', 'Initialization Error'); + showGlobalError( + 'Failed to initialize the app. Please refresh the page.', + 'Initialization Error' + ); } serviceWorker.register(); diff --git a/frontend/src/pages/Driver/Rides.tsx b/frontend/src/pages/Driver/Rides.tsx index 5bce4678d..81ecb6642 100644 --- a/frontend/src/pages/Driver/Rides.tsx +++ b/frontend/src/pages/Driver/Rides.tsx @@ -425,7 +425,10 @@ const Rides = () => { setAllDriverRides(rides); } catch (error) { console.error('Failed to fetch driver rides:', error); - showError(`Failed to fetch driver rides: ${formatErrorMessage(error)}`, 'Rides Error'); + showError( + `Failed to fetch driver rides: ${formatErrorMessage(error)}`, + 'Rides Error' + ); } finally { setLoadingRides(false); } diff --git a/frontend/src/serviceWorker.ts b/frontend/src/serviceWorker.ts index 9bc18eda3..11030be62 100644 --- a/frontend/src/serviceWorker.ts +++ b/frontend/src/serviceWorker.ts @@ -107,7 +107,9 @@ function registerValidSW(swUrl: string, config?: Config) { .catch((error) => { console.error('Error during service worker registration:', error); showGlobalError( - `Failed to register service worker: ${formatErrorMessage(error)}. Some features may not be available offline.`, + `Failed to register service worker: ${formatErrorMessage( + error + )}. Some features may not be available offline.`, 'Service Worker Error' ); }); From 4f09b8381d71d4fd2c5a3db5e78b874b9f42a7f0 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Wed, 8 Apr 2026 21:52:06 -0400 Subject: [PATCH 11/12] fixes --- .../components/AuthManager/AuthManager.tsx | 2 +- .../Locations/LocationFormModal.tsx | 8 ++- .../components/Locations/LocationsContent.tsx | 10 ++-- .../src/components/ResponsiveRideCard.tsx | 57 ++++++++++++++----- .../RideDetails/RideDetailsComponent.tsx | 1 + .../RiderComponents/RequestRideDialog.tsx | 9 ++- frontend/src/context/LocationsContext.tsx | 4 +- 7 files changed, 68 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/AuthManager/AuthManager.tsx b/frontend/src/components/AuthManager/AuthManager.tsx index 2890b383b..e05d9904e 100644 --- a/frontend/src/components/AuthManager/AuthManager.tsx +++ b/frontend/src/components/AuthManager/AuthManager.tsx @@ -261,7 +261,7 @@ const AuthManager = () => { setAuthToken(''); setSignedIn(false); setRefreshUser(() => () => {}); - window.location.href = `${process.env.VITE_SERVER_URL}/api/sso/logout`; + window.location.href = `${import.meta.env.VITE_SERVER_URL}/api/sso/logout`; } function createRefresh(userId: string, userType: string, token: string) { diff --git a/frontend/src/components/Locations/LocationFormModal.tsx b/frontend/src/components/Locations/LocationFormModal.tsx index c1b12fb54..28012f151 100644 --- a/frontend/src/components/Locations/LocationFormModal.tsx +++ b/frontend/src/components/Locations/LocationFormModal.tsx @@ -165,7 +165,13 @@ export const LocationFormModal: React.FC = ({ }; return ( - + {mode === 'add' ? 'Add New Location' : 'Edit Location'} diff --git a/frontend/src/components/Locations/LocationsContent.tsx b/frontend/src/components/Locations/LocationsContent.tsx index 2b3bc0dd4..d24bea20e 100644 --- a/frontend/src/components/Locations/LocationsContent.tsx +++ b/frontend/src/components/Locations/LocationsContent.tsx @@ -28,10 +28,12 @@ const LocationsContent: React.FC = ({ setFilteredLocations(locations); }, [locations]); - const uniqueTags = useMemo( - () => Array.from(new Set(locations.map((location) => location.tag))), - [locations] - ); + const uniqueTags = useMemo(() => { + const tags = locations + .map((location) => location.tag) + .filter((tag): tag is string => typeof tag === 'string' && tag.length > 0); + return Array.from(new Set(tags)); + }, [locations]); const handleFilterApply = (filteredItems: LocationType[]) => { setFilteredLocations(filteredItems); diff --git a/frontend/src/components/ResponsiveRideCard.tsx b/frontend/src/components/ResponsiveRideCard.tsx index 1bfacb87f..f3ce5ca8b 100644 --- a/frontend/src/components/ResponsiveRideCard.tsx +++ b/frontend/src/components/ResponsiveRideCard.tsx @@ -1,6 +1,7 @@ import React, { FC, ReactNode, useState } from 'react'; import { SchedulingState, Status, Tag } from '../types'; import { RideType } from '@carriage-web/shared/types/ride'; +import { LocationType } from '@carriage-web/shared/types/location'; import { BadgeRounded, FlagRounded, @@ -51,25 +52,45 @@ const renderFormattedTime = (time: Date): ReactNode => { ); }; +/** Both endpoints must be finite numbers so Map defaultCenter and markers never receive NaN. */ +const hasValidMapCoords = (loc: LocationType): boolean => { + const lat = Number(loc.lat); + const lng = Number(loc.lng); + return Number.isFinite(lat) && Number.isFinite(lng); +}; + const ResponsiveRideCard: FC = ({ ride, handleEdit, }) => { const [expanded, setExpanded] = useState(false); - // Check if either location is a custom location (with no valid coordinates) + // Custom tag / placeholder zeros, or missing non-finite coords — skip map to avoid Google Maps errors const hasCustomLocation = () => { const isPickupCustom = ride.startLocation.tag === Tag.CUSTOM || - ride.startLocation.lat === 0 || - ride.startLocation.lng === 0; + Number(ride.startLocation.lat) === 0 || + Number(ride.startLocation.lng) === 0; const isDropoffCustom = ride.endLocation.tag === Tag.CUSTOM || - ride.endLocation.lat === 0 || - ride.endLocation.lng === 0; + Number(ride.endLocation.lat) === 0 || + Number(ride.endLocation.lng) === 0; return isPickupCustom || isDropoffCustom; }; + const canShowRouteMap = + hasValidMapCoords(ride.startLocation) && + hasValidMapCoords(ride.endLocation) && + !hasCustomLocation(); + + const coordsInvalidForMap = + !hasValidMapCoords(ride.startLocation) || + !hasValidMapCoords(ride.endLocation); + + const mapPlaceholderSubtitle = coordsInvalidForMap + ? 'Map not available — location coordinates are missing or invalid.' + : 'Map not available for custom locations'; + return (
@@ -162,7 +183,7 @@ const ResponsiveRideCard: FC = ({ {/* expanded location view */} {expanded && (
- {hasCustomLocation() ? ( + {!canShowRouteMap ? (
= ({ marginBottom: '8px', }} > - 📍 Custom Location + 📍 {coordsInvalidForMap ? 'Map unavailable' : 'Custom Location'}

- Map not available for custom locations + {mapPlaceholderSubtitle}

@@ -196,18 +217,24 @@ const ResponsiveRideCard: FC = ({ = ({ = ({ onClose={onClose} maxWidth="md" fullScreen={isMobile} + disableEnforceFocus PaperProps={{ className: isMobile ? styles.modalMobile : styles.modal, }} diff --git a/frontend/src/components/RiderComponents/RequestRideDialog.tsx b/frontend/src/components/RiderComponents/RequestRideDialog.tsx index fd8ece84d..87a773629 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -630,7 +630,14 @@ const RequestRideDialog: React.FC = ({ : null; return ( - + {!ride ? 'Request a Ride' : 'Edit Ride'} { const filtered = locationsData.filter((loc) => loc.tag !== Tag.CUSTOM); filtered.sort((a: LocationType, b: LocationType) => { - return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + const nameA = (a.name ?? '').toLowerCase(); + const nameB = (b.name ?? '').toLowerCase(); + return nameA.localeCompare(nameB); }); componentMounted.current && setLocations(filtered); From 7c66c222eb587db83ed03a21a7ce5b7abecd3610 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Wed, 8 Apr 2026 21:52:22 -0400 Subject: [PATCH 12/12] lint --- frontend/src/components/Locations/LocationsContent.tsx | 4 +++- frontend/src/components/ResponsiveRideCard.tsx | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Locations/LocationsContent.tsx b/frontend/src/components/Locations/LocationsContent.tsx index d24bea20e..2ecff48cc 100644 --- a/frontend/src/components/Locations/LocationsContent.tsx +++ b/frontend/src/components/Locations/LocationsContent.tsx @@ -31,7 +31,9 @@ const LocationsContent: React.FC = ({ const uniqueTags = useMemo(() => { const tags = locations .map((location) => location.tag) - .filter((tag): tag is string => typeof tag === 'string' && tag.length > 0); + .filter( + (tag): tag is string => typeof tag === 'string' && tag.length > 0 + ); return Array.from(new Set(tags)); }, [locations]); diff --git a/frontend/src/components/ResponsiveRideCard.tsx b/frontend/src/components/ResponsiveRideCard.tsx index f3ce5ca8b..292f006cf 100644 --- a/frontend/src/components/ResponsiveRideCard.tsx +++ b/frontend/src/components/ResponsiveRideCard.tsx @@ -206,7 +206,10 @@ const ResponsiveRideCard: FC = ({ marginBottom: '8px', }} > - 📍 {coordsInvalidForMap ? 'Map unavailable' : 'Custom Location'} + 📍{' '} + {coordsInvalidForMap + ? 'Map unavailable' + : 'Custom Location'}

{mapPlaceholderSubtitle}