Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

148 changes: 148 additions & 0 deletions frontend/src/components/Modal/CancelRideConfirmationDialog.tsx
Comment thread
selenaliu1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
} from '@mui/material';
import { RideType, SchedulingState } from '../../types';
import { UserRole } from '../../util/rideValidation';
import axios from '../../util/axios';
import { useToast, ToastStatus } from '../../context/toastContext';
import { useRides } from '../../context/RidesContext';

interface CancelRideConfirmationDialogProps {
open: boolean;
onClose: () => void;
ride: RideType;
userRole?: UserRole;
onSuccess?: () => void;
}

const CancelRideConfirmationDialog: React.FC<
CancelRideConfirmationDialogProps
> = ({ open, onClose, ride, userRole: propUserRole, onSuccess }) => {
const { showToast } = useToast();
const { refreshRides } = useRides();

// Get user role from localStorage if not provided as prop
const getUserRole = (): UserRole => {
if (propUserRole) return propUserRole;
const userType = localStorage.getItem('userType');
if (userType === 'Admin') return 'admin';
if (userType === 'Driver') return 'driver';
if (userType === 'Rider') return 'rider';
return 'rider';
};

const userRole = getUserRole();

const handleCancelConfirm = async () => {
// Check for recurring rides (not supported yet)
if (ride.isRecurring) {
showToast('Recurring ride deletion not supported yet', ToastStatus.ERROR);
return;
}

// Only admins can reject rides; riders can only cancel
// If ride has no driver (unscheduled) AND user is admin, reject it
if (!ride.driver && userRole === 'admin') {
try {
// Set schedulingState to REJECTED
await axios.put(`/api/rides/${ride.id}`, {
schedulingState: SchedulingState.REJECTED,
});

// Close the cancel confirmation modal
onClose();

// Refresh the rides data
refreshRides();

// Show success message
showToast('Ride Rejected', ToastStatus.SUCCESS);
onSuccess?.();
} catch (error) {
console.error('Failed to reject ride:', error);
showToast('Failed to reject ride', ToastStatus.ERROR);
}
return;
}

// All other cases: call DELETE endpoint
// Backend will handle:
// - If unscheduled ride + user is rider: physically delete the ride
// - If scheduled ride (has driver) + user is admin: set status to CANCELLED
try {
await axios.delete(`/api/rides/${ride.id}`);

// Close the cancel confirmation modal
onClose();

// Refresh the rides data
refreshRides();

// Show success message
showToast('Ride Cancelled', ToastStatus.SUCCESS);
onSuccess?.();
} catch (error) {
console.error('Failed to cancel ride:', error);
showToast('Failed to cancel ride', ToastStatus.ERROR);
}
};

return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="xs">
<DialogTitle>
{!ride.driver && userRole === 'admin' ? 'Reject Ride' : 'Cancel Ride'}
</DialogTitle>
<DialogContent>
<Typography>
{!ride.driver && userRole === 'admin'
? 'Are you sure you want to reject this ride? This will mark the ride as rejected and notify the rider.'
: 'Are you sure you want to cancel this ride? This action cannot be undone.'}
</Typography>
<Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
<Typography variant="body2" color="textSecondary">
Ride Summary
</Typography>
<Typography variant="body2">
{new Date(ride.startTime).toLocaleDateString()} at{' '}
{new Date(ride.startTime).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</Typography>
{ride.riders && ride.riders.length > 0 && (
<Typography variant="body2">
Rider:{' '}
{ride.riders
.map((rider) => rider.firstName + ' ' + rider.lastName)
.join(', ')}
</Typography>
)}
{ride.driver && (
<Typography variant="body2">
Driver: {ride.driver?.firstName + ' ' + ride.driver?.lastName}
</Typography>
)}
<Typography variant="body2">
From: {ride.startLocation.name}
</Typography>
<Typography variant="body2">To: {ride.endLocation.name}</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Keep Ride</Button>
<Button onClick={handleCancelConfirm} variant="contained" color="error">
{!ride.driver && userRole === 'admin' ? 'Reject Ride' : 'Cancel Ride'}
</Button>
</DialogActions>
</Dialog>
);
};

