diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index ede503997..42bc42f3c 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -1,5 +1,6 @@ import { ConflictException, + HttpException, Injectable, InternalServerErrorException, Logger, @@ -75,10 +76,18 @@ export class AuthService { return sub ?? ''; } catch (error) { - if (error instanceof Error && error.name == 'UsernameExistsException') { + if (error instanceof HttpException) { + throw error; + } else if ( + error instanceof Error && + error.name == 'UsernameExistsException' + ) { throw new ConflictException('A user with this email already exists'); } else { - throw new InternalServerErrorException('Failed to create user'); + const reason = error instanceof Error ? error.message : String(error); + throw new InternalServerErrorException( + `Failed to create user: ${reason}`, + ); } } } @@ -97,8 +106,9 @@ export class AuthService { `Failed to add user ${username} to group ${groupName}`, error, ); + const reason = error instanceof Error ? error.message : String(error); throw new InternalServerErrorException( - `Failed to add user to group ${groupName}`, + `Failed to add user to group ${groupName}: ${reason}`, ); } } diff --git a/apps/backend/src/auth/dtos/sign-up.dto.ts b/apps/backend/src/auth/dtos/sign-up.dto.ts index 6fa066a3f..59e13223f 100644 --- a/apps/backend/src/auth/dtos/sign-up.dto.ts +++ b/apps/backend/src/auth/dtos/sign-up.dto.ts @@ -16,8 +16,7 @@ export class SignUpDto { @IsString() @IsNotEmpty() @IsPhoneNumber('US', { - message: - 'phone must be a valid phone number (make sure all the digits are correct)', + message: 'Phone must be a valid US phone number.', }) phone!: string; } diff --git a/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts b/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts index e5cacdad1..6c5460802 100644 --- a/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts +++ b/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts @@ -41,8 +41,7 @@ export class FoodManufacturerApplicationDto { @IsString() @IsNotEmpty() @IsPhoneNumber('US', { - message: - 'contactPhone must be a valid phone number (make sure all the digits are correct)', + message: 'Phone must be a valid US phone number.', }) contactPhone!: string; @@ -67,8 +66,7 @@ export class FoodManufacturerApplicationDto { @IsOptional() @IsString() @IsPhoneNumber('US', { - message: - 'secondaryContactPhone must be a valid phone number (make sure all the digits are correct)', + message: 'Secondary phone must be a valid US phone number.', }) @IsNotEmpty() secondaryContactPhone?: string; diff --git a/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts b/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts index 7130c690e..d291253ce 100644 --- a/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts +++ b/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts @@ -37,8 +37,7 @@ export class UpdateFoodManufacturerApplicationDto { @IsOptional() @IsString() @IsPhoneNumber('US', { - message: - 'secondaryContactPhone must be a valid phone number (make sure all the digits are correct)', + message: 'Secondary phone contact must be a valid US phone number.', }) @IsNotEmpty() secondaryContactPhone?: string; diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.ts index 92b4cd374..f60e3a4c4 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.ts @@ -159,7 +159,7 @@ export class FoodManufacturersController { type: 'string', format: 'phone', example: '(508) 508-6789', - description: 'Must be a valid US phone number', + description: 'Phone must be a valid US phone number', }, secondaryContactFirstName: { type: 'string', @@ -178,7 +178,7 @@ export class FoodManufacturersController { type: 'string', format: 'phone', example: '(508) 528-6789', - description: 'Must be a valid US phone number', + description: 'Phone must be a valid US phone number', }, unlistedProductAllergens: { type: 'array', diff --git a/apps/backend/src/pantries/dtos/pantry-application.dto.ts b/apps/backend/src/pantries/dtos/pantry-application.dto.ts index d908dcf51..e3971bca8 100644 --- a/apps/backend/src/pantries/dtos/pantry-application.dto.ts +++ b/apps/backend/src/pantries/dtos/pantry-application.dto.ts @@ -40,8 +40,7 @@ export class PantryApplicationDto { @IsString() @IsNotEmpty() @IsPhoneNumber('US', { - message: - 'contactPhone must be a valid phone number (make sure all the digits are correct)', + message: 'Phone must be a valid US phone number.', }) contactPhone!: string; @@ -74,8 +73,7 @@ export class PantryApplicationDto { @IsOptional() @IsString() @IsPhoneNumber('US', { - message: - 'secondaryContactPhone must be a valid phone number (make sure all the digits are correct)', + message: 'Secondary phone must be a valid US phone number.', }) @IsNotEmpty() secondaryContactPhone?: string; diff --git a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts index b744ca63b..632d10acd 100644 --- a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts +++ b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts @@ -44,8 +44,7 @@ export class UpdatePantryApplicationDto { @IsOptional() @IsString() @IsPhoneNumber('US', { - message: - 'Secondary contact phone must be a valid phone number (make sure all the digits are correct)', + message: 'Secondary phone must be a valid US phone number.', }) @IsNotEmpty() secondaryContactPhone?: string; diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index 913ee0998..daaf470e8 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -171,7 +171,7 @@ export class PantriesController { type: 'string', format: 'phone', example: '(508) 508-6789', - description: 'Must be a valid US phone number', + description: 'Phone must be a valid US phone number', }, hasEmailContact: { type: 'boolean', @@ -198,7 +198,7 @@ export class PantriesController { type: 'string', format: 'phone', example: '(508) 528-6789', - description: 'Must be a valid US phone number', + description: 'Phone must be a valid US phone number', }, pantryName: { type: 'string', diff --git a/apps/backend/src/users/dtos/update-user-info.dto.ts b/apps/backend/src/users/dtos/update-user-info.dto.ts index 94716f917..45ec0f991 100644 --- a/apps/backend/src/users/dtos/update-user-info.dto.ts +++ b/apps/backend/src/users/dtos/update-user-info.dto.ts @@ -23,8 +23,7 @@ export class UpdateUserInfoDto { @IsString() @IsNotEmpty() @IsPhoneNumber('US', { - message: - 'phone must be a valid phone number (make sure all the digits are correct)', + message: 'Phone must be a valid US phone number.', }) phone?: string; } diff --git a/apps/backend/src/users/dtos/userSchema.dto.ts b/apps/backend/src/users/dtos/userSchema.dto.ts index 2f110c0bd..5876eb470 100644 --- a/apps/backend/src/users/dtos/userSchema.dto.ts +++ b/apps/backend/src/users/dtos/userSchema.dto.ts @@ -27,8 +27,7 @@ export class userSchemaDto { @IsString() @IsNotEmpty() @IsPhoneNumber('US', { - message: - 'phone must be a valid phone number (make sure all the digits are correct)', + message: 'Phone must be a valid US phone number.', }) phone!: string; diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index cfaf7a919..50b375792 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -1,4 +1,8 @@ -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { + createBrowserRouter, + Navigate, + RouterProvider, +} from 'react-router-dom'; import Root from '@containers/root'; import NotFound from '@containers/404'; import FormRequests from '@containers/formRequests'; @@ -9,7 +13,6 @@ import ApprovePantries from '@containers/approvePantries'; import PantryApplicationDetails from '@containers/pantryApplicationDetails'; import VolunteerManagement from '@containers/userManagement'; import AdminDonation from '@containers/adminDonation'; -import Homepage from '@containers/homepage'; import AdminOrderManagement from '@containers/adminOrderManagement'; import { Amplify } from 'aws-amplify'; import CognitoAuthConfig from './aws-exports'; @@ -50,7 +53,7 @@ const router = createBrowserRouter([ // Public routes (no auth needed) { index: true, - element: , + element: , }, { path: ROUTES.LOGIN, diff --git a/apps/frontend/src/assets/login_background.png b/apps/frontend/src/assets/login_background.png index a45dd03c6..59617c0c4 100644 Binary files a/apps/frontend/src/assets/login_background.png and b/apps/frontend/src/assets/login_background.png differ diff --git a/apps/frontend/src/components/Navbar.tsx b/apps/frontend/src/components/Navbar.tsx index 856a0753c..c6a033478 100644 --- a/apps/frontend/src/components/Navbar.tsx +++ b/apps/frontend/src/components/Navbar.tsx @@ -228,7 +228,7 @@ const Navbar: React.FC = () => { ApiClient.getMe() .then(setCurrentUser) .catch(() => setCurrentUser(null)); - } else { + } else if (authStatus === 'unauthenticated') { setCurrentUser(null); } }, [authStatus]); @@ -271,7 +271,6 @@ const Navbar: React.FC = () => { navigate(ROUTES.LOGIN, { replace: true }); }; - // Should be changed once other dashboards are implmented const ROLE_DASHBOARD_ROUTE: Record = { [Role.ADMIN]: ROUTES.ADMIN_DASHBOARD, [Role.FOODMANUFACTURER]: ROUTES.FM_DASHBOARD, @@ -330,7 +329,7 @@ const Navbar: React.FC = () => { overflow="hidden" style={{ whiteSpace: 'normal', wordBreak: 'break-word' }} > - {roleLabel ? `${roleLabel} Dashboard` : 'Dashboard'} + {roleLabel ? `${roleLabel}` : 'Profile'} { - - = ({ ); const itemsPerPage = 10; - const totalPages = Math.ceil(filteredRequests.length / itemsPerPage); const paginatedRequests = filteredRequests.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage, @@ -469,59 +466,14 @@ const RequestManagement: React.FC = ({ )} - {totalPages > 1 && ( - + setCurrentPage(e.page)} - > - - - setCurrentPage((prev) => Math.max(prev - 1, 1))} - > - - - - - ( - - {page.value} - - )} - /> - - - - setCurrentPage((prev) => Math.min(prev + 1, totalPages)) - } - > - - - - - - )} + onPageChange={setCurrentPage} + /> + ); }; diff --git a/apps/frontend/src/components/forms/addNewVolunteerModal.tsx b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx index 1c0e3e96e..f175d9dba 100644 --- a/apps/frontend/src/components/forms/addNewVolunteerModal.tsx +++ b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx @@ -35,8 +35,10 @@ const NewVolunteerModal: React.FC = ({ const [isOpen, setIsOpen] = useState(false); const [alertState, setAlertMessage] = useAlert(); + const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async () => { + if (isSubmitting) return; if (!firstName || !lastName || !email || !phone || phone === '+1') { setAlertMessage('Please fill in all fields. *', AlertStatus.ERROR); return; @@ -50,6 +52,7 @@ const NewVolunteerModal: React.FC = ({ role: Role.VOLUNTEER, }; + setIsSubmitting(true); try { await ApiClient.postUser(newVolunteer); if (onSubmitSuccess) onSubmitSuccess(); @@ -90,6 +93,8 @@ const NewVolunteerModal: React.FC = ({ if (onSubmitFail) onSubmitFail(); handleClear(); } + } finally { + setIsSubmitting(false); } }; @@ -226,6 +231,7 @@ const NewVolunteerModal: React.FC = ({ bg={'blue.hover'} color={'white'} onClick={handleSubmit} + disabled={isSubmitting} > Submit diff --git a/apps/frontend/src/components/forms/assignVolunteersModal.tsx b/apps/frontend/src/components/forms/assignVolunteersModal.tsx index e4a4dffe4..120fab770 100644 --- a/apps/frontend/src/components/forms/assignVolunteersModal.tsx +++ b/apps/frontend/src/components/forms/assignVolunteersModal.tsx @@ -52,6 +52,8 @@ const AssignVolunteersModal: React.FC = ({ const [searchName, setSearchName] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const handleSearchNameChange = ( event: React.ChangeEvent, ) => { @@ -99,6 +101,8 @@ const AssignVolunteersModal: React.FC = ({ }; const handleSave = async () => { + if (isSubmitting) return; + setIsSubmitting(true); try { const originalIds = new Set(pantry.volunteers.map((v) => v.userId)); @@ -120,6 +124,8 @@ const AssignVolunteersModal: React.FC = ({ onClose(); } catch { setAlertMessage('Error saving volunteer assignments', AlertStatus.ERROR); + } finally { + setIsSubmitting(false); } }; @@ -273,6 +279,7 @@ const AssignVolunteersModal: React.FC = ({ fontWeight={600} onClick={handleSave} px={10} + disabled={isSubmitting} > Save Changes diff --git a/apps/frontend/src/components/forms/createNewOrderModal.tsx b/apps/frontend/src/components/forms/createNewOrderModal.tsx index 458f655fb..b9a1d3d8c 100644 --- a/apps/frontend/src/components/forms/createNewOrderModal.tsx +++ b/apps/frontend/src/components/forms/createNewOrderModal.tsx @@ -54,6 +54,7 @@ const CreateNewOrderModal: React.FC = ({ const [itemAllocations, setItemAllocations] = useState< Record >({}); + const [isSubmittingOrder, setIsSubmittingOrder] = useState(false); useEffect(() => { const fetchManufacturers = async () => { @@ -120,6 +121,7 @@ const CreateNewOrderModal: React.FC = ({ }; const onSubmitNewOrder = async () => { + if (isSubmittingOrder) return; const cleanedAllocations: Record = {}; for (const item in itemAllocations) { @@ -136,12 +138,15 @@ const CreateNewOrderModal: React.FC = ({ itemAllocations: cleanedAllocations, }; + setIsSubmittingOrder(true); try { await apiClient.createOrder(data); onClose(); onSuccess(); } catch { setAlertMessage('Error creating new order', AlertStatus.ERROR); + } finally { + setIsSubmittingOrder(false); } }; @@ -459,6 +464,7 @@ const CreateNewOrderModal: React.FC = ({ bg={'blue.hover'} color={'white'} onClick={onSubmitNewOrder} + disabled={isSubmittingOrder} > Continue diff --git a/apps/frontend/src/components/forms/donationDetailsModal.tsx b/apps/frontend/src/components/forms/donationDetailsModal.tsx index 018a39f0b..c22059f2c 100644 --- a/apps/frontend/src/components/forms/donationDetailsModal.tsx +++ b/apps/frontend/src/components/forms/donationDetailsModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Box, Text, @@ -47,8 +47,13 @@ const DonationDetailsModal: React.FC = ({ const [alertState, setAlertMessage] = useAlert(); const [isEditing, setIsEditing] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); - const donationId = donation?.donationId; + const donationRef = useRef(donation); + if (donation) donationRef.current = donation; + const displayDonation = donation ?? donationRef.current; + + const donationId = displayDonation?.donationId; const handleCancel = () => { setIsEditing(false); @@ -79,6 +84,7 @@ const DonationDetailsModal: React.FC = ({ foodRescue: r.foodRescue, })); + setIsSubmitting(true); try { await ApiClient.editDonationItems(donationId, body); await loadItems(); @@ -90,6 +96,8 @@ const DonationDetailsModal: React.FC = ({ 'Donation items could not be updated.', AlertStatus.ERROR, ); + } finally { + setIsSubmitting(false); } }; @@ -124,7 +132,7 @@ const DonationDetailsModal: React.FC = ({ )} - {donation !== null && ( + {displayDonation !== null && ( = ({ Donation #{donationId} Stock - {donation.status === DonationStatus.AVAILABLE && + {displayDonation.status === DonationStatus.AVAILABLE && !isEditing && ( <> = ({ )} - {donation.foodManufacturer?.foodManufacturerName} + {displayDonation.foodManufacturer?.foodManufacturerName} + + + {formatDate(displayDonation.dateDonated)} - {formatDate(donation.dateDonated)} @@ -172,6 +182,7 @@ const DonationDetailsModal: React.FC = ({ onCancel={handleCancel} onSubmit={handleUpdate} submitButtonLabel="Update Donation" + isSubmitting={isSubmitting} > ) : ( @@ -226,27 +237,28 @@ const DonationDetailsModal: React.FC = ({ )} - {!isEditing && donation.recurrence !== RecurrenceEnum.NONE && ( - - - Donation sets up recurring reminders - + {!isEditing && + displayDonation.recurrence !== RecurrenceEnum.NONE && ( + + + Donation sets up recurring reminders + - {donation.nextDonationDates && - donation.nextDonationDates.length > 0 && ( - - - Upcoming reminder emails - - - {donation.nextDonationDates - .map((date) => formatDate(date)) - .join(', ')} - - - )} - - )} + {displayDonation.nextDonationDates && + displayDonation.nextDonationDates.length > 0 && ( + + + Upcoming reminder emails + + + {displayDonation.nextDonationDates + .map((date) => formatDate(date)) + .join(', ')} + + + )} + + )} diff --git a/apps/frontend/src/components/forms/editableDonationItemsTable.tsx b/apps/frontend/src/components/forms/editableDonationItemsTable.tsx index dbaad9cd3..2a9933d71 100644 --- a/apps/frontend/src/components/forms/editableDonationItemsTable.tsx +++ b/apps/frontend/src/components/forms/editableDonationItemsTable.tsx @@ -122,6 +122,7 @@ interface EditableDonationItemsTableProps { onCancel: () => void; onSubmit: (rows: DonationRow[], recurrence: RecurrenceData | null) => void; submitButtonLabel: string; + isSubmitting?: boolean; } const EditableDonationItemsTable: React.FC = ({ @@ -130,6 +131,7 @@ const EditableDonationItemsTable: React.FC = ({ onCancel, onSubmit, submitButtonLabel, + isSubmitting, }) => { const [rows, setRows] = useState(initialRows ?? [BLANK_ROW]); @@ -200,7 +202,7 @@ const EditableDonationItemsTable: React.FC = ({ endsAfter, ); - const isSubmitDisabled = firstValidationError !== null; + const isSubmitDisabled = firstValidationError !== null || !!isSubmitting; const getSelectedDaysText = () => { const selected = (Object.keys(repeatOn) as DayOfWeek[]).filter( @@ -672,7 +674,7 @@ const EditableDonationItemsTable: React.FC = ({ > Cancel - + - diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index 1c3e2a701..d79f6fda3 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -35,6 +35,7 @@ const NewDonationFormModal: React.FC = ({ const [alertState, setAlertMessage] = useAlert(); const [manufacturers, setManufacturers] = useState([]); const [selectedFmId, setSelectedFmId] = useState(foodManufacturerId); + const [isSubmitting, setIsSubmitting] = useState(false); // Load the manufacturers this representative can donate for so they can // choose which one the donation is for. @@ -84,12 +85,15 @@ const NewDonationFormModal: React.FC = ({ })), }; + setIsSubmitting(true); try { await ApiClient.postDonation(donationBody); onDonationSuccess(); onClose(); } catch { setAlertMessage('Error submitting new donation', AlertStatus.ERROR); + } finally { + setIsSubmitting(false); } }; @@ -191,6 +195,7 @@ const NewDonationFormModal: React.FC = ({ onCancel={onClose} onSubmit={handleLogDonation} submitButtonLabel="Submit Donation" + isSubmitting={isSubmitting} > diff --git a/apps/frontend/src/components/forms/orderReceivedActionModal.tsx b/apps/frontend/src/components/forms/orderReceivedActionModal.tsx index 940222499..00fe75319 100644 --- a/apps/frontend/src/components/forms/orderReceivedActionModal.tsx +++ b/apps/frontend/src/components/forms/orderReceivedActionModal.tsx @@ -49,6 +49,7 @@ const OrderReceivedActionModal: React.FC = ({ const [dateReceived, setDateReceived] = useState(''); const [photos, setPhotos] = useState([]); const [invalidPhotoExists, setInvalidPhotoExists] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const isFormValid = dateReceived !== '' && !invalidPhotoExists; @@ -64,6 +65,8 @@ const OrderReceivedActionModal: React.FC = ({ }; const handleSubmit = async () => { + if (isSubmitting) return; + setIsSubmitting(true); try { // TODO: fix date/time storage/handling const dto: ConfirmDeliveryDto = { @@ -80,6 +83,8 @@ const OrderReceivedActionModal: React.FC = ({ resetForm(); onError(); onClose(); + } finally { + setIsSubmitting(false); } }; @@ -309,7 +314,7 @@ const OrderReceivedActionModal: React.FC = ({ onClick={handleSubmit} bg={isFormValid ? 'blue.hover' : 'neutral.400'} color={'white'} - disabled={!isFormValid} + disabled={!isFormValid || isSubmitting} > Continue diff --git a/apps/frontend/src/components/forms/pantryApplicationForm.tsx b/apps/frontend/src/components/forms/pantryApplicationForm.tsx index 69e3cc63f..e1781bfde 100644 --- a/apps/frontend/src/components/forms/pantryApplicationForm.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationForm.tsx @@ -22,6 +22,7 @@ import { Form, redirect, useActionData, + useNavigation, } from 'react-router-dom'; import React, { useEffect, useState } from 'react'; import { USPhoneInput } from '@components/forms/usPhoneInput'; @@ -91,6 +92,8 @@ const PantryApplicationForm: React.FC = () => { const [otherEmailContact, setOtherEmailContact] = useState(false); const [alertState, setAlertMessage] = useAlert(); const actionData = useActionData() as { error?: string } | undefined; + const navigation = useNavigation(); + const isSubmitting = navigation.state === 'submitting'; const sectionTitleStyles = { fontFamily: 'inter', @@ -1169,7 +1172,13 @@ const PantryApplicationForm: React.FC = () => { > Cancel - diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index ed586a89b..58feeb433 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -25,20 +25,18 @@ import { CloseButton, Tabs, Badge, - Pagination, - ButtonGroup, - IconButton, Button, Textarea, Input, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft, ChevronDownIcon } from 'lucide-react'; +import { ChevronDownIcon } from 'lucide-react'; import { TagGroup } from './tagGroup'; import { useGroupedItemsByFoodType } from '../../hooks/groupedItemsByFoodType'; import { useModalBodyCleanup } from '../../hooks/modalBodyCleanup'; import { useAlert } from '../../hooks/alert'; import { FloatingAlert } from '../floatingAlert'; import { EditButton, DeleteButton } from '@components/editDeleteButtons'; +import { PaginationControl } from '@components/pagination'; interface RequestDetailsModalProps { request: FoodRequestSummaryDto; @@ -664,56 +662,12 @@ const RequestDetailsModal: React.FC = ({ {orderDetailsList.length > 0 && ( - setCurrentPage(page)} - > - - - - setCurrentPage((prev) => - Math.max(prev - 1, 1), - ) - } - > - - - - - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> - - - - setCurrentPage((prev) => - Math.min(prev + 1, orderDetailsList.length), - ) - } - > - - - - - + onPageChange={setCurrentPage} + /> )} diff --git a/apps/frontend/src/components/forms/requestFormModal.tsx b/apps/frontend/src/components/forms/requestFormModal.tsx index 987a65d5e..e27af19b6 100644 --- a/apps/frontend/src/components/forms/requestFormModal.tsx +++ b/apps/frontend/src/components/forms/requestFormModal.tsx @@ -49,6 +49,7 @@ const FoodRequestFormModal: React.FC = ({ const [feedbackOnPriorDonation, setFeedbackOnPriorDonation] = useState(''); const [alertState, setAlertMessage] = useAlert(); + const [isSubmitting, setIsSubmitting] = useState(false); const isFormValid = requestedSize !== '' && @@ -88,6 +89,7 @@ const FoodRequestFormModal: React.FC = ({ }, [isOpen, previousRequest, setAlertMessage]); const handleSubmit = async () => { + if (isSubmitting) return; const foodRequestData: CreateFoodRequestBody = { pantryId, requestedSize: requestedSize as RequestSize, @@ -97,6 +99,7 @@ const FoodRequestFormModal: React.FC = ({ requestedFoodTypes: selectedFoodTypes, }; + setIsSubmitting(true); try { await apiClient.createFoodRequest(foodRequestData); setAlertMessage('Request submitted', AlertStatus.INFO); @@ -104,6 +107,8 @@ const FoodRequestFormModal: React.FC = ({ onSuccess(); } catch { setAlertMessage('Request could not be submitted.', AlertStatus.ERROR); + } finally { + setIsSubmitting(false); } }; @@ -408,7 +413,7 @@ const FoodRequestFormModal: React.FC = ({ onClick={handleSubmit} bg={isFormValid ? 'blue.hover' : 'neutral.400'} color={'white'} - disabled={!isFormValid} + disabled={!isFormValid || isSubmitting} > Continue diff --git a/apps/frontend/src/components/pagination.tsx b/apps/frontend/src/components/pagination.tsx new file mode 100644 index 000000000..b196532e4 --- /dev/null +++ b/apps/frontend/src/components/pagination.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { Pagination, ButtonGroup, IconButton } from '@chakra-ui/react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +interface PaginationControlProps { + count: number; + pageSize: number; + page: number; + onPageChange: (page: number) => void; +} + +export const PaginationControl: React.FC = ({ + count, + pageSize, + page, + onPageChange, +}) => { + const totalPages = Math.ceil(count / pageSize); + + if (totalPages <= 1) return null; + + return ( + onPageChange(e.page)} + > + + + + + + + + ( + + {p.value} + + )} + /> + + + + + + + + + ); +}; diff --git a/apps/frontend/src/containers/adminDonation.tsx b/apps/frontend/src/containers/adminDonation.tsx index 3d3a98e4a..680e48ac2 100644 --- a/apps/frontend/src/containers/adminDonation.tsx +++ b/apps/frontend/src/containers/adminDonation.tsx @@ -1,15 +1,12 @@ import React, { useState, useEffect } from 'react'; -import { ArrowDownUp, ChevronRight, ChevronLeft, Funnel } from 'lucide-react'; +import { ArrowDownUp, Funnel } from 'lucide-react'; import { Box, Button, Table, Heading, - Pagination, - IconButton, Checkbox, VStack, - ButtonGroup, Link, } from '@chakra-ui/react'; import { AlertStatus, Donation } from '../types/types'; @@ -22,6 +19,7 @@ import { useSearchParams, useNavigate } from 'react-router-dom'; import { ROUTES } from '../routes'; import FMDeleteDonationActionModal from '@components/forms/fmDeleteDonationModal'; import PageEmptyState from '@components/pageEmptyState'; +import { PaginationControl } from '@components/pagination'; const AdminDonation: React.FC = () => { const [searchParams] = useSearchParams(); @@ -80,9 +78,10 @@ const AdminDonation: React.FC = () => { setCurrentPage(Math.floor(idx / itemsPerPage) + 1); } } else { + setAlertMessage('Donation not found.', AlertStatus.ERROR); navigate(ROUTES.ADMIN_DONATION, { replace: true }); } - }, [searchParams, donations, navigate]); + }, [searchParams, donations, navigate, setAlertMessage]); // Pre-fill manufacturer filter from the foodManufacturerId url param and then // clear the param, so navigating from "View Donations" filters to that @@ -146,7 +145,6 @@ const AdminDonation: React.FC = () => { ); const itemsPerPage = 10; - const totalPages = Math.ceil(filteredDonations.length / itemsPerPage); const paginatedDonations = filteredDonations.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage, @@ -258,72 +256,79 @@ const AdminDonation: React.FC = () => { Sort - - - - - Donation # - - - Manufacturer - - - Date Started - - - - - {paginatedDonations.map((donation, index) => ( - - + ) : ( + + + + - setSelectedDonation(donation)} - > - {donation.donationId} - - - + - {donation.foodManufacturer?.foodManufacturerName} - - + - {formatDate(donation.dateDonated)} - + Date Started + - ))} - - + + + {paginatedDonations.map((donation, index) => ( + + + setSelectedDonation(donation)} + > + {donation.donationId} + + + + {donation.foodManufacturer?.foodManufacturerName} + + + {formatDate(donation.dateDonated)} + + + ))} + + + )} )} @@ -341,68 +346,25 @@ const AdminDonation: React.FC = () => { }} /> - {selectedDonation !== null && ( - { - setSelectedDonation(null); - navigate(ROUTES.ADMIN_DONATION, { replace: true }); - }} - onSuccess={() => fetchDonations()} - onDelete={() => setDeleteDonation(selectedDonation)} - /> - )} + { + setSelectedDonation(null); + navigate(ROUTES.ADMIN_DONATION, { replace: true }); + }} + onSuccess={() => fetchDonations()} + onDelete={() => setDeleteDonation(selectedDonation)} + /> - {totalPages > 1 && ( - + setCurrentPage(e.page)} - > - - - - - - ( - - {page.value} - - )} - /> - - - - - - - )} + onPageChange={setCurrentPage} + /> + ); }; diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx index 80911d06c..1902cde5c 100644 --- a/apps/frontend/src/containers/adminDonationStats.tsx +++ b/apps/frontend/src/containers/adminDonationStats.tsx @@ -1,27 +1,19 @@ -import React, { useState, useEffect } from 'react'; -import { - ChevronRight, - ChevronLeft, - ChevronDown, - Funnel, - Search, -} from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { ChevronDown, Funnel, Search } from 'lucide-react'; import { Box, Button, Table, Heading, - Pagination, - IconButton, Checkbox, VStack, - ButtonGroup, Input, } from '@chakra-ui/react'; import { AlertStatus, PantryStats, TotalStats } from '../types/types'; import ApiClient from '@api/apiClient'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; +import { PaginationControl } from '@components/pagination'; const AdminDonationStats: React.FC = () => { // Individual and combined pantry stats to be displayed @@ -40,6 +32,9 @@ const AdminDonationStats: React.FC = () => { const [alertState, setAlertMessage] = useAlert(); + const totalStatsRequestIdRef = useRef(0); + const pantryStatsRequestIdRef = useRef(0); + useEffect(() => { const fetchInitialData = async () => { try { @@ -60,23 +55,26 @@ const AdminDonationStats: React.FC = () => { }, [setAlertMessage]); useEffect(() => { - // Total stats only displayed on first page, so no need to do anything on page change - if (currentPage !== 1) return; - + const requestId = ++totalStatsRequestIdRef.current; const fetchTotalStats = async () => { try { const stats = await ApiClient.getTotalStats( selectedYears.length ? selectedYears : undefined, ); - setTotalStats(stats); + if (requestId === totalStatsRequestIdRef.current) { + setTotalStats(stats); + } } catch { - setAlertMessage('Error fetching total stats', AlertStatus.ERROR); + if (requestId === totalStatsRequestIdRef.current) { + setAlertMessage('Error fetching total stats', AlertStatus.ERROR); + } } }; fetchTotalStats(); - }, [setAlertMessage, selectedYears, currentPage]); + }, [setAlertMessage, selectedYears]); useEffect(() => { + const requestId = ++pantryStatsRequestIdRef.current; const fetchStats = async () => { try { const stats = await ApiClient.getPantryStats({ @@ -84,9 +82,13 @@ const AdminDonationStats: React.FC = () => { years: selectedYears.length ? selectedYears : undefined, page: currentPage, }); - setPantryStats(stats); + if (requestId === pantryStatsRequestIdRef.current) { + setPantryStats(stats); + } } catch { - setAlertMessage('Error fetching pantry stats', AlertStatus.ERROR); + if (requestId === pantryStatsRequestIdRef.current) { + setAlertMessage('Error fetching pantry stats', AlertStatus.ERROR); + } } }; fetchStats(); @@ -121,7 +123,6 @@ const AdminDonationStats: React.FC = () => { const pantryList = selectedPantries.length > 0 ? selectedPantries : pantryNameOptions; const totalCount = pantryList.length; - const totalPages = Math.ceil(totalCount / itemsPerPage); const tableHeaderStyles = { borderBottom: '1px solid', @@ -409,85 +410,83 @@ const AdminDonationStats: React.FC = () => { - {currentPage === 1 && ( - - - All Pantries - - - {totalStats?.totalItems ?? 0} - - - {(totalStats?.totalOz ?? 0).toFixed(2)} - - - {(totalStats?.totalLbs ?? 0).toFixed(2)} - - - ${(totalStats?.totalDonatedFoodValue ?? 0).toFixed(2)} - - - ${(totalStats?.totalShippingCost ?? 0).toFixed(2)} - - - ${(totalStats?.totalShippingCostPaidBySsf ?? 0).toFixed(2)} - - - ${(totalStats?.totalValue ?? 0).toFixed(2)} - - - {(totalStats?.percentageFoodRescueItems ?? 0).toFixed(2)}% - - - {(totalStats?.foodRescueLbs ?? 0).toFixed(2)} - - - )} + + + All Pantries + + + {totalStats?.totalItems ?? 0} + + + {(totalStats?.totalOz ?? 0).toFixed(2)} + + + {(totalStats?.totalLbs ?? 0).toFixed(2)} + + + ${(totalStats?.totalDonatedFoodValue ?? 0).toFixed(2)} + + + ${(totalStats?.totalShippingCost ?? 0).toFixed(2)} + + + ${(totalStats?.totalShippingCostPaidBySsf ?? 0).toFixed(2)} + + + ${(totalStats?.totalValue ?? 0).toFixed(2)} + + + {(totalStats?.percentageFoodRescueItems ?? 0).toFixed(2)}% + + + {(totalStats?.foodRescueLbs ?? 0).toFixed(2)} + + {pantryStats.map((stat) => ( { - {totalPages > 1 && ( - + setCurrentPage(e.page)} - > - - - setCurrentPage((prev) => Math.max(prev - 1, 1))} - > - - - - - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> - - - - setCurrentPage((prev) => Math.min(prev + 1, totalPages)) - } - > - - - - - - )} + onPageChange={setCurrentPage} + /> + ); }; diff --git a/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx b/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx index ec4d39f91..7484dffea 100644 --- a/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx +++ b/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx @@ -6,15 +6,12 @@ import { Input, VStack, Box, - Pagination, - ButtonGroup, - IconButton, Link, Button, Checkbox, Badge, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft, Funnel, Search } from 'lucide-react'; +import { Funnel, Search } from 'lucide-react'; import { AlertStatus, FoodManufacturer } from '../types/types'; import { DonateWastedFood } from '../types/manufacturerEnums'; import ApiClient from '@api/apiClient'; @@ -22,6 +19,7 @@ import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; import { useNavigate } from 'react-router-dom'; import { ROUTES } from '../routes'; +import { PaginationControl } from '@components/pagination'; const AdminFoodManufacturerManagement: React.FC = () => { const navigate = useNavigate(); @@ -292,60 +290,12 @@ const AdminFoodManufacturerManagement: React.FC = () => { - setCurrentPage(page)} - > - - - - setCurrentPage((prev) => Math.max(prev - 1, 1)) - } - > - - - - - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> - - - - setCurrentPage((prev) => - Math.min( - prev + 1, - Math.ceil(filteredFMs.length / pageSize), - ), - ) - } - > - - - - - + onPageChange={setCurrentPage} + /> diff --git a/apps/frontend/src/containers/adminOrderManagement.tsx b/apps/frontend/src/containers/adminOrderManagement.tsx index 25940eaea..ea1d26f27 100644 --- a/apps/frontend/src/containers/adminOrderManagement.tsx +++ b/apps/frontend/src/containers/adminOrderManagement.tsx @@ -1,26 +1,15 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Box, Button, Table, Heading, - Pagination, - IconButton, VStack, - ButtonGroup, Checkbox, Input, Link, } from '@chakra-ui/react'; -import { - ArrowDownUp, - ChevronRight, - ChevronLeft, - Funnel, - Mail, - CircleCheck, - Search, -} from 'lucide-react'; +import { ArrowDownUp, Funnel, Mail, CircleCheck, Search } from 'lucide-react'; import { formatDate, getInitials, @@ -35,6 +24,7 @@ import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { ROUTES } from '../routes'; +import { PaginationControl } from '@components/pagination'; // Extending the OrderSummary type to include assignee color for display type OrderWithColor = OrderSummary & { assigneeColor?: string }; @@ -106,49 +96,49 @@ const AdminOrderManagement: React.FC = () => { const MAX_PER_STATUS = 5; - useEffect(() => { - // Fetch all orders on component mount and sorts them into their appropriate status lists - const fetchOrders = async () => { - try { - const data = await ApiClient.getAllOrders(); - - const grouped: Record = { - [OrderStatus.SHIPPED]: [], - [OrderStatus.PENDING]: [], - [OrderStatus.DELIVERED]: [], - [OrderStatus.CLOSED]: [], - }; - - for (const order of data) { - const status = order.status; - const orderWithColor: OrderWithColor = { ...order }; - - if (order.assignee) { - orderWithColor.assigneeColor = - USER_ICON_COLORS[order.assignee.id % USER_ICON_COLORS.length]; - } - - grouped[status].push(orderWithColor); + // Fetches all orders and sorts them into their appropriate status lists + const fetchOrders = useCallback(async () => { + try { + const data = await ApiClient.getAllOrders(); + + const grouped: Record = { + [OrderStatus.SHIPPED]: [], + [OrderStatus.PENDING]: [], + [OrderStatus.DELIVERED]: [], + [OrderStatus.CLOSED]: [], + }; + + for (const order of data) { + const status = order.status; + const orderWithColor: OrderWithColor = { ...order }; + + if (order.assignee) { + orderWithColor.assigneeColor = + USER_ICON_COLORS[order.assignee.id % USER_ICON_COLORS.length]; } - setStatusOrders(grouped); - - // Initialize current page for each status - const initialPages: Record = { - [OrderStatus.SHIPPED]: 1, - [OrderStatus.PENDING]: 1, - [OrderStatus.DELIVERED]: 1, - [OrderStatus.CLOSED]: 1, - }; - setCurrentPages(initialPages); - } catch { - setAlertMessage('Error fetching orders', AlertStatus.ERROR); + grouped[status].push(orderWithColor); } - }; - fetchOrders(); + setStatusOrders(grouped); + + // Initialize current page for each status + const initialPages: Record = { + [OrderStatus.SHIPPED]: 1, + [OrderStatus.PENDING]: 1, + [OrderStatus.DELIVERED]: 1, + [OrderStatus.CLOSED]: 1, + }; + setCurrentPages(initialPages); + } catch { + setAlertMessage('Error fetching orders', AlertStatus.ERROR); + } }, [setAlertMessage]); + useEffect(() => { + fetchOrders(); + }, [fetchOrders]); + // Helper to reset page for a specific status const resetPageForStatus = (status: OrderStatus) => { setCurrentPages((prev) => ({ ...prev, [status]: 1 })); @@ -188,9 +178,10 @@ const AdminOrderManagement: React.FC = () => { } } } else { + setAlertMessage('Order not found.', AlertStatus.ERROR); navigate(ROUTES.ADMIN_ORDER_MANAGEMENT, { replace: true }); } - }, [searchParams, statusOrders, navigate]); + }, [searchParams, statusOrders, navigate, setAlertMessage]); // Pre-fill pantry filter from url param and then clear the param. useEffect(() => { @@ -314,6 +305,7 @@ const AdminOrderManagement: React.FC = () => { setSelectedOrderId(null); navigate(ROUTES.ADMIN_ORDER_MANAGEMENT, { replace: true }); }} + onSuccess={fetchOrders} /> ); @@ -356,7 +348,6 @@ const OrderStatusSection: React.FC = ({ const [isSortOpen, setIsSortOpen] = useState(false); const MAX_PER_STATUS = 5; - const totalPages = Math.ceil(totalOrders / MAX_PER_STATUS); const handleFilterChange = (pantry: string, checked: boolean) => { const newSelected = checked @@ -650,251 +641,205 @@ const OrderStatusSection: React.FC = ({ - No Orders + {filterState.selectedPantries.length > 0 + ? 'No Matching Orders' + : 'No Orders'} - You have no {ORDER_STATUS_LABELS[status].toLowerCase()} orders - at this time. + {filterState.selectedPantries.length > 0 + ? 'No orders match the selected filter.' + : `You have no ${ORDER_STATUS_LABELS[ + status + ].toLowerCase()} orders at this time.`} ) : ( <> - - - - - Order # - - - Status - - - Assignee - - - Pantry - - - Dates - - - Action Required - - - - - {orders.map((order, index) => { - const pantry = order.request.pantry; - - return ( - + + + + + Order # + + + Status + + + Assignee + + - + + Dates + + + Action Required + + + + + {orders.map((order, index) => { + const pantry = order.request.pantry; + + return ( + - onOrderSelect(order.orderId)} + - {order.orderId} - - - - onOrderSelect(order.orderId)} + > + {order.orderId} + + + - {ORDER_STATUS_LABELS[order.status]} - - - - + {ORDER_STATUS_LABELS[order.status]} + + + - {order.assignee ? ( - - {getInitials( - order.assignee.firstName, - order.assignee.lastName, - )} - - ) : ( - - — - - )} - - - - {pantry.pantryName} - - - {formatDate(order.createdAt)}- - {order.deliveredAt && formatDate(order.deliveredAt)} - - - - ); - })} - - - - {totalPages > 1 && ( - - onPageChange(e.page)} - > - - - - - - ( - + {order.assignee ? ( + + {getInitials( + order.assignee.firstName, + order.assignee.lastName, + )} + + ) : ( + + — + + )} + + + - {page.value} - - )} - /> + {pantry.pantryName} + + + {formatDate(order.createdAt)}- + {order.deliveredAt && formatDate(order.deliveredAt)} + + + + ); + })} + + + - - - - - - - )} + + + )} diff --git a/apps/frontend/src/containers/adminPantryManagement.tsx b/apps/frontend/src/containers/adminPantryManagement.tsx index a6baea579..779ef3d6a 100644 --- a/apps/frontend/src/containers/adminPantryManagement.tsx +++ b/apps/frontend/src/containers/adminPantryManagement.tsx @@ -6,15 +6,12 @@ import { Input, VStack, Box, - Pagination, - ButtonGroup, - IconButton, Link, Button, Checkbox, Badge, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft, Funnel, Search } from 'lucide-react'; +import { Funnel, Search } from 'lucide-react'; import { AlertStatus, ApprovedPantryResponse } from '../types/types'; import ApiClient from '@api/apiClient'; import { FloatingAlert } from '@components/floatingAlert'; @@ -24,6 +21,7 @@ import { RefrigeratedDonation } from '../types/pantryEnums'; import AssignVolunteersModal from '@components/forms/assignVolunteersModal'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { ROUTES } from '../routes'; +import { PaginationControl } from '@components/pagination'; const AdminPantryManagement: React.FC = () => { const navigate = useNavigate(); @@ -259,238 +257,218 @@ const AdminPantryManagement: React.FC = () => { )} - - - - - Pantry - - - Assignee - - - Refrigerator-Friendly - - - Action - - - - - {paginatedPantries?.map((pantry) => ( - - - - navigate( - ROUTES.PANTRY_MANAGEMENT_DETAILS.replace( - ':pantryId', - pantry.pantryId.toString(), - ), - ) - } + + {selectedPantries.length > 0 + ? 'No Matching Pantries' + : 'No Pantries'} + + + {selectedPantries.length > 0 + ? 'No pantries match the selected filter.' + : 'You have no pantries at this time.'} + + + ) : ( + + + + + Pantry + + + Assignee + + + Refrigerator-Friendly + + + Action + + + + + {paginatedPantries?.map((pantry) => ( + + + + navigate( + ROUTES.PANTRY_MANAGEMENT_DETAILS.replace( + ':pantryId', + pantry.pantryId.toString(), + ), + ) + } + > + {pantry.pantryName} + + + setSelectedPantryToAssignVolunteers(pantry)} + cursor="pointer" + _hover={{ bg: 'gray.50' }} > - {pantry.pantryName} - - - setSelectedPantryToAssignVolunteers(pantry)} - cursor="pointer" - _hover={{ bg: 'gray.50' }} - > - - {pantry.volunteers && pantry.volunteers.length > 0 ? ( - (() => { - const volunteers = pantry.volunteers.filter( - (volunteer) => volunteer.active, - ); - const maxVisible = 3; + + {pantry.volunteers && + pantry.volunteers.some( + (volunteer) => volunteer.active, + ) ? ( + (() => { + const volunteers = pantry.volunteers.filter( + (volunteer) => volunteer.active, + ); + const maxVisible = 3; - const hasOverflow = volunteers.length > maxVisible; - const visibleVolunteers = hasOverflow - ? volunteers.slice(0, maxVisible - 1) - : volunteers; + const hasOverflow = volunteers.length > maxVisible; + const visibleVolunteers = hasOverflow + ? volunteers.slice(0, maxVisible - 1) + : volunteers; - const remainingCount = - volunteers.length - (maxVisible - 1); + const remainingCount = + volunteers.length - (maxVisible - 1); - return ( - <> - {visibleVolunteers.map((volunteer, index) => ( - - {getInitials( - volunteer.firstName, - volunteer.lastName, - )} - - ))} + return ( + <> + {visibleVolunteers.map((volunteer, index) => ( + + {getInitials( + volunteer.firstName, + volunteer.lastName, + )} + + ))} - {hasOverflow && ( - - +{remainingCount} - - )} - - ); - })() - ) : ( - - No Volunteer - - )} - - - - - {pantry.refrigeratedDonation === RefrigeratedDonation.NO - ? 'Not Refrigerator-Friendly' - : 'Refrigerator/Freezer-Friendly'} - - - - - navigate( - `${ROUTES.ADMIN_ORDER_MANAGEMENT}?pantryId=${pantry.pantryId}`, - ) - } - > - View Orders - - - - ))} - {selectedPantryToAssignVolunteers && ( - setSelectedPantryToAssignVolunteers(null)} - onSuccess={handleAssignVolunteersSuccess} - isOpen={true} - /> - )} - - + {hasOverflow && ( + + +{remainingCount} + + )} + + ); + })() + ) : ( + + No Volunteer + + )} + + + + + {pantry.refrigeratedDonation === RefrigeratedDonation.NO + ? 'Not Refrigerator-Friendly' + : 'Refrigerator/Freezer-Friendly'} + + + + + navigate( + `${ROUTES.ADMIN_ORDER_MANAGEMENT}?pantryId=${pantry.pantryId}`, + ) + } + > + View Orders + + + + ))} + {selectedPantryToAssignVolunteers && ( + setSelectedPantryToAssignVolunteers(null)} + onSuccess={handleAssignVolunteersSuccess} + isOpen={true} + /> + )} + + + )} - setCurrentPage(page)} - > - - - - setCurrentPage((prev) => Math.max(prev - 1, 1)) - } - > - - - - - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> - - - - setCurrentPage((prev) => - Math.min( - prev + 1, - Math.ceil(filteredPantries.length / pageSize), - ), - ) - } - > - - - - - + onPageChange={setCurrentPage} + /> diff --git a/apps/frontend/src/containers/approveFoodManufacturers.tsx b/apps/frontend/src/containers/approveFoodManufacturers.tsx index 0bffd7529..b464237f2 100644 --- a/apps/frontend/src/containers/approveFoodManufacturers.tsx +++ b/apps/frontend/src/containers/approveFoodManufacturers.tsx @@ -7,22 +7,14 @@ import { Heading, VStack, Checkbox, - Pagination, - ButtonGroup, - IconButton, Link, } from '@chakra-ui/react'; import ApiClient from '@api/apiClient'; import { AlertStatus, FoodManufacturer } from '../types/types'; -import { - ArrowDownUp, - ChevronLeft, - ChevronRight, - CircleCheck, - Funnel, -} from 'lucide-react'; +import { ArrowDownUp, CircleCheck, CircleX, Funnel } from 'lucide-react'; import { useAlert } from '../hooks/alert'; import { FloatingAlert } from '@components/floatingAlert'; +import { PaginationControl } from '@components/pagination'; import { ROUTES } from '../routes'; const ApproveFoodManufacturers: React.FC = () => { @@ -30,6 +22,7 @@ const ApproveFoodManufacturers: React.FC = () => { const [foodManufacturers, setFoodManufacturers] = useState< FoodManufacturer[] >([]); + const [hasError, setHasError] = useState(false); const [sortAsc, setSortAsc] = useState(false); const [selectedFoodManufacturers, setSelectedFoodManufacturers] = useState< string[] @@ -44,7 +37,9 @@ const ApproveFoodManufacturers: React.FC = () => { try { const data = await ApiClient.getAllPendingFoodManufacturers(); setFoodManufacturers(data); + setHasError(false); } catch { + setHasError(true); setAlertMessage('Error fetching food manufacturers', AlertStatus.ERROR); } }; @@ -91,7 +86,6 @@ const ApproveFoodManufacturers: React.FC = () => { ); const itemsPerPage = 10; - const totalPages = Math.ceil(filteredFoodManufacturers.length / itemsPerPage); const paginatedFoodManufacturers = filteredFoodManufacturers.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage, @@ -134,7 +128,7 @@ const ApproveFoodManufacturers: React.FC = () => { timeout={6000} /> )} - {foodManufacturers.length === 0 ? ( + {!hasError && foodManufacturers.length === 0 ? ( { There are no applications to review at this time + ) : hasError ? ( + + + + + + Unable to Load Applications + + + Something went wrong while loading applications. Please try again + later. + + ) : ( @@ -242,148 +261,142 @@ const ApproveFoodManufacturers: React.FC = () => { Sort - - - - - Application # - - - Food Manufacturer - - - Date Applied - - - Actions - - - - - {paginatedFoodManufacturers.map((foodManufacturer, index) => ( - - + + + + + No Matching Applications + + + No applications match the selected filter. + + + ) : ( + + + + - {foodManufacturer.foodManufacturerId} - - + - {foodManufacturer.foodManufacturerName} - - + - {new Date(foodManufacturer.dateApplied).toLocaleDateString( - 'en-US', - { + Date Applied + + + Actions + + + + + {paginatedFoodManufacturers.map((foodManufacturer, index) => ( + + + {foodManufacturer.foodManufacturerId} + + + {foodManufacturer.foodManufacturerName} + + + {new Date( + foodManufacturer.dateApplied, + ).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric', - }, - )} - - - + - View Details - - - - ))} - - + { + e.preventDefault(); + navigate( + ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace( + ':applicationId', + String(foodManufacturer.foodManufacturerId), + ), + ); + }} + > + View Details + + + + ))} + + + )} - {totalPages > 1 && ( - + setCurrentPage(e.page)} - > - - - - - - ( - - {page.value} - - )} - /> - - - - - - - )} + onPageChange={setCurrentPage} + /> + )} diff --git a/apps/frontend/src/containers/approvePantries.tsx b/apps/frontend/src/containers/approvePantries.tsx index 6fcca178f..6653c3146 100644 --- a/apps/frontend/src/containers/approvePantries.tsx +++ b/apps/frontend/src/containers/approvePantries.tsx @@ -7,27 +7,20 @@ import { Heading, VStack, Checkbox, - Pagination, - ButtonGroup, - IconButton, Link, } from '@chakra-ui/react'; import ApiClient from '@api/apiClient'; import { AlertStatus, Pantry } from '../types/types'; -import { - ArrowDownUp, - ChevronLeft, - ChevronRight, - CircleCheck, - Funnel, -} from 'lucide-react'; +import { ArrowDownUp, CircleCheck, CircleX, Funnel } from 'lucide-react'; import { useAlert } from '../hooks/alert'; import { FloatingAlert } from '@components/floatingAlert'; +import { PaginationControl } from '@components/pagination'; import { ROUTES } from '../routes'; const ApprovePantries: React.FC = () => { const navigate = useNavigate(); const [pantries, setPantries] = useState([]); + const [hasError, setHasError] = useState(false); const [sortAsc, setSortAsc] = useState(false); const [selectedPantries, setSelectedPantries] = useState([]); const [currentPage, setCurrentPage] = useState(1); @@ -40,7 +33,9 @@ const ApprovePantries: React.FC = () => { try { const data = await ApiClient.getAllPendingPantries(); setPantries(data); + setHasError(false); } catch { + setHasError(true); setAlertMessage('Error fetching pantries', AlertStatus.ERROR); } }; @@ -82,7 +77,6 @@ const ApprovePantries: React.FC = () => { ); const itemsPerPage = 10; - const totalPages = Math.ceil(filteredPantries.length / itemsPerPage); const paginatedPantries = filteredPantries.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage, @@ -125,7 +119,7 @@ const ApprovePantries: React.FC = () => { timeout={6000} /> )} - {pantries.length === 0 ? ( + {!hasError && pantries.length === 0 ? ( { There are no applications to review at this time + ) : hasError ? ( + + + + + + Unable to Load Applications + + + Something went wrong while loading applications. Please try again + later. + + ) : ( @@ -314,6 +333,15 @@ const ApprovePantries: React.FC = () => { ':applicationId', pantry.pantryId.toString(), )} + onClick={(event) => { + event.preventDefault(); + navigate( + ROUTES.PANTRY_APPLICATION_DETAILS.replace( + ':applicationId', + pantry.pantryId.toString(), + ), + ); + }} > View Details @@ -323,55 +351,14 @@ const ApprovePantries: React.FC = () => { - {totalPages > 1 && ( - + setCurrentPage(e.page)} - > - - - - - - ( - - {page.value} - - )} - /> - - - - - - - )} + onPageChange={setCurrentPage} + /> + )} diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index 078d2a685..0af1a6626 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -1,16 +1,6 @@ import ApiClient from '@api/apiClient'; -import { - Box, - Button, - ButtonGroup, - Flex, - Heading, - IconButton, - Link, - Pagination, - Table, -} from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft, Mail } from 'lucide-react'; +import { Box, Button, Flex, Heading, Link, Table } from '@chakra-ui/react'; +import { Mail } from 'lucide-react'; import { capitalize, formatDate, DONATION_STATUS_COLORS } from '@utils/utils'; import { AlertStatus, @@ -29,6 +19,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAlert } from '../hooks/alert'; import FMDeleteDonationActionModal from '@components/forms/fmDeleteDonationModal'; import { ROUTES } from '../routes'; +import { PaginationControl } from '@components/pagination'; const MAX_PER_STATUS = 5; @@ -385,8 +376,6 @@ const DonationStatusSection: React.FC = ({ onActionSelect, showManufacturer, }) => { - const totalPages = Math.ceil(totalDonations / MAX_PER_STATUS); - const tableHeaderStyles = { borderBottom: '1px solid', borderColor: 'neutral.100', @@ -559,65 +548,14 @@ const DonationStatusSection: React.FC = ({ - {totalPages > 1 && ( - - onPageChange(e.page)} - > - - - - - - ( - - {page.value} - - )} - /> - - - - - - - - )} + + + )} diff --git a/apps/frontend/src/containers/formRequests.tsx b/apps/frontend/src/containers/formRequests.tsx index ead737050..81880c9b2 100644 --- a/apps/frontend/src/containers/formRequests.tsx +++ b/apps/frontend/src/containers/formRequests.tsx @@ -8,12 +8,8 @@ import { useDisclosure, Link, Badge, - Pagination, - ButtonGroup, - IconButton, Flex, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft } from 'lucide-react'; import FoodRequestFormModal from '@components/forms/requestFormModal'; import { FoodRequestStatus, FoodRequestSummaryDto } from '../types/types'; import RequestDetailsModal from '@components/forms/requestDetailsModal'; @@ -25,6 +21,7 @@ import { useAlert } from '../hooks/alert'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { ROUTES } from '../routes'; import { AlertStatus } from '../types/types'; +import { PaginationControl } from '@components/pagination'; const FormRequests: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); @@ -262,50 +259,12 @@ const FormRequests: React.FC = () => { /> )} - setCurrentPage(page)} - > - - - setCurrentPage((prev) => Math.max(prev - 1, 1))} - > - - - - - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> - - - - setCurrentPage((prev) => - Math.min(prev + 1, Math.ceil(requests.length / pageSize)), - ) - } - > - - - - - + onPageChange={setCurrentPage} + /> ); diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx deleted file mode 100644 index 0e9e931c7..000000000 --- a/apps/frontend/src/containers/homepage.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import React from 'react'; -import { Link as RouterLink } from 'react-router-dom'; -import { ROUTES } from '../routes'; -import { - Box, - Container, - Heading, - VStack, - List, - ListItem, - Link, - Text, - Alert, -} from '@chakra-ui/react'; -import { useAuthenticator } from '@aws-amplify/ui-react'; - -const Homepage: React.FC = () => { - const { authStatus } = useAuthenticator((context) => [context.authStatus]); - - return ( - - - - Site Navigation - - - - - Profile View - - - - - Pantry View - - - - - Dashboard - - - - - Request Form - - - - - - Pantry Application - - - - - - - Order Management - - - - - - - - - Food Manufacturer View - - - - - - Donation Management - - - - - - - Food Manufacturer Application - - - - - - - Food Manufacturer Dashboard - - - - - - - - - Volunteer View - - - - - - Dashboard - - - - - - - Assigned Pantries - - - - - - - Food Request Management - - - - - - - Order Management - - - - - - - - - Admin View - - - - - - Approve Pantries - - - - - - - Approve Food Manufacturers - - - - - - - User Management - - - - - - - Donation Management - - - - - - - Donation Statistics - - - - - - - Order Management - - - - - - - Pantry Management - - - - - - Dashboard - - - - - - Food Request Management - - - - - - - {authStatus !== 'authenticated' && ( - - - Other Pages - - - - - Login - - - - - Sign Up - - - - - )} - - - - - - - Note: This is a temporary navigation page for - development purposes. - - - - - - - - ); -}; - -export default Homepage; diff --git a/apps/frontend/src/containers/loginPage.tsx b/apps/frontend/src/containers/loginPage.tsx index dfcd36602..5a37dc4b3 100644 --- a/apps/frontend/src/containers/loginPage.tsx +++ b/apps/frontend/src/containers/loginPage.tsx @@ -37,7 +37,7 @@ const LoginPage: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); - const from = location.state?.from?.pathname || ROUTES.HOME; + const from = location.state?.from?.pathname || ROUTES.PROFILE; useEffect(() => { if (authStatus === 'authenticated') { diff --git a/apps/frontend/src/containers/pantryOrderManagement.tsx b/apps/frontend/src/containers/pantryOrderManagement.tsx index 1efd6fc71..95daa2a82 100644 --- a/apps/frontend/src/containers/pantryOrderManagement.tsx +++ b/apps/frontend/src/containers/pantryOrderManagement.tsx @@ -1,21 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { - Box, - Button, - Table, - Heading, - Pagination, - IconButton, - VStack, - ButtonGroup, -} from '@chakra-ui/react'; -import { - ArrowDownUp, - ChevronRight, - ChevronLeft, - Mail, - CircleCheck, -} from 'lucide-react'; +import { Box, Button, Table, Heading, VStack } from '@chakra-ui/react'; +import { ArrowDownUp, Mail, CircleCheck } from 'lucide-react'; import { formatDate, getInitials, @@ -31,6 +16,7 @@ import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { ROUTES } from '../routes'; +import { PaginationControl } from '@components/pagination'; type OrderWithColor = OrderSummary & { assigneeColor?: string }; const MAX_PER_STATUS = 5; @@ -301,8 +287,6 @@ const OrderStatusSection: React.FC = ({ }) => { const [isSortOpen, setIsSortOpen] = useState(false); - const totalPages = Math.ceil(totalOrders / MAX_PER_STATUS); - const tableHeaderStyles = { borderBottom: '1px solid', borderColor: 'neutral.100', @@ -630,65 +614,14 @@ const OrderStatusSection: React.FC = ({ - {totalPages > 1 && ( - - onPageChange(e.page)} - > - - - - - - ( - - {page.value} - - )} - /> - - - - - - - - )} + + + )} diff --git a/apps/frontend/src/containers/userManagement.tsx b/apps/frontend/src/containers/userManagement.tsx index 104df7eac..c6034175f 100644 --- a/apps/frontend/src/containers/userManagement.tsx +++ b/apps/frontend/src/containers/userManagement.tsx @@ -10,19 +10,12 @@ import { Box, Badge, InputGroup, - Pagination, - ButtonGroup, IconButton, Link, Menu, Portal, } from '@chakra-ui/react'; -import { - SearchIcon, - ChevronRight, - ChevronLeft, - EllipsisVertical, -} from 'lucide-react'; +import { SearchIcon, EllipsisVertical } from 'lucide-react'; import { AlertStatus, Role, User } from '../types/types'; import ApiClient from '@api/apiClient'; import NewVolunteerModal from '@components/forms/addNewVolunteerModal'; @@ -31,6 +24,7 @@ import ConfirmActionModal from '@components/forms/confirmActionModal'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; import { getInitials, USER_ICON_COLORS } from '@utils/utils'; +import { PaginationControl } from '@components/pagination'; const VolunteerManagement: React.FC = () => { const navigate = useNavigate(); @@ -219,7 +213,7 @@ const VolunteerManagement: React.FC = () => { {paginatedVolunteers?.map((volunteer) => ( - + { {getInitials(volunteer.firstName, volunteer.lastName)} {volunteer.firstName} {volunteer.lastName} + {volunteer.role === Role.ADMIN && ( + + Admin + + )} @@ -322,57 +333,12 @@ const VolunteerManagement: React.FC = () => { - setCurrentPage(page)} - > - - - - setCurrentPage((prev) => Math.max(prev - 1, 1)) - } - > - - - - - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> - - - - setCurrentPage((prev) => - Math.min( - prev + 1, - Math.ceil(filteredVolunteers.length / pageSize), - ), - ) - } - > - - - - - + onPageChange={setCurrentPage} + /> diff --git a/apps/frontend/src/containers/volunteerOrderManagement.tsx b/apps/frontend/src/containers/volunteerOrderManagement.tsx index ff193db2a..f396a2a2b 100644 --- a/apps/frontend/src/containers/volunteerOrderManagement.tsx +++ b/apps/frontend/src/containers/volunteerOrderManagement.tsx @@ -4,24 +4,13 @@ import { Button, Table, Heading, - Pagination, - IconButton, VStack, - ButtonGroup, Checkbox, Input, Link, Spinner, } from '@chakra-ui/react'; -import { - ArrowDownUp, - ChevronRight, - ChevronLeft, - Funnel, - Mail, - CircleCheck, - Search, -} from 'lucide-react'; +import { ArrowDownUp, Funnel, Mail, CircleCheck, Search } from 'lucide-react'; import { formatDate, getInitials, @@ -45,6 +34,7 @@ import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { ROUTES } from '../routes'; +import { PaginationControl } from '@components/pagination'; type VolunteerOrderWithColor = VolunteerOrder & { assigneeColor?: string }; @@ -447,7 +437,6 @@ const OrderStatusSection: React.FC = ({ const [isSortOpen, setIsSortOpen] = useState(false); const MAX_PER_STATUS = 5; - const totalPages = Math.ceil(totalOrders / MAX_PER_STATUS); const handleFilterChange = (pantry: string, checked: boolean) => { const newSelected = checked @@ -921,66 +910,14 @@ const OrderStatusSection: React.FC = ({ - {totalPages > 1 && ( - - onPageChange(e.page)} - > - - - - - - ( - - {page.value} - - )} - /> - - - - - - - - )} + + + )}