Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -18,9 +19,11 @@ const App = () => {

return (
<Router>
<ToastProvider>
<AuthManager />
</ToastProvider>
<ErrorModalProvider>
<ToastProvider>
<AuthManager />
</ToastProvider>
</ErrorModalProvider>
</Router>
);
};
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/AuthManager/AuthManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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!}`;

Expand Down Expand Up @@ -67,6 +68,7 @@ const AuthManager = () => {
setUnregisteredUser(null);
logout();
};
const { showError } = useErrorModal();

useEffect(() => {
const token = jwtValue();
Expand Down Expand Up @@ -209,6 +211,7 @@ const AuthManager = () => {
}
} catch (error) {
console.error('Error decrypting JWT:', error);
showError(`Error decrypting JWT: ${formatErrorMessage(error)}`, 'Authentication Error');
}
return '';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/EmployeeCards/EmployeeCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
};
Expand Down
104 changes: 75 additions & 29 deletions frontend/src/components/EmployeeModal/EmployeeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -76,6 +77,7 @@ const EmployeeModal = ({
setIsOpen,
}: EmployeeModalProps) => {
const { showToast } = useToast();
const { showError } = useErrorModal();
const {
updateAdminInfo,
updateDriverInfo,
Expand Down Expand Up @@ -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.');
}
}
Expand All @@ -182,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:
Expand Down Expand Up @@ -233,9 +238,13 @@ const EmployeeModal = ({
async function deleteEmployee(id: string, endpoint: string): Promise<void> {
// 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');
});
}
}

Expand All @@ -262,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;
Expand Down Expand Up @@ -364,11 +405,13 @@ 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) {
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 {
Expand All @@ -377,9 +420,12 @@ 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) {
showToast('An error occurred: ', ToastStatus.ERROR);
showError(
`An error occurred while saving employee: ${formatErrorMessage(error)}`,
'Employees Error'
);
} finally {
closeModal();
}
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/ExportButton/ExportButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +23,7 @@ const ExportButton = ({
}: clickHandler) => {
const [downloadData, setDownloadData] = useState<string>('');
const { showToast } = useToast();
const { showError } = useErrorModal();
const csvLink = useRef<
CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement }
>(null);
Expand All @@ -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 (
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/components/Locations/LocationFormModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ 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';
import { useToast, ToastStatus } from '../../context/toastContext';

const CAMPUS_OPTIONS = [
{ value: Tag.NORTH, label: 'North Campus' },
Expand Down Expand Up @@ -67,7 +69,9 @@ export const LocationFormModal: React.FC<Props> = ({
const [loadingAddr, setLoadingAddr] = useState(false);
const [error, setError] = useState<string | null>(null);
const [locationImages, setLocationImages] = useState<LocationImage[]>([]);

const { showError } = useErrorModal();
const { showToast } = useToast();

useEffect(() => {
if (!open) return;

Expand Down Expand Up @@ -98,6 +102,10 @@ export const LocationFormModal: React.FC<Props> = ({
} 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);
}
Expand All @@ -114,6 +122,10 @@ export const LocationFormModal: React.FC<Props> = ({
} 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);
}
Expand Down Expand Up @@ -147,6 +159,7 @@ export const LocationFormModal: React.FC<Props> = ({
imagesList: locationImages,
};
onSubmit(updatedLocation);
showToast('Location saved successfully', ToastStatus.SUCCESS);
onClose();
};

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/Locations/PlacesSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand Down Expand Up @@ -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]
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/Modal/RiderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand Down Expand Up @@ -55,6 +56,8 @@ const RiderModal = ({
if (isRiderWeb) {
refreshUser();
}
}).catch((error) => {
showError(`Failed to save student: ${formatErrorMessage(error)}`, 'Students Error');
});
setIsSubmitted(false);
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Modal/modal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
left: 0;
height: 100%;
width: 100%;
z-index: 1000;
z-index: 10000;
}

.modal {
Expand All @@ -17,7 +17,7 @@
background-color: white;
padding: 2rem 2.25rem;
border-radius: 1rem;
z-index: 1010;
z-index: 10010;
}

.title {
Expand Down
Loading
Loading