export default CancelRideConfirmationDialog;
3 changes: 3 additions & 0 deletions frontend/src/components/RideDetails/RideEditContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ export const RideEditProvider: React.FC<RideEditProviderProps> = ({
];

for (const field of fieldsToCheck) {
// Skip driver field - handle it separately below
if (field === 'driver') continue;

if (
JSON.stringify(editedRide[field]) !==
JSON.stringify(originalRide[field])
Expand Down
135 changes: 100 additions & 35 deletions frontend/src/components/RideDetails/RideTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { RideType, Status, SchedulingState } from '../../types';
import AuthContext from '../../context/auth';
import RideDetailsComponent from './RideDetailsComponent';
import { getRiderDisplayState } from '../../util/rideValidation';

interface RideTableProps {
rides: RideType[];
Expand Down Expand Up @@ -251,7 +252,6 @@ const RideTable: React.FC<RideTableProps> = ({
{ key: 'endTime', label: 'End Time', sortable: true },
{ key: 'from', label: 'From', sortable: false },
{ key: 'to', label: 'To', sortable: false },
{ key: 'status', label: 'Status', sortable: true },
];

switch (userRole) {
Expand All @@ -268,11 +268,13 @@ const RideTable: React.FC<RideTableProps> = ({
case 'driver':
return [
...baseColumns,
{ key: 'status', label: 'Status', sortable: true },
{ key: 'type', label: 'Type', sortable: false },
];
case 'admin':
return [
...baseColumns,
{ key: 'status', label: 'Status', sortable: true },
{
key: 'schedulingState',
label: 'Scheduling State',
Expand All @@ -291,10 +293,12 @@ const RideTable: React.FC<RideTableProps> = ({
const filteredRides = useMemo(() => {
let filtered = rides;

// For drivers, only show scheduled rides
// For drivers, only show scheduled rides (including modified ones)
if (userRole === 'driver') {
filtered = filtered.filter(
(ride) => ride.schedulingState === SchedulingState.SCHEDULED
(ride) =>
ride.schedulingState === SchedulingState.SCHEDULED ||
ride.schedulingState === SchedulingState.SCHEDULED_WITH_MODIFICATION
);
}

Expand Down Expand Up @@ -634,38 +638,99 @@ const RideTable: React.FC<RideTableProps> = ({
}}
aria-label="Open ride details"
>
<TableCell>{formatDate(ride.startTime)}</TableCell>
<TableCell>{formatTime(ride.startTime)}</TableCell>
<TableCell>{formatTime(ride.endTime)}</TableCell>
<TableCell>{ride.startLocation.name}</TableCell>
<TableCell>{ride.endLocation.name}</TableCell>
<TableCell>
<Chip
label={ride.status.replace(/_/g, ' ')}
color={getStatusColor(ride.status)}
size="small"
/>
</TableCell>
{userRole !== 'driver' && (
<TableCell>
<Chip
label={ride.schedulingState}
color={getSchedulingStateColor(
ride.schedulingState
)}
size="small"
variant="outlined"
/>
</TableCell>
)}
<TableCell>
<Chip
label={temporalType}
color={getTemporalTypeColor(temporalType)}
size="small"
variant="outlined"
/>
</TableCell>
{columns.map((column) => {
switch (column.key) {
case 'date':
return (
<TableCell key={column.key}>
{formatDate(ride.startTime)}
</TableCell>
);
case 'startTime':
return (
<TableCell key={column.key}>
{formatTime(ride.startTime)}
</TableCell>
);
case 'endTime':
return (
<TableCell key={column.key}>
{formatTime(ride.endTime)}
</TableCell>
);
case 'from':
return (
<TableCell key={column.key}>
{ride.startLocation.name}
</TableCell>
);
case 'to':
return (
<TableCell key={column.key}>
{ride.endLocation.name}
</TableCell>
);
case 'status':
return (
<TableCell key={column.key}>
<Chip
label={ride.status.replace(/_/g, ' ')}
color={getStatusColor(ride.status)}
size="small"
/>
</TableCell>
);
case 'schedulingState':
return (
<TableCell key={column.key}>
<Chip
label={
userRole === 'rider'
? getRiderDisplayState(ride)
: ride.schedulingState
}
color={
userRole === 'rider'
? getRiderDisplayState(ride) ===
'Cancelled' ||
getRiderDisplayState(ride) ===
'Rejected'
? 'error'
: getRiderDisplayState(ride) ===
'Scheduled' ||
getRiderDisplayState(ride) ===
'Scheduled (Modified)'
? 'success'
: getRiderDisplayState(ride) ===
'Requested'
? 'warning'
: 'default'
: getSchedulingStateColor(
ride.schedulingState
)
}
size="small"
variant={
userRole === 'rider' ? 'filled' : 'outlined'
}
/>
</TableCell>
);
case 'type':
return (
<TableCell key={column.key}>
<Chip
label={temporalType}
color={getTemporalTypeColor(temporalType)}
size="small"
variant="outlined"
/>
</TableCell>
);
default:
return null;
}
})}
</TableRow>
);
})}
Expand Down
Loading
Loading