From 4988761d938a6441caecdf9b7391f841c536ff77 Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Sun, 2 Nov 2025 18:05:03 -0500 Subject: [PATCH 1/2] Fix time validation, standardize request modals --- .../components/RideDetails/RideActions.tsx | 49 ++++ .../RideDetails/RideEditContext.tsx | 2 +- .../components/RideDetails/RideOverview.tsx | 245 ++++++------------ .../components/RideDetails/TimeValidation.tsx | 22 ++ .../components/RiderComponents/MainCard.tsx | 26 +- 5 files changed, 154 insertions(+), 190 deletions(-) diff --git a/frontend/src/components/RideDetails/RideActions.tsx b/frontend/src/components/RideDetails/RideActions.tsx index 8de9d13ce..936286084 100644 --- a/frontend/src/components/RideDetails/RideActions.tsx +++ b/frontend/src/components/RideDetails/RideActions.tsx @@ -30,6 +30,7 @@ import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import CloseIcon from '@mui/icons-material/Close'; import { RideType, Status, SchedulingState } from '../../types'; import { useRideEdit } from './RideEditContext'; +import { validateRideTimes, TimeValidationError } from './TimeValidation'; import { canUpdateStatus, canCancelRide, @@ -92,6 +93,8 @@ const RideActions: React.FC = ({ const [selectedStatus, setSelectedStatus] = useState(null); const [updating, setUpdating] = useState(false); const [saving, setSaving] = useState(false); + const [timeErrorOpen, setTimeErrorOpen] = useState(false); + const [timeErrors, setTimeErrors] = useState([]); const ride = editedRide!; // We know this exists from the context const rideCompleted = ride.status === Status.COMPLETED; @@ -159,6 +162,24 @@ const RideActions: React.FC = ({ }; const handleSave = async () => { + // Validate times before saving; show modal with errors if invalid + const allowPastTimes = false; + const timeValidation = validateRideTimes( + ride.startTime, + ride.endTime, + { + allowPastTimes, + maxDurationHours: 24, + minDurationMinutes: 5, + } + ); + + if (!timeValidation.isValid) { + setTimeErrors(timeValidation.errors); + setTimeErrorOpen(true); + return; + } + setSaving(true); try { const success = await saveChanges(); @@ -527,6 +548,34 @@ const RideActions: React.FC = ({ + {/* Time Validation Errors Modal */} + setTimeErrorOpen(false)} + fullWidth + maxWidth="sm" + > + Cannot Save – Please Fix Time Settings + + + The following issues were found: + + + {timeErrors.map((err, idx) => ( + + + + + + + ))} + + + + + + + {/* Contact Admin Modal */} = ({ editedRide.startTime, editedRide.endTime, { - allowPastTimes: !isNewRide(editedRide), + allowPastTimes: false, maxDurationHours: 24, minDurationMinutes: 5, } diff --git a/frontend/src/components/RideDetails/RideOverview.tsx b/frontend/src/components/RideDetails/RideOverview.tsx index 7dbfb55bd..583ea0c9f 100644 --- a/frontend/src/components/RideDetails/RideOverview.tsx +++ b/frontend/src/components/RideDetails/RideOverview.tsx @@ -37,7 +37,6 @@ 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'; interface RideOverviewProps { @@ -208,6 +207,9 @@ const RideOverview: React.FC = ({ userRole }) => { const formatDateTime = (dateTimeString: string) => { const date = new Date(dateTimeString); + if (isNaN(date.getTime())) { + return { date: '', time: '' }; + } return { date: date.toLocaleDateString('en-US', { weekday: 'long', @@ -228,34 +230,25 @@ const RideOverview: React.FC = ({ userRole }) => { // Helper functions for date/time editing const handleStartDateChange = (newDate: Dayjs | null) => { - if (!newDate) return; + if (!newDate || !newDate.isValid()) return; const currentStartTime = dayjs(ride.startTime); + const startHour = currentStartTime.isValid() ? currentStartTime.hour() : 0; + const startMinute = currentStartTime.isValid() ? currentStartTime.minute() : 0; const updatedStartTime = newDate - .hour(currentStartTime.hour()) - .minute(currentStartTime.minute()) + .hour(startHour) + .minute(startMinute) .second(0) .millisecond(0); - - 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()); - } - updateRideField('startTime', updatedStartTime.toISOString()); }; const handleStartTimeChange = (newTime: Dayjs | null) => { - if (!newTime) return; + if (!newTime || !newTime.isValid()) return; const currentStartDate = dayjs(ride.startTime); - const updatedStartTime = currentStartDate + const base = currentStartDate.isValid() ? currentStartDate : dayjs(); + const updatedStartTime = base .hour(newTime.hour()) .minute(newTime.minute()) .second(0) @@ -276,12 +269,14 @@ const RideOverview: React.FC = ({ userRole }) => { }; const handleEndDateChange = (newDate: Dayjs | null) => { - if (!newDate) return; + if (!newDate || !newDate.isValid()) return; const currentEndTime = dayjs(ride.endTime); + const endHour = currentEndTime.isValid() ? currentEndTime.hour() : 0; + const endMinute = currentEndTime.isValid() ? currentEndTime.minute() : 0; const updatedEndTime = newDate - .hour(currentEndTime.hour()) - .minute(currentEndTime.minute()) + .hour(endHour) + .minute(endMinute) .second(0) .millisecond(0); @@ -289,10 +284,11 @@ const RideOverview: React.FC = ({ userRole }) => { }; const handleEndTimeChange = (newTime: Dayjs | null) => { - if (!newTime) return; + if (!newTime || !newTime.isValid()) return; const currentEndDate = dayjs(ride.endTime); - const updatedEndTime = currentEndDate + const base = currentEndDate.isValid() ? currentEndDate : dayjs(); + const updatedEndTime = base .hour(newTime.hour()) .minute(newTime.minute()) .second(0) @@ -384,150 +380,65 @@ const RideOverview: React.FC = ({ userRole }) => { {isEditing ? (
- {(() => { - // 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; - - return ( - <> -
-
- -
-
- - {/* Start time specific error - directly below start time field */} - {startTimePastError && ( - - {startTimePastError.message} - - )} -
-
- -
-
- -
-
- - {/* End time specific errors - directly below end time field */} - {(endTimeBeforeStartError || durationError) && ( - - { - (endTimeBeforeStartError || durationError) - ?.message - } - - )} -
-
- - ); - })()} + <> +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
) : ( diff --git a/frontend/src/components/RideDetails/TimeValidation.tsx b/frontend/src/components/RideDetails/TimeValidation.tsx index e81925f87..1e360c5e1 100644 --- a/frontend/src/components/RideDetails/TimeValidation.tsx +++ b/frontend/src/components/RideDetails/TimeValidation.tsx @@ -36,6 +36,28 @@ export const validateRideTimes = ( const end = dayjs(endTime); const now = dayjs(); + // Validate that parsed dates are valid + if (!start.isValid()) { + errors.push({ + type: 'invalid_time', + message: 'Start date/time is invalid', + }); + } + + if (!end.isValid()) { + errors.push({ + type: 'invalid_time', + message: 'End date/time is invalid', + }); + } + + if (errors.length > 0) { + return { + isValid: false, + errors, + }; + } + // Check if start time is in the past if (!allowPastTimes && start.isBefore(now, 'minute')) { errors.push({ diff --git a/frontend/src/components/RiderComponents/MainCard.tsx b/frontend/src/components/RiderComponents/MainCard.tsx index c52c31676..ccbb68b34 100644 --- a/frontend/src/components/RiderComponents/MainCard.tsx +++ b/frontend/src/components/RiderComponents/MainCard.tsx @@ -19,8 +19,7 @@ import CallIcon from '@mui/icons-material/Call'; import CancelIcon from '@mui/icons-material/Cancel'; import styles from './maincard.module.css'; import DeleteOrEditTypeModal from 'components/Modal/DeleteOrEditTypeModal'; -import RequestRideDialog from './RequestRideDialog'; -import { useLocations } from 'context/LocationsContext'; +import RideDetailsComponent from 'components/RideDetails/RideDetailsComponent'; import DriverInfoDialog from './DriverInfoDialog'; import { Dialog, @@ -41,12 +40,11 @@ const MainCard: React.FC = ({ ride }) => { const [cancelOpen, setCancelOpen] = useState(false); const [openDeleteOrEditModal, setOpenDeleteOrEditModal] = useState(false); const [editOpen, setEditOpen] = useState(false); - const [openEditModal, setOpenEditModal] = useState(false); + // Removed RequestRideDialog flow; using RideDetailsComponent instead const [contactOpen, setContactOpen] = useState(false); const [openDriverInfoDialog, setOpenDriverInfoDialog] = useState(false); const [adminContactOpen, setAdminContactOpen] = useState(false); const [imageError, setImageError] = useState(false); - const { locations } = useLocations(); const formatDateTime = (dateString: string) => { const date = new Date(dateString); return { @@ -139,29 +137,13 @@ const MainCard: React.FC = ({ ride }) => { > - { setEditOpen(!editOpen); }} - onSubmit={() => { - setOpenEditModal(!openEditModal); - }} ride={ride} - supportedLocations={locations - .map((l) => ({ - id: String(l.id), - name: l.name, - address: l.address, - shortName: l.shortName, - info: l.info ?? '', - tag: (l.tag as any) ?? '', - lat: Number(l.lat), - lng: Number(l.lng), - photoLink: l.photoLink, - images: l.images, - })) - .filter((l) => Number.isFinite(l.lat) && Number.isFinite(l.lng))} + initialEditingState={true} /> {ride.driver && ( <> From 09217727d24040db7b17cb9057f79f97539ac31c Mon Sep 17 00:00:00 2001 From: Matthew Kim Date: Sun, 2 Nov 2025 18:16:18 -0500 Subject: [PATCH 2/2] Update MainCard.tsx --- frontend/src/components/RiderComponents/MainCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/RiderComponents/MainCard.tsx b/frontend/src/components/RiderComponents/MainCard.tsx index ccbb68b34..dd944af4c 100644 --- a/frontend/src/components/RiderComponents/MainCard.tsx +++ b/frontend/src/components/RiderComponents/MainCard.tsx @@ -40,7 +40,6 @@ const MainCard: React.FC = ({ ride }) => { const [cancelOpen, setCancelOpen] = useState(false); const [openDeleteOrEditModal, setOpenDeleteOrEditModal] = useState(false); const [editOpen, setEditOpen] = useState(false); - // Removed RequestRideDialog flow; using RideDetailsComponent instead const [contactOpen, setContactOpen] = useState(false); const [openDriverInfoDialog, setOpenDriverInfoDialog] = useState(false); const [adminContactOpen, setAdminContactOpen] = useState(false);