diff --git a/apps/frontend/src/components/dashboardCard.tsx b/apps/frontend/src/components/dashboardCard.tsx index 140c0f4aa..5eaa9b74e 100644 --- a/apps/frontend/src/components/dashboardCard.tsx +++ b/apps/frontend/src/components/dashboardCard.tsx @@ -30,7 +30,7 @@ export const CARD_TYPE_ICON: Record = { const CARD_TYPE_DATE_LABEL: Record = { [DashboardCardType.ACTION]: 'Applied', [DashboardCardType.ORDER]: 'Requested', - [DashboardCardType.UPCOMING_DONATION]: 'Scheduled', + [DashboardCardType.UPCOMING_DONATION]: 'Scheduled to send', [DashboardCardType.RECENT_DONATION]: 'Donated', [DashboardCardType.FOOD_REQUEST]: 'Requested', }; diff --git a/apps/frontend/src/components/forms/addNewVolunteerModal.tsx b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx index aa146fd76..bb72d2ec9 100644 --- a/apps/frontend/src/components/forms/addNewVolunteerModal.tsx +++ b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx @@ -109,6 +109,8 @@ const NewVolunteerModal: React.FC = ({ return ( e.open ? setIsOpen(true) : closeAndReset() diff --git a/apps/frontend/src/components/forms/assignVolunteersModal.tsx b/apps/frontend/src/components/forms/assignVolunteersModal.tsx index 120fab770..c8e1e5693 100644 --- a/apps/frontend/src/components/forms/assignVolunteersModal.tsx +++ b/apps/frontend/src/components/forms/assignVolunteersModal.tsx @@ -131,6 +131,8 @@ const AssignVolunteersModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/changePasswordModal.tsx b/apps/frontend/src/components/forms/changePasswordModal.tsx index c74a34012..295fc6f01 100644 --- a/apps/frontend/src/components/forms/changePasswordModal.tsx +++ b/apps/frontend/src/components/forms/changePasswordModal.tsx @@ -103,6 +103,8 @@ const ChangePasswordModal: React.FC = ({ return ( { if (!e.open) { diff --git a/apps/frontend/src/components/forms/completeRequiredActionsModal.tsx b/apps/frontend/src/components/forms/completeRequiredActionsModal.tsx index ab85d4c95..39ccb88e3 100644 --- a/apps/frontend/src/components/forms/completeRequiredActionsModal.tsx +++ b/apps/frontend/src/components/forms/completeRequiredActionsModal.tsx @@ -64,6 +64,8 @@ const CompleteRequiredActionsModal: React.FC< return ( { diff --git a/apps/frontend/src/components/forms/confirmActionModal.tsx b/apps/frontend/src/components/forms/confirmActionModal.tsx index 2bebeb0d7..ee99a64ea 100644 --- a/apps/frontend/src/components/forms/confirmActionModal.tsx +++ b/apps/frontend/src/components/forms/confirmActionModal.tsx @@ -20,6 +20,8 @@ const ConfirmActionModal: React.FC = ({ return ( !e.open && onClose()} > diff --git a/apps/frontend/src/components/forms/confirmFoodManufacturerDecisionModal.tsx b/apps/frontend/src/components/forms/confirmFoodManufacturerDecisionModal.tsx index b4bdbba66..b5094a2b1 100644 --- a/apps/frontend/src/components/forms/confirmFoodManufacturerDecisionModal.tsx +++ b/apps/frontend/src/components/forms/confirmFoodManufacturerDecisionModal.tsx @@ -24,6 +24,8 @@ const ConfirmFoodManufacturerDecisionModal: React.FC< useModalBodyCleanup(); return ( !e.open && onClose()} > diff --git a/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx b/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx index e9dd0e073..8f4a26d58 100644 --- a/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx +++ b/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx @@ -22,6 +22,8 @@ const ConfirmPantryDecisionModal: React.FC = ({ useModalBodyCleanup(); return ( !e.open && onClose()} > diff --git a/apps/frontend/src/components/forms/createNewOrderModal.tsx b/apps/frontend/src/components/forms/createNewOrderModal.tsx index b9a1d3d8c..c2fe9b6f8 100644 --- a/apps/frontend/src/components/forms/createNewOrderModal.tsx +++ b/apps/frontend/src/components/forms/createNewOrderModal.tsx @@ -166,6 +166,8 @@ const CreateNewOrderModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/donationDetailsModal.tsx b/apps/frontend/src/components/forms/donationDetailsModal.tsx index 241fb8774..cef00b984 100644 --- a/apps/frontend/src/components/forms/donationDetailsModal.tsx +++ b/apps/frontend/src/components/forms/donationDetailsModal.tsx @@ -115,6 +115,8 @@ const DonationDetailsModal: React.FC = ({ return ( { if (!e.open) { diff --git a/apps/frontend/src/components/forms/editableFMApplication.tsx b/apps/frontend/src/components/forms/editableFMApplication.tsx index 835500242..f11a7595f 100644 --- a/apps/frontend/src/components/forms/editableFMApplication.tsx +++ b/apps/frontend/src/components/forms/editableFMApplication.tsx @@ -116,7 +116,7 @@ const EditableFMApplication: React.FC = ({ AlertStatus.ERROR, ); } - }, [foodManufacturerId]); + }, [foodManufacturerId, setAlertMessage]); useEffect(() => { // Fetch the application when we don't have one loaded already diff --git a/apps/frontend/src/components/forms/editablePantryApplication.tsx b/apps/frontend/src/components/forms/editablePantryApplication.tsx index 31c79325b..ba6905574 100644 --- a/apps/frontend/src/components/forms/editablePantryApplication.tsx +++ b/apps/frontend/src/components/forms/editablePantryApplication.tsx @@ -291,7 +291,7 @@ const EditablePantryApplication: React.FC = ({ } finally { setIsLoading(false); } - }, []); + }, [setAlertMessage]); useEffect(() => { if (!initialApplication) { diff --git a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx index 760331583..824bc71f8 100644 --- a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx +++ b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx @@ -37,7 +37,7 @@ interface FmCompleteRequiredActionsModalProps { donation: DonationDetails; isOpen: boolean; onClose: () => void; - onSuccess: () => void; + onSuccess: (allOrdersComplete: boolean) => void; } interface OrderFormData { @@ -160,16 +160,16 @@ const FmCompleteRequiredActionsModal: React.FC< const [isSubmitting, setIsSubmitting] = useState(false); const [alertState, setAlertMessage] = useAlert(); - // True once every relevant item has both ozPerItem and estimatedValue filled in + // True once every relevant item has both ozPerItem and estimatedValue set to at least 0.01 const isSubmitEnabled = useMemo( () => donation.relevantDonationItems.length > 0 && donation.relevantDonationItems.every( (item) => - itemFormData[item.itemId].ozPerItem !== '' && - itemFormData[item.itemId].estimatedValue !== '', + parseFloat(itemFormData[item.itemId].ozPerItem) >= 0.01 && + parseFloat(itemFormData[item.itemId].estimatedValue) >= 0.01, ), - [itemFormData], + [itemFormData, donation.relevantDonationItems], ); // The order currently shown in the shipping stage based on the current page @@ -284,7 +284,13 @@ const FmCompleteRequiredActionsModal: React.FC< }); } - onSuccess(); + // Whether every pending order now has both a shipping cost and a tracking + // link set, i.e. will flip to SHIPPED once this save lands on the backend + const allOrdersComplete = orders.every((order) => { + const { trackingLink, shippingCost } = orderFormData[order.orderId]; + return trackingLink.trim() !== '' && shippingCost !== ''; + }); + onSuccess(allOrdersComplete); } catch (error) { const rawMsg = axios.isAxiosError(error) && error.response?.data?.message; const msg = Array.isArray(rawMsg) ? rawMsg[0] : rawMsg; @@ -312,6 +318,8 @@ const FmCompleteRequiredActionsModal: React.FC< return ( { diff --git a/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx b/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx index da85d57a6..5b27733c0 100644 --- a/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx +++ b/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { Box, Button, @@ -29,24 +29,30 @@ const FMDeleteDonationActionModal: React.FC< > = ({ donation, isOpen, onClose, onSuccess }) => { useModalBodyCleanup(); const [alertState, setAlertMessage] = useAlert(); + const [isDeleting, setIsDeleting] = useState(false); const donationRef = useRef(donation); if (donation) donationRef.current = donation; const displayDonation = donation ?? donationRef.current; const onDeleteDonation = async () => { - if (!donation) return; + if (!donation || isDeleting) return; + setIsDeleting(true); try { await apiClient.deleteDonation(donation.donationId); onClose(); onSuccess(); } catch { setAlertMessage('Donation could not be deleted.', AlertStatus.ERROR); + } finally { + setIsDeleting(false); } }; return ( { @@ -107,6 +113,7 @@ const FMDeleteDonationActionModal: React.FC< textAlign="center" lineHeight="28px" onClick={onClose} + disabled={isDeleting} > Cancel @@ -121,6 +128,8 @@ const FMDeleteDonationActionModal: React.FC< flexShrink={0} textAlign="center" onClick={onDeleteDonation} + loading={isDeleting} + disabled={isDeleting} > Delete diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index d79f6fda3..e696b9d44 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -99,6 +99,8 @@ const NewDonationFormModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/orderDetailsModal.tsx b/apps/frontend/src/components/forms/orderDetailsModal.tsx index 3b85143ab..e3c6f3fdb 100644 --- a/apps/frontend/src/components/forms/orderDetailsModal.tsx +++ b/apps/frontend/src/components/forms/orderDetailsModal.tsx @@ -191,6 +191,8 @@ const OrderDetailsModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/orderReceivedActionModal.tsx b/apps/frontend/src/components/forms/orderReceivedActionModal.tsx index 00fe75319..02658cf43 100644 --- a/apps/frontend/src/components/forms/orderReceivedActionModal.tsx +++ b/apps/frontend/src/components/forms/orderReceivedActionModal.tsx @@ -90,6 +90,8 @@ const OrderReceivedActionModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/pantryApplicationModal.tsx b/apps/frontend/src/components/forms/pantryApplicationModal.tsx index 2c5cad669..a5256752a 100644 --- a/apps/frontend/src/components/forms/pantryApplicationModal.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationModal.tsx @@ -19,6 +19,8 @@ const PantryApplicationModal: React.FC = ({ const pantryUser = pantry.pantryUser; return ( { if (!e.open) onClose(); diff --git a/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx b/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx index 14a8ac8be..e8fd2df2c 100644 --- a/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx +++ b/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx @@ -40,6 +40,8 @@ const PantryDeleteRequestActionModal: React.FC< return ( { diff --git a/apps/frontend/src/components/forms/promoteVolunteerModal.tsx b/apps/frontend/src/components/forms/promoteVolunteerModal.tsx index 53d574391..2717cd8dc 100644 --- a/apps/frontend/src/components/forms/promoteVolunteerModal.tsx +++ b/apps/frontend/src/components/forms/promoteVolunteerModal.tsx @@ -18,6 +18,8 @@ const PromoteVolunteerModal: React.FC = ({ return ( !e.open && onClose()} > diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index d805c00ed..1504af70b 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -188,6 +188,8 @@ const RequestDetailsModal: React.FC = ({ /> )} { diff --git a/apps/frontend/src/components/forms/requestFormModal.tsx b/apps/frontend/src/components/forms/requestFormModal.tsx index e27af19b6..855aec6bd 100644 --- a/apps/frontend/src/components/forms/requestFormModal.tsx +++ b/apps/frontend/src/components/forms/requestFormModal.tsx @@ -114,6 +114,8 @@ const FoodRequestFormModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/resubmitDonationModal.tsx b/apps/frontend/src/components/forms/resubmitDonationModal.tsx index 1fc49e2c8..835b23e71 100644 --- a/apps/frontend/src/components/forms/resubmitDonationModal.tsx +++ b/apps/frontend/src/components/forms/resubmitDonationModal.tsx @@ -85,6 +85,15 @@ const ResubmitDonationModal: React.FC = ({ [setAlertMessage], ); + const handleSelect = useCallback( + (donationId: number) => { + setSelectedDonationId(donationId); + fetchItemsForDonation(donationId); + onSelect(donationId); + }, + [fetchItemsForDonation, onSelect], + ); + useEffect(() => { if ( isOpen && @@ -93,13 +102,7 @@ const ResubmitDonationModal: React.FC = ({ ) { handleSelect(initialDonationId); } - }, [isOpen, initialDonationId, selectedDonationId, fetchItemsForDonation]); - - const handleSelect = (donationId: number) => { - setSelectedDonationId(donationId); - fetchItemsForDonation(donationId); - onSelect(donationId); - }; + }, [isOpen, initialDonationId, selectedDonationId, handleSelect]); const handleClose = () => { setSelectedDonationId(null); @@ -139,6 +142,8 @@ const ResubmitDonationModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/volunteerCloseOrderModal.tsx b/apps/frontend/src/components/forms/volunteerCloseOrderModal.tsx index ff7934092..3491bd03c 100644 --- a/apps/frontend/src/components/forms/volunteerCloseOrderModal.tsx +++ b/apps/frontend/src/components/forms/volunteerCloseOrderModal.tsx @@ -43,6 +43,8 @@ const VolunteerCloseOrderModal: React.FC = ({ return ( { diff --git a/apps/frontend/src/components/forms/volunteerCloseRequestModal.tsx b/apps/frontend/src/components/forms/volunteerCloseRequestModal.tsx index 9057b2853..693eab2c7 100644 --- a/apps/frontend/src/components/forms/volunteerCloseRequestModal.tsx +++ b/apps/frontend/src/components/forms/volunteerCloseRequestModal.tsx @@ -43,6 +43,8 @@ const VolunteerCloseRequestActionModal: React.FC< return ( { diff --git a/apps/frontend/src/components/forms/volunteerRequestActionRequiredModal.tsx b/apps/frontend/src/components/forms/volunteerRequestActionRequiredModal.tsx index 39733d811..8fa6f239b 100644 --- a/apps/frontend/src/components/forms/volunteerRequestActionRequiredModal.tsx +++ b/apps/frontend/src/components/forms/volunteerRequestActionRequiredModal.tsx @@ -31,6 +31,8 @@ const VolunteerRequestActionRequiredModal: React.FC< return ( { diff --git a/apps/frontend/src/components/sectionEmptyState.tsx b/apps/frontend/src/components/sectionEmptyState.tsx index 96a91773c..1dedd1680 100644 --- a/apps/frontend/src/components/sectionEmptyState.tsx +++ b/apps/frontend/src/components/sectionEmptyState.tsx @@ -17,9 +17,6 @@ const SectionEmptyState: React.FC = ({ entity, subtitle }) => { py={10} gap={2} > - - Nothing to see here! - {message} diff --git a/apps/frontend/src/containers/adminDashboard.tsx b/apps/frontend/src/containers/adminDashboard.tsx index a1a0e3b81..fe7779522 100644 --- a/apps/frontend/src/containers/adminDashboard.tsx +++ b/apps/frontend/src/containers/adminDashboard.tsx @@ -1,5 +1,5 @@ import ApiClient from '@api/apiClient'; -import { Box, Heading, Text } from '@chakra-ui/react'; +import { Box, Button, Heading, Text } from '@chakra-ui/react'; import DashboardCard, { DashboardCardType, DONATION_STATUS_BADGE, @@ -33,66 +33,82 @@ const AdminDashboard: React.FC = () => { const [recentDonations, setRecentDonations] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [stats, setStats] = useState | null>(null); + const [pendingApplicationsFailed, setPendingApplicationsFailed] = + useState(false); + const [recentOrdersFailed, setRecentOrdersFailed] = useState(false); + const [recentDonationsFailed, setRecentDonationsFailed] = useState(false); + const [statsFetchFailed, setStatsFetchFailed] = useState(false); - useEffect(() => { - const fetchMe = async () => { - let user: User; - try { - user = await ApiClient.getMe(); - setCurrentUser(user); - } catch { - setAlertMessage('Error fetching user data', AlertStatus.ERROR); - return; - } - + const fetchStats = React.useCallback( + async (userId: number) => { + setStatsFetchFailed(false); try { - const userStats = await ApiClient.getUserStats(user.id); + const userStats = await ApiClient.getUserStats(userId); setStats(userStats); } catch { + setStatsFetchFailed(true); setAlertMessage( 'Error fetching dashboard statistics', AlertStatus.ERROR, ); } - }; + }, + [setAlertMessage], + ); - const fetchPendingApplications = async () => { - try { - const applications = await ApiClient.getRecentPendingApplications(); - setPendingApplications(applications); - } catch { - setAlertMessage( - 'Error fetching pending applications', - AlertStatus.ERROR, - ); - } - }; + const fetchPendingApplications = React.useCallback(async () => { + setPendingApplicationsFailed(false); + try { + const applications = await ApiClient.getRecentPendingApplications(); + setPendingApplications(applications); + } catch { + setPendingApplicationsFailed(true); + setAlertMessage('Error fetching pending applications', AlertStatus.ERROR); + } + }, [setAlertMessage]); - const fetchRecentOrders = async () => { - try { - const allOrders = await ApiClient.getAllOrders(); - const sortedOrders = allOrders.sort( - (a: OrderSummary, b: OrderSummary) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ); - setRecentOrders(sortedOrders.slice(0, 2)); - } catch { - setAlertMessage('Error fetching recent orders', AlertStatus.ERROR); - } - }; + const fetchRecentOrders = React.useCallback(async () => { + setRecentOrdersFailed(false); + try { + const allOrders = await ApiClient.getAllOrders(); + const sortedOrders = allOrders.sort( + (a: OrderSummary, b: OrderSummary) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + setRecentOrders(sortedOrders.slice(0, 2)); + } catch { + setRecentOrdersFailed(true); + setAlertMessage('Error fetching recent orders', AlertStatus.ERROR); + } + }, [setAlertMessage]); - const fetchRecentDonations = async () => { + const fetchRecentDonations = React.useCallback(async () => { + setRecentDonationsFailed(false); + try { + const allDonations = await ApiClient.getAllDonations(); + const sortedDonations = allDonations.sort( + (a: Donation, b: Donation) => + new Date(b.dateDonated).getTime() - new Date(a.dateDonated).getTime(), + ); + setRecentDonations(sortedDonations.slice(0, 2)); + } catch { + setRecentDonationsFailed(true); + setAlertMessage('Error fetching recent donations', AlertStatus.ERROR); + } + }, [setAlertMessage]); + + useEffect(() => { + const fetchMe = async () => { + let user: User; try { - const allDonations = await ApiClient.getAllDonations(); - const sortedDonations = allDonations.sort( - (a: Donation, b: Donation) => - new Date(b.dateDonated).getTime() - - new Date(a.dateDonated).getTime(), - ); - setRecentDonations(sortedDonations.slice(0, 2)); + user = await ApiClient.getMe(); + setCurrentUser(user); } catch { - setAlertMessage('Error fetching recent donations', AlertStatus.ERROR); + setAlertMessage('Error fetching user data', AlertStatus.ERROR); + return; } + + await fetchStats(user.id); }; const load = async () => { @@ -109,14 +125,23 @@ const AdminDashboard: React.FC = () => { }; load(); - }, [setAlertMessage]); + }, [ + setAlertMessage, + fetchStats, + fetchPendingApplications, + fetchRecentOrders, + fetchRecentDonations, + ]); if (loading) return null; const isPageEmpty = pendingApplications.length === 0 && + !pendingApplicationsFailed && recentOrders.length === 0 && - recentDonations.length === 0; + !recentOrdersFailed && + recentDonations.length === 0 && + !recentDonationsFailed; return ( @@ -132,7 +157,24 @@ const AdminDashboard: React.FC = () => { Welcome, {currentUser?.firstName} {currentUser?.lastName} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( { Pending Actions - {pendingApplications.length === 0 ? ( + {pendingApplicationsFailed ? ( + + + + + + + ) : pendingApplications.length === 0 ? ( @@ -194,7 +248,19 @@ const AdminDashboard: React.FC = () => { Recent Orders - {recentOrders.length === 0 ? ( + {recentOrdersFailed ? ( + + + + + + + ) : recentOrders.length === 0 ? ( @@ -235,7 +301,19 @@ const AdminDashboard: React.FC = () => { Recent Donations - {recentDonations.length === 0 ? ( + {recentDonationsFailed ? ( + + + + + + + ) : recentDonations.length === 0 ? ( diff --git a/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx b/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx index af9676ed5..4565abb9a 100644 --- a/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx +++ b/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx @@ -155,7 +155,7 @@ const FoodManufacturerApplicationDetails: React.FC = () => { } finally { setLoading(false); } - }, [id]); + }, [id, setAlertMessage]); useEffect(() => { fetchApplicationDetails(); diff --git a/apps/frontend/src/containers/foodManufacturerDashboard.tsx b/apps/frontend/src/containers/foodManufacturerDashboard.tsx index c18b0905f..066d2e44f 100644 --- a/apps/frontend/src/containers/foodManufacturerDashboard.tsx +++ b/apps/frontend/src/containers/foodManufacturerDashboard.tsx @@ -1,5 +1,5 @@ import ApiClient from '@api/apiClient'; -import { Box, Heading, Text } from '@chakra-ui/react'; +import { Box, Button, Heading, Text } from '@chakra-ui/react'; import DashboardCard, { DashboardCardType } from '@components/dashboardCard'; import { FloatingAlert } from '@components/floatingAlert'; import PageEmptyState from '@components/pageEmptyState'; @@ -14,83 +14,121 @@ import { Donation, DonationDetails, DonationReminderDto, - FoodManufacturer, + ManufacturerSummary, User, } from '../types/types'; +const formatManufacturerNames = (names: string[]): string => { + if (names.length <= 1) return names[0] ?? ''; + if (names.length === 2) return `${names[0]} and ${names[1]}`; + return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`; +}; + const FoodManufacturerDashboard: React.FC = () => { const navigate = useNavigate(); const [errorAlertState, setErrorMessage] = useAlert(); const [loading, setLoading] = useState(true); - const [foodManufacturer, setFoodManufacturer] = - useState(null); + const [foodManufacturers, setFoodManufacturers] = useState< + ManufacturerSummary[] + >([]); const [upcomingReminders, setUpcomingReminders] = useState< DonationReminderDto[] >([]); const [recentDonations, setRecentDonations] = useState([]); + const [donationsFetchFailed, setDonationsFetchFailed] = useState(false); + const [remindersFetchFailed, setRemindersFetchFailed] = useState(false); const [stats, setStats] = useState | null>(null); + const [statsFetchFailed, setStatsFetchFailed] = useState(false); + const [currentUser, setCurrentUser] = useState(null); - useEffect(() => { - const fetchFmData = async () => { - let currentUser: User; + const fetchUserAndManufacturers = + React.useCallback(async (): Promise => { try { - currentUser = await ApiClient.getMe(); - const fmId = await ApiClient.getCurrentUserFoodManufacturerId(); - const fm = await ApiClient.getFoodManufacturer(fmId); - setFoodManufacturer(fm); + const user = await ApiClient.getMe(); + setCurrentUser(user); + const fms = await ApiClient.getMyFoodManufacturers(); + setFoodManufacturers(fms); + return user; } catch { setErrorMessage('Error fetching dashboard data', AlertStatus.ERROR); - return; - } finally { - setLoading(false); + return null; } + }, [setErrorMessage]); + const fetchStats = React.useCallback( + async (userId: number) => { + setStatsFetchFailed(false); try { - const userStats = await ApiClient.getUserStats(currentUser.id); + const userStats = await ApiClient.getUserStats(userId); setStats(userStats); } catch { + setStatsFetchFailed(true); setErrorMessage( 'Error fetching dashboard statistics', AlertStatus.ERROR, ); } + }, + [setErrorMessage], + ); - const [reminders, donations] = await Promise.allSettled([ - ApiClient.getNextTwoDonationReminders(), - ApiClient.getAllDonationsByFoodManufacturer(), - ]); + const fetchReminders = React.useCallback(async () => { + setRemindersFetchFailed(false); + try { + const reminders = await ApiClient.getNextTwoDonationReminders(); + setUpcomingReminders(reminders); + } catch { + setRemindersFetchFailed(true); + setErrorMessage('Error fetching upcoming donations.', AlertStatus.ERROR); + } + }, [setErrorMessage]); - if (reminders.status === 'fulfilled') { - setUpcomingReminders(reminders.value); - } else { - setErrorMessage( - 'Error fetching upcoming donations.', - AlertStatus.ERROR, - ); - } + const fetchRecentDonations = React.useCallback(async () => { + setDonationsFetchFailed(false); + try { + const data = await ApiClient.getAllDonationsByFoodManufacturer(); + const sorted = data + .map((d: DonationDetails) => d.donation) + .sort( + (a: Donation, b: Donation) => + new Date(b.dateDonated).getTime() - + new Date(a.dateDonated).getTime(), + ) + .slice(0, 2); + setRecentDonations(sorted); + } catch { + setDonationsFetchFailed(true); + setErrorMessage('Error fetching recent donations.', AlertStatus.ERROR); + } + }, [setErrorMessage]); - if (donations.status === 'fulfilled') { - const sorted = donations.value - .map((d: DonationDetails) => d.donation) - .sort( - (a: Donation, b: Donation) => - new Date(b.dateDonated).getTime() - - new Date(a.dateDonated).getTime(), - ) - .slice(0, 2); - setRecentDonations(sorted); - } else { - setErrorMessage('Error fetching recent donations.', AlertStatus.ERROR); - } + useEffect(() => { + const load = async () => { + const user = await fetchUserAndManufacturers(); + setLoading(false); + if (!user) return; + await Promise.allSettled([ + fetchStats(user.id), + fetchReminders(), + fetchRecentDonations(), + ]); }; - fetchFmData(); - }, [setErrorMessage]); + load(); + }, [ + fetchUserAndManufacturers, + fetchStats, + fetchReminders, + fetchRecentDonations, + ]); if (loading) return null; const isPageEmpty = - upcomingReminders.length === 0 && recentDonations.length === 0; + upcomingReminders.length === 0 && + !remindersFetchFailed && + recentDonations.length === 0 && + !donationsFetchFailed; return ( @@ -103,10 +141,30 @@ const FoodManufacturerDashboard: React.FC = () => { /> )} - Welcome, {foodManufacturer?.foodManufacturerName} + Welcome,{' '} + {formatManufacturerNames( + foodManufacturers.map((fm) => fm.foodManufacturerName), + )} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( { ) : ( <> - Upcoming Donations + Upcoming Email Reminders for Donations - {upcomingReminders.length === 0 ? ( + {remindersFetchFailed ? ( + + + + + + + ) : upcomingReminders.length === 0 ? ( @@ -141,10 +211,10 @@ const FoodManufacturerDashboard: React.FC = () => { subtitle={ reminder.donation.foodManufacturer?.foodManufacturerName } - linkText="View Donation Requirements" + linkText="Submit Donation" onLinkClick={() => navigate( - `${ROUTES.FM_DONATION_MANAGEMENT}?donationId=${reminder.donation.donationId}`, + `${ROUTES.FM_DONATION_MANAGEMENT}?resubmitDonationId=${reminder.donation.donationId}`, ) } /> @@ -155,7 +225,19 @@ const FoodManufacturerDashboard: React.FC = () => { Recent Donations - {recentDonations.length === 0 ? ( + {donationsFetchFailed ? ( + + + + + + + ) : recentDonations.length === 0 ? ( diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index 0af1a6626..824b95685 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -14,7 +14,7 @@ import FmCompleteRequiredActionsModal from '@components/forms/fmCompleteRequired import NewDonationFormModal from '@components/forms/newDonationFormModal'; import ResubmitDonationModal from '@components/forms/resubmitDonationModal'; import SectionEmptyState from '@components/sectionEmptyState'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAlert } from '../hooks/alert'; import FMDeleteDonationActionModal from '@components/forms/fmDeleteDonationModal'; @@ -59,73 +59,91 @@ const FoodManufacturerDonationManagement: React.FC = () => { const [deleteDonation, setDeleteDonation] = useState(null); // Fetch all donations on component mount and sorts them into their appropriate status lists - const fetchDonations = async () => { - try { - const data = await ApiClient.getAllDonationsByFoodManufacturer(); - - const grouped: Record = { - [DonationStatus.AVAILABLE]: [], - [DonationStatus.FULFILLED]: [], - [DonationStatus.MATCHED]: [], - }; - - data.forEach((donationDetail: DonationDetails) => { - grouped[donationDetail.donation.status].push(donationDetail); - }); - - (Object.keys(grouped) as DonationStatus[]).forEach((status) => { - grouped[status].sort( - (a, b) => - new Date(a.donation.dateDonated).getTime() - - new Date(b.donation.dateDonated).getTime(), - ); - }); - - setStatusDonations(grouped); - - const initialPages: Record = { - [DonationStatus.AVAILABLE]: 1, - [DonationStatus.FULFILLED]: 1, - [DonationStatus.MATCHED]: 1, - }; - - // Paginate the containing status to the page that holds this donation. - const donationIdParam = searchParams.get('donationId'); - if (donationIdParam) { - const id = Number(donationIdParam); - for (const status of Object.values(DonationStatus)) { - const idx = grouped[status].findIndex( - (d) => d.donation.donationId === id, + const fetchDonations = useCallback( + async (resetPages = false) => { + try { + const data = await ApiClient.getAllDonationsByFoodManufacturer(); + + const grouped: Record = { + [DonationStatus.AVAILABLE]: [], + [DonationStatus.FULFILLED]: [], + [DonationStatus.MATCHED]: [], + }; + + data.forEach((donationDetail: DonationDetails) => { + grouped[donationDetail.donation.status].push(donationDetail); + }); + + (Object.keys(grouped) as DonationStatus[]).forEach((status) => { + grouped[status].sort( + (a, b) => + new Date(a.donation.dateDonated).getTime() - + new Date(b.donation.dateDonated).getTime(), ); - if (idx >= 0) { - initialPages[status] = Math.floor(idx / MAX_PER_STATUS) + 1; - break; + }); + + setStatusDonations(grouped); + + setCurrentPages((prev) => { + const clamped = { ...prev }; + (Object.keys(grouped) as DonationStatus[]).forEach((status) => { + const totalPages = Math.max( + 1, + Math.ceil(grouped[status].length / MAX_PER_STATUS), + ); + clamped[status] = Math.min(clamped[status], totalPages); + }); + return clamped; + }); + + if (resetPages) { + const initialPages: Record = { + [DonationStatus.AVAILABLE]: 1, + [DonationStatus.FULFILLED]: 1, + [DonationStatus.MATCHED]: 1, + }; + + // Paginate the containing status to the page that holds this donation. + const donationIdParam = searchParams.get('donationId'); + if (donationIdParam) { + const id = Number(donationIdParam); + for (const status of Object.values(DonationStatus)) { + const idx = grouped[status].findIndex( + (d) => d.donation.donationId === id, + ); + if (idx >= 0) { + initialPages[status] = Math.floor(idx / MAX_PER_STATUS) + 1; + break; + } + } } - } - } - setCurrentPages(initialPages); + setCurrentPages(initialPages); + } - return grouped; - } catch (error) { - setAlertMessage('Error fetching donations', AlertStatus.ERROR); - return; - } - }; + return grouped; + } catch { + setAlertMessage('Error fetching donations', AlertStatus.ERROR); + return; + } + }, + [searchParams, setAlertMessage], + ); - const openResubmitFromQueryParam = ( - grouped: Record, - ) => { - if (!resubmitDonationId) return; - const id = parseInt(resubmitDonationId, 10); - const allDonations: DonationDetails[] = Object.values(grouped).flat(); - const exists = allDonations.some((d) => d.donation.donationId === id); - if (exists) { - setIsResubmitOpen(true); - } else { - navigate(ROUTES.FM_DONATION_MANAGEMENT); - } - }; + const openResubmitFromQueryParam = useCallback( + (grouped: Record) => { + if (!resubmitDonationId) return; + const id = parseInt(resubmitDonationId, 10); + const allDonations: DonationDetails[] = Object.values(grouped).flat(); + const exists = allDonations.some((d) => d.donation.donationId === id); + if (exists) { + setIsResubmitOpen(true); + } else { + navigate(ROUTES.FM_DONATION_MANAGEMENT); + } + }, + [resubmitDonationId, navigate], + ); // On page load, get the food manufacturer id, fetch its donations, // and open the resubmit modal if the URL specifies one. @@ -134,7 +152,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { try { const fmId = await ApiClient.getCurrentUserFoodManufacturerId(); setManufacturerId(fmId); - const grouped = await fetchDonations(); + const grouped = await fetchDonations(true); if (grouped) openResubmitFromQueryParam(grouped); } catch { setAlertMessage( @@ -146,7 +164,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { } }; init(); - }, []); + }, [fetchDonations, openResubmitFromQueryParam, setAlertMessage]); useEffect(() => { if (loading) return; @@ -161,8 +179,11 @@ const FoodManufacturerDonationManagement: React.FC = () => { .find((d) => d.donation.donationId === id); if (match) { setSelectedViewDetailsDonation(match.donation); - } else navigate(ROUTES.FM_DONATION_MANAGEMENT); - }, [searchParams, statusDonations, loading]); + } else { + setAlertMessage('Donation not found.', AlertStatus.ERROR); + navigate(ROUTES.FM_DONATION_MANAGEMENT, { replace: true }); + } + }, [searchParams, statusDonations, loading, navigate, setAlertMessage]); const handleResubmitClose = () => { setIsResubmitOpen(false); @@ -267,7 +288,13 @@ const FoodManufacturerDonationManagement: React.FC = () => { foodManufacturerId={manufacturerId} isOpen={isResubmitOpen} onClose={handleResubmitClose} - onSuccess={() => fetchDonations()} + onSuccess={() => { + fetchDonations(); + setAlertMessage( + 'Donation resubmitted successfully.', + AlertStatus.INFO, + ); + }} donations={Object.values(statusDonations).flat()} initialDonationId={ resubmitDonationId ? parseInt(resubmitDonationId, 10) : null @@ -283,11 +310,13 @@ const FoodManufacturerDonationManagement: React.FC = () => { donation={selectedActionDonation} isOpen={true} onClose={() => setSelectedActionDonation(null)} - onSuccess={() => { + onSuccess={(allOrdersComplete) => { setSelectedActionDonation(null); if (manufacturerId !== null) fetchDonations(); setAlertMessage( - 'Your details have been saved. Actions are complete once all shipment and item details are confirmed.', + allOrdersComplete + ? 'Your details have been saved and all required actions are complete.' + : 'Your details have been saved, but shipping cost and/or tracking link are still missing for one or more orders. Please complete them soon.', AlertStatus.INFO, ); }} diff --git a/apps/frontend/src/containers/pantryApplicationDetails.tsx b/apps/frontend/src/containers/pantryApplicationDetails.tsx index a053c78dc..dd9efc2fb 100644 --- a/apps/frontend/src/containers/pantryApplicationDetails.tsx +++ b/apps/frontend/src/containers/pantryApplicationDetails.tsx @@ -156,7 +156,7 @@ const PantryApplicationDetails: React.FC = () => { } finally { setLoading(false); } - }, [id]); + }, [id, setAlertMessage]); useEffect(() => { fetchApplicationDetails(); diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index 68aed6a2f..29c904e9b 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -1,5 +1,5 @@ import ApiClient from '@api/apiClient'; -import { Box, Heading, Text } from '@chakra-ui/react'; +import { Box, Button, Heading, Text } from '@chakra-ui/react'; import DashboardCard, { DashboardCardType, ORDER_STATUS_BADGE, @@ -18,6 +18,7 @@ import { FoodRequestSummaryDto, OrderSummary, PantryWithUser, + User, } from '../types/types'; const PantryDashboard: React.FC = () => { @@ -31,6 +32,63 @@ const PantryDashboard: React.FC = () => { >([]); const [recentOrders, setRecentOrders] = useState([]); const [stats, setStats] = useState | null>(null); + const [recentFoodRequestsFailed, setRecentFoodRequestsFailed] = + useState(false); + const [recentOrdersFailed, setRecentOrdersFailed] = useState(false); + const [statsFetchFailed, setStatsFetchFailed] = useState(false); + const [currentUser, setCurrentUser] = useState(null); + + const fetchStats = React.useCallback( + async (userId: number) => { + setStatsFetchFailed(false); + try { + const userStats = await ApiClient.getUserStats(userId); + setStats(userStats); + } catch { + setStatsFetchFailed(true); + setAlertMessage( + 'Error fetching dashboard statistics', + AlertStatus.ERROR, + ); + } + }, + [setAlertMessage], + ); + + const fetchFoodRequests = React.useCallback(async () => { + setRecentFoodRequestsFailed(false); + try { + const pantryFoodRequests = await ApiClient.getPantryRequests(); + const sortedFoodRequests = pantryFoodRequests + .filter( + (fr: FoodRequestSummaryDto) => fr.status === FoodRequestStatus.ACTIVE, + ) + .sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - + new Date(a.requestedAt).getTime(), + ); + setRecentFoodRequests(sortedFoodRequests.slice(0, 2)); + } catch { + setRecentFoodRequestsFailed(true); + setAlertMessage('Error fetching food requests', AlertStatus.ERROR); + } + }, [setAlertMessage]); + + const fetchOrders = React.useCallback(async () => { + setRecentOrdersFailed(false); + try { + const pantryOrders = await ApiClient.getPantryOrders(); + const sortedOrders = pantryOrders.sort( + (a: OrderSummary, b: OrderSummary) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + setRecentOrders(sortedOrders.slice(0, 4)); + } catch { + setRecentOrdersFailed(true); + setAlertMessage('Error fetching orders', AlertStatus.ERROR); + } + }, [setAlertMessage]); useEffect(() => { const fetchDashboardData = async () => { @@ -46,45 +104,12 @@ const PantryDashboard: React.FC = () => { } }; - const fetchFoodRequests = async () => { - try { - const pantryFoodRequests = await ApiClient.getPantryRequests(); - const sortedFoodRequests = pantryFoodRequests - .filter( - (fr: FoodRequestSummaryDto) => - fr.status === FoodRequestStatus.ACTIVE, - ) - .sort( - (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => - new Date(b.requestedAt).getTime() - - new Date(a.requestedAt).getTime(), - ); - setRecentFoodRequests(sortedFoodRequests.slice(0, 2)); - } catch { - setAlertMessage('Error fetching food requests', AlertStatus.ERROR); - } - }; - - const fetchOrders = async () => { - try { - const pantryOrders = await ApiClient.getPantryOrders(); - const sortedOrders = pantryOrders.sort( - (a: OrderSummary, b: OrderSummary) => - new Date(b.createdAt).getTime() - - new Date(a.createdAt).getTime(), - ); - setRecentOrders(sortedOrders.slice(0, 4)); - } catch { - setAlertMessage('Error fetching orders', AlertStatus.ERROR); - } - }; - await Promise.all([fetchPantry(), fetchFoodRequests(), fetchOrders()]); try { const user = await ApiClient.getMe(); - const userStats = await ApiClient.getUserStats(user.id); - setStats(userStats); + setCurrentUser(user); + await fetchStats(user.id); } catch { setAlertMessage( 'Error fetching dashboard statistics', @@ -98,12 +123,15 @@ const PantryDashboard: React.FC = () => { } }; fetchDashboardData(); - }, [setAlertMessage]); + }, [setAlertMessage, fetchStats, fetchFoodRequests, fetchOrders]); if (loading) return null; const isPageEmpty = - recentFoodRequests.length === 0 && recentOrders.length === 0; + recentFoodRequests.length === 0 && + !recentFoodRequestsFailed && + recentOrders.length === 0 && + !recentOrdersFailed; return ( @@ -119,7 +147,24 @@ const PantryDashboard: React.FC = () => { Welcome, {pantry?.pantryName} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( { Recent Food Requests - {recentFoodRequests.length === 0 ? ( + {recentFoodRequestsFailed ? ( + + + + + + + ) : recentFoodRequests.length === 0 ? ( @@ -164,7 +221,19 @@ const PantryDashboard: React.FC = () => { Recent Orders - {recentOrders.length === 0 ? ( + {recentOrdersFailed ? ( + + + + + + + ) : recentOrders.length === 0 ? ( diff --git a/apps/frontend/src/containers/userManagement.tsx b/apps/frontend/src/containers/userManagement.tsx index c6034175f..90f123b79 100644 --- a/apps/frontend/src/containers/userManagement.tsx +++ b/apps/frontend/src/containers/userManagement.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ROUTES } from '../routes'; import { @@ -39,18 +39,18 @@ const VolunteerManagement: React.FC = () => { const pageSize = 8; - const fetchVolunteers = async () => { + const fetchVolunteers = useCallback(async () => { try { const allVolunteers = await ApiClient.getVolunteers(); setVolunteers(allVolunteers); } catch { setAlertMessage('Error fetching volunteers', AlertStatus.ERROR); } - }; + }, [setAlertMessage]); useEffect(() => { fetchVolunteers(); - }, [setAlertMessage]); + }, [fetchVolunteers]); useEffect(() => { setCurrentPage(1); diff --git a/apps/frontend/src/containers/volunteerDashboard.tsx b/apps/frontend/src/containers/volunteerDashboard.tsx index 8eba47e2a..f1ba5e14b 100644 --- a/apps/frontend/src/containers/volunteerDashboard.tsx +++ b/apps/frontend/src/containers/volunteerDashboard.tsx @@ -1,5 +1,5 @@ import ApiClient from '@api/apiClient'; -import { Box, Heading, Text } from '@chakra-ui/react'; +import { Box, Button, Heading, Text } from '@chakra-ui/react'; import DashboardCard, { DashboardCardType, ORDER_STATUS_BADGE, @@ -31,52 +31,86 @@ const VolunteerDashboard: React.FC = () => { >([]); const [recentOrders, setRecentOrders] = useState([]); const [stats, setStats] = useState | null>(null); + const [recentFoodRequestsFailed, setRecentFoodRequestsFailed] = + useState(false); + const [recentOrdersFailed, setRecentOrdersFailed] = useState(false); + const [statsFetchFailed, setStatsFetchFailed] = useState(false); - useEffect(() => { - const fetchDashboardData = async () => { + const fetchStats = React.useCallback( + async (userId: number) => { + setStatsFetchFailed(false); try { - const currentUser = await ApiClient.getMe(); - setUser(currentUser); + const userStats = await ApiClient.getUserStats(userId); + setStats(userStats); + } catch { + setStatsFetchFailed(true); + setAlertMessage( + 'Error fetching dashboard statistics', + AlertStatus.ERROR, + ); + } + }, + [setAlertMessage], + ); - try { - const userStats = await ApiClient.getUserStats(currentUser.id); - setStats(userStats); - } catch { - setAlertMessage( - 'Error fetching dashboard statistics', - AlertStatus.ERROR, - ); - } + const fetchFoodRequests = React.useCallback(async () => { + setRecentFoodRequestsFailed(false); + try { + const requests = await ApiClient.getVolunteerAssignedRequests(); + const sorted = requests + .filter( + (r: FoodRequestSummaryDto) => r.status === FoodRequestStatus.ACTIVE, + ) + .sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - + new Date(a.requestedAt).getTime(), + ); + setRecentFoodRequests(sorted.slice(0, 2)); + } catch { + setRecentFoodRequestsFailed(true); + setAlertMessage('Error fetching food requests', AlertStatus.ERROR); + } + }, [setAlertMessage]); - const [requests, orders] = await Promise.all([ - ApiClient.getVolunteerAssignedRequests(), - ApiClient.getVolunteerRecentOrders(), - ]); + const fetchOrders = React.useCallback(async () => { + setRecentOrdersFailed(false); + try { + const orders = await ApiClient.getVolunteerRecentOrders(); + setRecentOrders(orders); + } catch { + setRecentOrdersFailed(true); + setAlertMessage('Error fetching orders', AlertStatus.ERROR); + } + }, [setAlertMessage]); - const sorted = requests - .filter( - (r: FoodRequestSummaryDto) => r.status === FoodRequestStatus.ACTIVE, - ) - .sort( - (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => - new Date(b.requestedAt).getTime() - - new Date(a.requestedAt).getTime(), - ); - setRecentFoodRequests(sorted.slice(0, 2)); - setRecentOrders(orders); + useEffect(() => { + const fetchDashboardData = async () => { + let currentUser: User; + try { + currentUser = await ApiClient.getMe(); + setUser(currentUser); } catch { setAlertMessage('Error fetching dashboard data', AlertStatus.ERROR); - } finally { setLoading(false); + return; } + + await fetchStats(currentUser.id); + + await Promise.all([fetchFoodRequests(), fetchOrders()]); + setLoading(false); }; fetchDashboardData(); - }, [setAlertMessage]); + }, [setAlertMessage, fetchStats, fetchFoodRequests, fetchOrders]); if (loading || !user) return null; const isPageEmpty = - recentFoodRequests.length === 0 && recentOrders.length === 0; + recentFoodRequests.length === 0 && + !recentFoodRequestsFailed && + recentOrders.length === 0 && + !recentOrdersFailed; return ( @@ -92,7 +126,24 @@ const VolunteerDashboard: React.FC = () => { Welcome, {user.firstName} {user.lastName} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( { Recent Food Requests - {recentFoodRequests.length === 0 ? ( + {recentFoodRequestsFailed ? ( + + + + + + + ) : recentFoodRequests.length === 0 ? ( @@ -139,7 +202,19 @@ const VolunteerDashboard: React.FC = () => { My Orders - {recentOrders.length === 0 ? ( + {recentOrdersFailed ? ( + + + + + + + ) : recentOrders.length === 0 ? ( diff --git a/apps/frontend/src/hooks/alert.ts b/apps/frontend/src/hooks/alert.ts index 2e8b056f2..ef2716631 100644 --- a/apps/frontend/src/hooks/alert.ts +++ b/apps/frontend/src/hooks/alert.ts @@ -17,7 +17,20 @@ export function useAlert(): [ const setAlertMessage = useCallback( (message: string, status: AlertStatus) => { - setAlertState({ message, status, id: idRef.current++ }); + setAlertState((prev) => { + if (prev && prev.status === status) { + const lines = prev.message.split('\n'); + if (lines.includes(message)) { + return { ...prev, id: idRef.current++ }; + } + return { + message: `${prev.message}\n${message}`, + status, + id: idRef.current++, + }; + } + return { message, status, id: idRef.current++ }; + }); }, [], );