diff --git a/frontend/src/components/EmployeeCards/EmployeeCards.tsx b/frontend/src/components/EmployeeCards/EmployeeCards.tsx index dde780853..b836d7a08 100644 --- a/frontend/src/components/EmployeeCards/EmployeeCards.tsx +++ b/frontend/src/components/EmployeeCards/EmployeeCards.tsx @@ -34,7 +34,6 @@ const EmployeeCard = ({ const navigate = useNavigate(); const netId = email.split('@')[0]; const fmtPhone = formatPhone(phoneNumber); - const formatAvail = (availability: { [key: string]: { startTime: string; endTime: string }; }) => { @@ -59,17 +58,6 @@ const EmployeeCard = ({ return 'Driver'; }; - const userInfo = { - id, - firstName, - lastName, - netId, - type, - phone: fmtPhone, - photoLink, - startDate, - }; - const handleClick = () => { const path = isAdmin || isBoth ? `/admin/admins/${id}` : `/admin/drivers/${id}`; diff --git a/frontend/src/components/EmployeeModal/EmployeeModal.tsx b/frontend/src/components/EmployeeModal/EmployeeModal.tsx index 8e965fbbe..7ffda6e24 100644 --- a/frontend/src/components/EmployeeModal/EmployeeModal.tsx +++ b/frontend/src/components/EmployeeModal/EmployeeModal.tsx @@ -72,9 +72,6 @@ const EmployeeModal = ({ const { refreshAdmins, refreshDrivers } = useEmployees(); const methods = useForm(); - const modalTitle = existingEmployee ? 'Edit Profile' : 'Add an Employee'; - const submitButtonText = existingEmployee ? 'Save' : 'Add'; - const closeModal = () => { methods.clearErrors(); setIsOpen(false); @@ -90,127 +87,258 @@ const EmployeeModal = ({ * the start and end time of each availibility period */ const parseAvailability = (availability: ObjectType[]) => { - const result: ObjectType = {}; - availability.forEach(({ startTime, endTime, days }) => { - days.forEach((day: string) => { - result[day] = { startTime, endTime }; + if (availability === null || availability === undefined) { + console.error('Null ptr: Availablity'); + return []; // placeholder + } else { + const result: ObjectType = {}; + availability.forEach(({ startTime, endTime, days }) => { + days.forEach((day: string) => { + result[day] = { startTime, endTime }; + }); }); - }); - return result; + return result; + } }; - const uploadPhotoForEmployee = async ( + async function uploadEmployeePhoto( employeeId: string, table: string, refresh: () => Promise, - isCreate: boolean // show toast if new employee is created - ) => { - const photo = { + imageBase64: string + ): Promise { + const photoData = { id: employeeId, tableName: table, fileBuffer: imageBase64, }; - // Upload image - await axios - .post('/api/upload', photo) - .then(() => { - refresh(); - }) - .catch((err) => console.log(err)); - }; + try { + await axios.post('/api/upload/', photoData); + } catch (error) { + console.error('Error uploading photo:', error); + } + refresh(); + } - const createNewEmployee = async ( + /** + * Creates a new employee using the provided data and endpoint. Optionally uploads a photo if + * [iteration] is 0 and [imageBase64] is non-empty. + * + * @param id - The unique identifier of the employee to create, or an empty string if not provided. + * @param employeeData - The data to create the new employee with. This should be a valid object. + * @param endpoint - The API endpoint to which the employee data will be sent. + * @param refresh - A function that refreshes the employee data or table after the creation. + * @param iteration - A non-negative integer used to conditionally upload a photo. + * @param imageBase64 - A base64 encoded string representing the employee's photo (optional). + * + * @returns A promise that resolves to the created employee data. + * + * @throws {Error} Throws an error if any of the parameters are invalid or if the creation fails. + */ + async function createEmployee( + id: string, employeeData: AdminData | DriverData, endpoint: string, refresh: () => Promise, - table: string - ) => { - const res = await axios.post(endpoint, employeeData); - if (imageBase64 === '') { - // If no image has been uploaded, create new employee - refresh(); - showToast('The employee has been added.', ToastStatus.SUCCESS); - } else { - const { data: createdEmployee } = await res.data; - uploadPhotoForEmployee(createdEmployee.id, table, refresh, true); + table: string, + iteration: number + ): Promise { + if (Boolean(id) && id !== '') { + (employeeData as any).id = id; } - return res; - }; + const { + data: { data: createdEmployee }, + } = await axios.post(endpoint, employeeData); + if (iteration === 0 && imageBase64 !== '') { + await uploadEmployeePhoto( + createdEmployee.id, + table, + refresh, + imageBase64 + ); + } + await refresh(); + return createdEmployee; + } - const updateExistingEmployee = async ( + /** + * Updates an existing employee's data using the specified endpoint. Optionally uploads a photo + * if [iteration] is 0 and [imageBase64] is non-empty. + * + * @param id - The unique identifier of the employee to update. + * @param employeeData - The data to update the employee with. This should be a valid object. + * @param endpoint - The API endpoint to which the employee data will be sent. + * @param refresh - A function that refreshes the employee data or table after the update. + * @param iteration - A non-negative integer used to conditionally upload a photo. + * @param imageBase64 - A base64 encoded string representing the employee's photo (optional). + * + * @returns A promise that resolves after successfully updating the employee and refreshing + * the data. + * + * @throws {Error} Throws an error if any of the parameters are invalid or if the update fails. + */ + + async function updateEmployee( + id: string, employeeData: AdminData | DriverData, endpoint: string, refresh: () => Promise, - table: string - ) => { - const updatedEmployee = await axios - .put(`${endpoint}/${existingEmployee!.id}`, employeeData) - .then((res) => { - refresh(); - showToast('The employee has been edited.', ToastStatus.SUCCESS); - return res.data; - }); - if (imageBase64 !== '') { - uploadPhotoForEmployee(updatedEmployee.id, table, refresh, false); + table: string, + iteration: number + ): Promise { + await axios.put(`${endpoint}/${id}`, employeeData); + // iteration count prevents a second write to S3 + if (iteration === 0 && imageBase64 !== '') { + uploadEmployeePhoto(id, table, refresh, imageBase64); } - return updatedEmployee; - }; + refresh(); + } - const createOrUpdateDriver = async ( - driver: AdminData | DriverData, - isNewDriver = false - ) => { - if (isNewDriver) { - return await createNewEmployee( - driver, - '/api/drivers', - () => refreshDrivers(), - 'Drivers' - ); - } else { - return await updateExistingEmployee( - driver, - '/api/drivers', - () => refreshDrivers(), - 'Drivers' - ); - } - }; + /** + * [deleteEmployee id emptype] removes an employee with the specified [id] from the backend, + * using the employee type [emptype] ('drivers' or 'admins') to determine the endpoint. + * + * @param id - The unique identifier of the employee to delete. + * @param emptype - The type of employee, either 'drivers' or 'admins'. + * + * @returns A promise that resolves after successfully deleting the employee. + * + * @throws {Error} Throws an error if the id is not a valid string or the emptype is not 'drivers' or 'admins'. + */ + async function deleteEmployee(id: string, emptype: 'drivers' | 'admins') { + await axios.delete(`/api/${emptype}/${id}`); + } - const createOrUpdateAdmin = async (admin: AdminData, isNewAdmin = false) => { - if (isNewAdmin) { - await createNewEmployee( - admin, - '/api/admins', - () => refreshAdmins(), - 'Admins' - ); - } else { - await updateExistingEmployee( - admin, - '/api/admins', - () => refreshAdmins(), - 'Admins' - ); - } - }; + /** + * Processes and assigns roles ('driver', 'admin') for an employee, handling creation, + * updating, or deletion of their information as needed. + * + * @param selectedRole - Valid array of roles to assign. + * @param existingEmployee - Existing employee data object or `null` (if new). + * @param admin - Valid employee data object for the 'admin' role. + * @param driver - Valid employee data object for the 'driver' role. + * @returns A promise that resolves when all role processing is complete. + * @throws {Error} If input parameters fail validation (invalid types or structure). + * + */ + async function processRoles( + selectedRole: any, + existingEmployee: any, + admin: any, + driver: any + ) { + const containsDriver = selectedRole.includes('driver'); + const containsAdmin = + selectedRole.includes('sds-admin') || + selectedRole.includes('redrunner-admin'); - const deleteDriver = async (id: string | undefined) => { - await axios.delete(`/api/drivers/${id}`); - }; + const rolesToProcess = []; + if (containsAdmin) rolesToProcess.push('admins'); + if (containsDriver) rolesToProcess.push('drivers'); - const deleteAdmin = async (id: string | undefined) => { - await axios.delete(`/api/admins/${id}`); - }; + let newEmployee = null; // To track new employee creation + let iteration = 0; + + for (const role of rolesToProcess) { + const apiEndpoint = role === 'admins' ? '/api/admins' : '/api/drivers'; + const refreshFunction = + role === 'admins' ? refreshAdmins : refreshDrivers; + const entityType = role === 'admins' ? 'Admins' : 'Drivers'; + + if (Boolean(existingEmployee)) { + switch (role) { + case 'admins': + if (existingEmployee.isDriver && !containsDriver) { + // Transition from driver to admin + await deleteEmployee( + newEmployee?.id || existingEmployee.id, + 'drivers' + ); + } + + if (!existingEmployee.isAdmin) { + // Create admin + await createEmployee( + newEmployee?.id || existingEmployee.id, + admin, + apiEndpoint, + refreshFunction, + entityType, + iteration + ); + } else { + // Update admin + await updateEmployee( + newEmployee?.id || existingEmployee.id, + admin, + apiEndpoint, + refreshFunction, + entityType, + iteration + ); + } + break; + + case 'drivers': + if (existingEmployee.isAdmin && !containsAdmin) { + // Transition from admin to driver + await deleteEmployee( + newEmployee?.id || existingEmployee.id, + 'admins' + ); + } - const onSubmit = async (data: ObjectType) => { + if (!existingEmployee.isDriver) { + // Create driver + await createEmployee( + newEmployee?.id || existingEmployee.id, + driver, + apiEndpoint, + refreshFunction, + entityType, + iteration + ); + } else { + // Update driver + await updateEmployee( + newEmployee?.id || existingEmployee.id, + driver, + apiEndpoint, + refreshFunction, + entityType, + iteration + ); + } + break; + } + } else if (!newEmployee) { + // Create a new employee if no existing employee is present + newEmployee = await createEmployee( + '', + role === 'admins' ? admin : driver, + apiEndpoint, + refreshFunction, + entityType, + iteration + ); + existingEmployee = newEmployee; + showToast( + `Created a new employee with the role of ${role} based on your provided data`, + ToastStatus.SUCCESS + ); + } + iteration += 1; + } + } + + async function onSubmit(data: ObjectType) { const { firstName, lastName, netid, phoneNumber, startDate, availability } = data; const driver = { firstName, lastName, - email: netid + '@cornell.edu', + email: `${netid}@cornell.edu`, phoneNumber, startDate, availability: parseAvailability(availability), @@ -219,103 +347,44 @@ const EmployeeModal = ({ const admin = { firstName, lastName, - email: netid + '@cornell.edu', - type: selectedRole.filter((role) => !(role === 'driver')), + email: `${netid}@cornell.edu`, + type: selectedRole.filter((role) => role !== 'driver'), phoneNumber, availability: parseAvailability(availability), isDriver: selectedRole.includes('driver'), }; - const existingDriver = existingEmployee?.isDriver === undefined; - const existingAdmin = existingEmployee?.isDriver !== undefined; - - if (existingEmployee) { - if (selectedRole.includes('driver')) { - if (selectedRole.some((role) => role.includes('admin'))) { - if (existingDriver && existingAdmin) { - await createOrUpdateDriver(driver, false); - await createOrUpdateAdmin(admin, false); - } else if (existingDriver) { - await createOrUpdateDriver(driver, false); - await createOrUpdateAdmin( - { ...admin, id: existingEmployee.id }, - true - ); - } else if (existingAdmin) { - await createOrUpdateDriver( - { ...driver, id: existingEmployee.id }, - true - ); - await createOrUpdateAdmin(admin, false); - } - } else { - if (existingDriver && existingAdmin) { - await createOrUpdateDriver(driver, false); - await deleteAdmin(existingEmployee.id); - } else if (existingDriver) { - await createOrUpdateDriver(driver, false); - } else if (existingAdmin) { - await createOrUpdateDriver( - { ...driver, id: existingEmployee.id }, - true - ); - await deleteAdmin(existingEmployee.id); - } - } - } else { - if (existingDriver && existingAdmin) { - await deleteDriver(existingEmployee.id); - await createOrUpdateAdmin(admin, false); - } else if (existingDriver) { - await deleteDriver(existingEmployee.id); - await createOrUpdateAdmin( - { ...admin, id: existingEmployee.id }, - true - ); - } - } - } else { - if (selectedRole.includes('driver')) { - if (selectedRole.some((role) => role.includes('admin'))) { - const id = (await createOrUpdateDriver(driver, true)).data.data.id; - await createOrUpdateAdmin({ ...admin, id: id }, true); - } else { - await createOrUpdateDriver(driver, true); - } - } else { - await createOrUpdateAdmin(admin, true); - } + try { + await processRoles(selectedRole, existingEmployee, admin, driver); + showToast(`Employee information proccessed`, ToastStatus.SUCCESS); + } catch (error) { + showToast('An error occured: ', ToastStatus.ERROR); + } finally { + closeModal(); } - closeModal(); - }; + } - function updateBase64(e: React.ChangeEvent) { + async function updateBase64(e: React.ChangeEvent) { e.preventDefault(); - if (e.target.files && e.target.files[0]) { + const { files } = e.target; + if (files && files[0]) { + const file = files[0]; const reader = new FileReader(); - const file = e.target.files[0]; reader.readAsDataURL(file); - reader.onload = function () { - let res = reader.result; - if (res) { - res = res.toString(); - // remove "data:image/png;base64," and "data:image/jpeg;base64," - const strBase64 = res.toString().substring(res.indexOf(',') + 1); - setImageBase64(strBase64); + reader.onload = async () => { + const base64 = reader.result?.toString().split(',')[1]; // Extract base64 + if (base64) { + setImageBase64(base64); // Save the base64 string } }; - reader.onerror = function (error) { - console.log('Error reading file: ', error); - }; - } else { - console.log('Undefined file upload'); } } + return ( <> +
{ + methods.handleSubmit(onSubmit)(e); + }} aria-labelledby="employee-modal" >
diff --git a/frontend/src/components/EmployeeModal/Upload.tsx b/frontend/src/components/EmployeeModal/Upload.tsx index 62ef509cf..9c5ec7869 100644 --- a/frontend/src/components/EmployeeModal/Upload.tsx +++ b/frontend/src/components/EmployeeModal/Upload.tsx @@ -2,6 +2,8 @@ import React, { useState, createRef } from 'react'; import uploadBox from './upload.svg'; import styles from './employeemodal.module.css'; +const IMAGE_SIZE_LIMIT = 500000000; + type UploadProps = { imageChange: (e: React.ChangeEvent) => void; existingPhoto?: string; @@ -11,24 +13,24 @@ const Upload = ({ imageChange, existingPhoto }: UploadProps) => { const [imageURL, setImageURL] = useState( existingPhoto ? `${existingPhoto}` : '' ); + const [errorMessage, setErrorMessage] = useState(null); const inputRef = createRef(); - /* This is for accessibility purposes only */ + const handleKeyboardPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { inputRef.current && inputRef.current.click(); } }; + function previewImage(e: React.ChangeEvent) { - e.preventDefault(); const { files } = e.target; - if (files && files[0] && files[0].size < 500000) { - const file = files[0]; - const photoURL = URL.createObjectURL(file); - setImageURL(photoURL); + if (files && files[0] && files[0].size < IMAGE_SIZE_LIMIT) { + setImageURL(URL.createObjectURL(files[0])); + imageChange(e); } else { - console.log('invalid file'); + setErrorMessage(`Images must be under ${IMAGE_SIZE_LIMIT / 1000} KB`); + console.log(errorMessage); } - imageChange(e); } return ( @@ -45,10 +47,10 @@ const Upload = ({ imageChange, existingPhoto }: UploadProps) => { previewImage(e)} + onChange={previewImage} />