From 249d053bf2bf9ec03f46b4a4bf1c525a8fe5eed0 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 00:39:49 -0400 Subject: [PATCH 01/13] fix(fm): show alert on invalid donation deep-link instead of silent redirect --- .../src/containers/foodManufacturerDonationManagement.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index 0af1a6626..b09aac6b9 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -161,8 +161,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); From 640b08d236eff26aef478e2549be09c780d0dc3f Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 00:42:20 -0400 Subject: [PATCH 02/13] Fix dashboards showing false empty state on recent-item fetch failure --- .../src/containers/adminDashboard.tsx | 140 +++++++++++------ .../containers/foodManufacturerDashboard.tsx | 141 ++++++++++-------- .../src/containers/pantryDashboard.tsx | 108 +++++++++----- .../src/containers/volunteerDashboard.tsx | 116 +++++++++----- 4 files changed, 328 insertions(+), 177 deletions(-) diff --git a/apps/frontend/src/containers/adminDashboard.tsx b/apps/frontend/src/containers/adminDashboard.tsx index a1a0e3b81..1491df6ba 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,6 +33,51 @@ 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 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 = 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 = 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 () => { @@ -56,45 +101,6 @@ const AdminDashboard: React.FC = () => { } }; - const fetchPendingApplications = async () => { - try { - const applications = await ApiClient.getRecentPendingApplications(); - setPendingApplications(applications); - } catch { - setAlertMessage( - 'Error fetching pending applications', - AlertStatus.ERROR, - ); - } - }; - - 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 fetchRecentDonations = async () => { - 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 { - setAlertMessage('Error fetching recent donations', AlertStatus.ERROR); - } - }; - const load = async () => { try { await Promise.all([ @@ -109,14 +115,22 @@ const AdminDashboard: React.FC = () => { }; load(); - }, [setAlertMessage]); + }, [ + setAlertMessage, + 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 ( @@ -147,7 +161,19 @@ const AdminDashboard: React.FC = () => { Pending Actions - {pendingApplications.length === 0 ? ( + {pendingApplicationsFailed ? ( + + + + + + + ) : pendingApplications.length === 0 ? ( @@ -194,7 +220,19 @@ const AdminDashboard: React.FC = () => { Recent Orders - {recentOrders.length === 0 ? ( + {recentOrdersFailed ? ( + + + + + + + ) : recentOrders.length === 0 ? ( @@ -235,7 +273,19 @@ const AdminDashboard: React.FC = () => { Recent Donations - {recentDonations.length === 0 ? ( + {recentDonationsFailed ? ( + + + + + + + ) : recentDonations.length === 0 ? ( diff --git a/apps/frontend/src/containers/foodManufacturerDashboard.tsx b/apps/frontend/src/containers/foodManufacturerDashboard.tsx index c18b0905f..efcb5d739 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,89 @@ 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 [stats, setStats] = useState | null>(null); + const fetchFmData = React.useCallback(async () => { + setDonationsFetchFailed(false); + let currentUser: User; + try { + currentUser = await ApiClient.getMe(); + const fms = await ApiClient.getMyFoodManufacturers(); + setFoodManufacturers(fms); + } catch { + setErrorMessage('Error fetching dashboard data', AlertStatus.ERROR); + return; + } finally { + setLoading(false); + } + + try { + const userStats = await ApiClient.getUserStats(currentUser.id); + setStats(userStats); + } catch { + setErrorMessage('Error fetching dashboard statistics', AlertStatus.ERROR); + } + + const [reminders, donations] = await Promise.allSettled([ + ApiClient.getNextTwoDonationReminders(), + ApiClient.getAllDonationsByFoodManufacturer(), + ]); + + if (reminders.status === 'fulfilled') { + setUpcomingReminders(reminders.value); + } else { + setErrorMessage('Error fetching upcoming donations.', AlertStatus.ERROR); + } + + 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 { + setDonationsFetchFailed(true); + setErrorMessage('Error fetching recent donations.', AlertStatus.ERROR); + } + }, [setErrorMessage]); + useEffect(() => { - const fetchFmData = async () => { - let currentUser: User; - try { - currentUser = await ApiClient.getMe(); - const fmId = await ApiClient.getCurrentUserFoodManufacturerId(); - const fm = await ApiClient.getFoodManufacturer(fmId); - setFoodManufacturer(fm); - } catch { - setErrorMessage('Error fetching dashboard data', AlertStatus.ERROR); - return; - } finally { - setLoading(false); - } - - try { - const userStats = await ApiClient.getUserStats(currentUser.id); - setStats(userStats); - } catch { - setErrorMessage( - 'Error fetching dashboard statistics', - AlertStatus.ERROR, - ); - } - - const [reminders, donations] = await Promise.allSettled([ - ApiClient.getNextTwoDonationReminders(), - ApiClient.getAllDonationsByFoodManufacturer(), - ]); - - if (reminders.status === 'fulfilled') { - setUpcomingReminders(reminders.value); - } else { - setErrorMessage( - 'Error fetching upcoming donations.', - AlertStatus.ERROR, - ); - } - - 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); - } - }; fetchFmData(); - }, [setErrorMessage]); + }, [fetchFmData]); if (loading) return null; const isPageEmpty = - upcomingReminders.length === 0 && recentDonations.length === 0; + upcomingReminders.length === 0 && + recentDonations.length === 0 && + !donationsFetchFailed; return ( @@ -103,7 +109,10 @@ const FoodManufacturerDashboard: React.FC = () => { /> )} - Welcome, {foodManufacturer?.foodManufacturerName} + Welcome,{' '} + {formatManufacturerNames( + foodManufacturers.map((fm) => fm.foodManufacturerName), + )} {stats && } @@ -155,7 +164,19 @@ const FoodManufacturerDashboard: React.FC = () => { Recent Donations - {recentDonations.length === 0 ? ( + {donationsFetchFailed ? ( + + + + + + + ) : recentDonations.length === 0 ? ( diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index 68aed6a2f..16945176b 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, @@ -31,6 +31,44 @@ 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 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,39 +84,6 @@ 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 { @@ -98,12 +103,15 @@ const PantryDashboard: React.FC = () => { } }; fetchDashboardData(); - }, [setAlertMessage]); + }, [setAlertMessage, fetchFoodRequests, fetchOrders]); if (loading) return null; const isPageEmpty = - recentFoodRequests.length === 0 && recentOrders.length === 0; + recentFoodRequests.length === 0 && + !recentFoodRequestsFailed && + recentOrders.length === 0 && + !recentOrdersFailed; return ( @@ -134,7 +142,19 @@ const PantryDashboard: React.FC = () => { Recent Food Requests - {recentFoodRequests.length === 0 ? ( + {recentFoodRequestsFailed ? ( + + + + + + + ) : recentFoodRequests.length === 0 ? ( @@ -164,7 +184,19 @@ const PantryDashboard: React.FC = () => { Recent Orders - {recentOrders.length === 0 ? ( + {recentOrdersFailed ? ( + + + + + + + ) : recentOrders.length === 0 ? ( diff --git a/apps/frontend/src/containers/volunteerDashboard.tsx b/apps/frontend/src/containers/volunteerDashboard.tsx index 8eba47e2a..30357e31e 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,76 @@ 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 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 fetchOrders = React.useCallback(async () => { + setRecentOrdersFailed(false); + try { + const orders = await ApiClient.getVolunteerRecentOrders(); + setRecentOrders(orders); + } catch { + setRecentOrdersFailed(true); + setAlertMessage('Error fetching orders', AlertStatus.ERROR); + } + }, [setAlertMessage]); useEffect(() => { const fetchDashboardData = async () => { + let currentUser: User; try { - const currentUser = await ApiClient.getMe(); + currentUser = await ApiClient.getMe(); setUser(currentUser); - - try { - const userStats = await ApiClient.getUserStats(currentUser.id); - setStats(userStats); - } catch { - setAlertMessage( - 'Error fetching dashboard statistics', - AlertStatus.ERROR, - ); - } - - const [requests, orders] = await Promise.all([ - ApiClient.getVolunteerAssignedRequests(), - ApiClient.getVolunteerRecentOrders(), - ]); - - 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); } catch { setAlertMessage('Error fetching dashboard data', AlertStatus.ERROR); - } finally { setLoading(false); + return; } + + try { + const userStats = await ApiClient.getUserStats(currentUser.id); + setStats(userStats); + } catch { + setAlertMessage( + 'Error fetching dashboard statistics', + AlertStatus.ERROR, + ); + } + + await Promise.all([fetchFoodRequests(), fetchOrders()]); + setLoading(false); }; fetchDashboardData(); - }, [setAlertMessage]); + }, [setAlertMessage, fetchFoodRequests, fetchOrders]); if (loading || !user) return null; const isPageEmpty = - recentFoodRequests.length === 0 && recentOrders.length === 0; + recentFoodRequests.length === 0 && + !recentFoodRequestsFailed && + recentOrders.length === 0 && + !recentOrdersFailed; return ( @@ -107,7 +131,19 @@ const VolunteerDashboard: React.FC = () => { Recent Food Requests - {recentFoodRequests.length === 0 ? ( + {recentFoodRequestsFailed ? ( + + + + + + + ) : recentFoodRequests.length === 0 ? ( @@ -139,7 +175,19 @@ const VolunteerDashboard: React.FC = () => { My Orders - {recentOrders.length === 0 ? ( + {recentOrdersFailed ? ( + + + + + + + ) : recentOrders.length === 0 ? ( From 67f007f3515ec8b162850bfb8ee9c677e910f28b Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 01:11:50 -0400 Subject: [PATCH 03/13] fix: stack distinct dashboard fetch errors instead of overwriting useAlert overwrote its single error slot on each call, so concurrent fetch failures (stats, reminders, donations) silently dropped all but the last. Now same-status errors append as separate lines, with exact duplicates deduped. Affects all four dashboards (admin/FM/pantry/ volunteer) since they share this hook. --- apps/frontend/src/hooks/alert.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/hooks/alert.ts b/apps/frontend/src/hooks/alert.ts index 2e8b056f2..c724b97ac 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; + } + return { + message: `${prev.message}\n${message}`, + status, + id: idRef.current++, + }; + } + return { message, status, id: idRef.current++ }; + }); }, [], ); From 2453e73f1dedeeee3b89f1be0374a86adfaae79c Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 01:20:08 -0400 Subject: [PATCH 04/13] fix(fm): stop donation table pagination resetting on unrelated mutations fetchDonations() reset all three status tables' current page to 1 on every successful mutation, even ones scoped to a different table. Only reset pagination on the initial load/deep-link fetch. --- .../foodManufacturerDonationManagement.tsx | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index b09aac6b9..fc2566e71 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -59,7 +59,7 @@ 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 () => { + const fetchDonations = async (resetPages = false) => { try { const data = await ApiClient.getAllDonationsByFoodManufacturer(); @@ -83,28 +83,30 @@ const FoodManufacturerDonationManagement: React.FC = () => { 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, - ); - if (idx >= 0) { - initialPages[status] = Math.floor(idx / MAX_PER_STATUS) + 1; - break; + 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) { @@ -134,7 +136,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( From 20ba840a7cba5b0495675aed239cd384162a7d2e Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 02:07:46 -0400 Subject: [PATCH 05/13] fix(fm): guard delete donation against double-submit Delete button had no disabled/loading state, so a fast double-click fired two delete requests; the second 404s after the first succeeds and shows a false "could not be deleted" error. --- .../src/components/forms/fmDeleteDonationModal.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx b/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx index da85d57a6..7c3910145 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,19 +29,23 @@ 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); } }; @@ -107,6 +111,7 @@ const FMDeleteDonationActionModal: React.FC< textAlign="center" lineHeight="28px" onClick={onClose} + disabled={isDeleting} > Cancel @@ -121,6 +126,8 @@ const FMDeleteDonationActionModal: React.FC< flexShrink={0} textAlign="center" onClick={onDeleteDonation} + loading={isDeleting} + disabled={isDeleting} > Delete From 068e2f05290381443fb38814a0a183f0189b46f4 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 02:08:33 -0400 Subject: [PATCH 06/13] fix(frontend): prevent focus-trap crash on modal open Dialog.Root mounted via `{x && }` opens already `open=true` on its first render, so focus-trap activates before the CloseButton ref settles and finds zero focusable elements, throwing "Your focus-trap needs to have at least one focusable element" to the error boundary. Add lazyMount/unmountOnExit to every Dialog.Root so Ark UI defers mounting the content tree until the open transition begins. --- .../components/forms/addNewVolunteerModal.tsx | 2 ++ .../components/forms/assignVolunteersModal.tsx | 2 ++ .../components/forms/changePasswordModal.tsx | 2 ++ .../forms/completeRequiredActionsModal.tsx | 2 ++ .../components/forms/confirmActionModal.tsx | 2 ++ .../confirmFoodManufacturerDecisionModal.tsx | 2 ++ .../forms/confirmPantryDecisionModal.tsx | 2 ++ .../components/forms/createNewOrderModal.tsx | 2 ++ .../components/forms/donationDetailsModal.tsx | 2 ++ .../forms/fmCompleteRequiredActionsModal.tsx | 18 +++++++++++++----- .../components/forms/fmDeleteDonationModal.tsx | 2 ++ .../components/forms/newDonationFormModal.tsx | 2 ++ .../src/components/forms/orderDetailsModal.tsx | 2 ++ .../forms/orderReceivedActionModal.tsx | 2 ++ .../forms/pantryApplicationModal.tsx | 2 ++ .../forms/pantryDeleteRequestModal.tsx | 2 ++ .../components/forms/promoteVolunteerModal.tsx | 2 ++ .../components/forms/requestDetailsModal.tsx | 2 ++ .../src/components/forms/requestFormModal.tsx | 2 ++ .../components/forms/resubmitDonationModal.tsx | 2 ++ .../forms/volunteerCloseOrderModal.tsx | 2 ++ .../forms/volunteerCloseRequestModal.tsx | 2 ++ .../volunteerRequestActionRequiredModal.tsx | 2 ++ .../foodManufacturerDonationManagement.tsx | 14 +++++++++++--- 24 files changed, 68 insertions(+), 8 deletions(-) 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/fmCompleteRequiredActionsModal.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx index 760331583..ff15a11a6 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,14 +160,14 @@ 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], ); @@ -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 7c3910145..5b27733c0 100644 --- a/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx +++ b/apps/frontend/src/components/forms/fmDeleteDonationModal.tsx @@ -51,6 +51,8 @@ const FMDeleteDonationActionModal: React.FC< return ( { 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..4bccb1b43 100644 --- a/apps/frontend/src/components/forms/resubmitDonationModal.tsx +++ b/apps/frontend/src/components/forms/resubmitDonationModal.tsx @@ -139,6 +139,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/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index fc2566e71..57857ea8a 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -272,7 +272,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 @@ -288,11 +294,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, ); }} From f042033706303bff046cc325a972f706d32fcce8 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 02:15:46 -0400 Subject: [PATCH 07/13] test(fm): add regression test for Oz/Value submit gating Covers the fix requiring ozPerItem/estimatedValue >= 0.01 before Complete Required Actions submit is enabled. Adds jsdom test environment to vite config and @testing-library/dom dependency. Co-Authored-By: Claude Sonnet 5 --- .../fmCompleteRequiredActionsModal.test.tsx | 87 +++++++++++++++++++ apps/frontend/vite.config.mts | 5 ++ package.json | 1 + yarn.lock | 71 ++++++++++++++- 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx diff --git a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx new file mode 100644 index 000000000..85bd352be --- /dev/null +++ b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx @@ -0,0 +1,87 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ChakraProvider, defaultSystem } from '@chakra-ui/react'; +import FmCompleteRequiredActionsModal from './fmCompleteRequiredActionsModal'; +import { + DonationDetails, + DonationStatus, + RecurrenceEnum, + FoodType, +} from '../../types/types'; + +vi.mock('@api/apiClient', () => ({ + default: { + updateDonationItemDetails: vi.fn(), + bulkUpdateTrackingCostInfo: vi.fn(), + }, +})); + +const donation: DonationDetails = { + donation: { + donationId: 1, + dateDonated: '2026-01-01', + status: DonationStatus.AVAILABLE, + recurrence: RecurrenceEnum.NONE, + recurrenceFreq: null, + nextDonationDates: null, + occurrencesRemaining: null, + }, + associatedPendingOrders: [], + relevantDonationItems: [ + { + itemId: 1, + itemName: 'Granola', + foodType: FoodType.GRANOLA, + allocatedQuantity: 10, + detailsConfirmed: false, + ozPerItem: 1, + estimatedValue: 1, + foodRescue: false, + }, + { + itemId: 2, + itemName: 'Granola Bars', + foodType: FoodType.GRANOLA_BARS, + allocatedQuantity: 5, + detailsConfirmed: false, + ozPerItem: 1, + estimatedValue: 1, + foodRescue: false, + }, + ], +}; + +const renderModal = () => + render( + + + , + ); + +describe('FmCompleteRequiredActionsModal item details submit gating', () => { + it("disables Submit when a row's Oz. per item is 0", () => { + renderModal(); + const ozInputs = screen.getAllByPlaceholderText('0.00'); + // First "0.00" input for each row is Oz. per item, second is Donation Value + fireEvent.change(ozInputs[0], { target: { value: '0' } }); + + const submitButton = screen.getByRole('button', { + name: 'Submit', + }) as HTMLButtonElement; + expect(submitButton.disabled).toBe(true); + }); + + it('enables Submit once every row has Oz. per item and Value >= 0.01', () => { + renderModal(); + const submitButton = screen.getByRole('button', { + name: 'Submit', + }) as HTMLButtonElement; + // Prefilled with valid values (1) for both items already + expect(submitButton.disabled).toBe(false); + }); +}); diff --git a/apps/frontend/vite.config.mts b/apps/frontend/vite.config.mts index 243b3b5ae..37500d4e0 100644 --- a/apps/frontend/vite.config.mts +++ b/apps/frontend/vite.config.mts @@ -35,4 +35,9 @@ export default defineConfig({ '@loaders': path.resolve(__dirname, './src/loaders'), }, }, + + test: { + environment: 'jsdom', + globals: true, + }, }); diff --git a/package.json b/package.json index 6710338ee..19fc9e9ae 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@nx/vite": "22.5.1", "@nx/vitest": "22.5.1", "@nx/webpack": "22.5.1", + "@testing-library/dom": "^10.4.1", "@testing-library/react": "16.1.0", "@types/jest": "30.0.0", "@types/multer": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 6facfa833..68e3bc1bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1080,6 +1080,15 @@ js-tokens "^4.0.0" picocolors "^1.1.1" +"@babel/code-frame@^7.10.4": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" @@ -1246,6 +1255,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" @@ -5462,6 +5476,20 @@ dependencies: tslib "^2.8.0" +"@testing-library/dom@^10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + picocolors "1.1.1" + pretty-format "^27.0.2" + "@testing-library/react@16.1.0": version "16.1.0" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.1.0.tgz#aa0c61398bac82eaf89776967e97de41ac742d71" @@ -5527,6 +5555,11 @@ dependencies: tslib "^2.4.0" +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/aws-lambda@^8.10.134": version "8.10.160" resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.160.tgz#7a3afdd919d730e4e2be7239d0af5f36a8b0ce1e" @@ -7396,6 +7429,13 @@ aria-hidden@^1.2.4: dependencies: tslib "^2.0.0" +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" @@ -8758,6 +8798,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + destroy@1.2.0, destroy@^1.2.0, destroy@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -8834,6 +8879,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -12139,6 +12189,11 @@ luxon@^3.2.1, luxon@~3.7.0: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-string@^0.30.12, magic-string@^0.30.3: version "0.30.21" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" @@ -13036,7 +13091,7 @@ pgpass@1.0.5: dependencies: split2 "^4.1.0" -picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@1.1.1, picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -13456,6 +13511,15 @@ pretty-format@30.2.0, pretty-format@^30.0.0: ansi-styles "^5.2.0" react-is "^18.3.1" +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-format@^29.5.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -13622,6 +13686,11 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" From a1d98e82ca3e08b6194439174a9561896def9b5c Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 02:16:01 -0400 Subject: [PATCH 08/13] fix(fm): link dashboard reminder to resubmit flow instead of read-only view Upcoming Donations card sent FM reps to a read-only donation-details view, while the equivalent reminder email linked straight to the Resubmit modal. Point the card at ?resubmitDonationId= to match, and relabel the card and link text accordingly. Co-Authored-By: Claude Sonnet 5 --- apps/frontend/src/components/dashboardCard.tsx | 2 +- apps/frontend/src/containers/foodManufacturerDashboard.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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/containers/foodManufacturerDashboard.tsx b/apps/frontend/src/containers/foodManufacturerDashboard.tsx index efcb5d739..acd816d6f 100644 --- a/apps/frontend/src/containers/foodManufacturerDashboard.tsx +++ b/apps/frontend/src/containers/foodManufacturerDashboard.tsx @@ -128,7 +128,7 @@ const FoodManufacturerDashboard: React.FC = () => { ) : ( <> - Upcoming Donations + Upcoming Email Reminders for Donations {upcomingReminders.length === 0 ? ( @@ -150,10 +150,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}`, ) } /> From 6deb953aa2846b356c53ad22da99c668dc769219 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Fri, 11 Sep 2026 02:16:46 -0400 Subject: [PATCH 09/13] Revert "test(fm): add regression test for Oz/Value submit gating" This reverts commit f042033706303bff046cc325a972f706d32fcce8. --- .../fmCompleteRequiredActionsModal.test.tsx | 87 ------------------- apps/frontend/vite.config.mts | 5 -- package.json | 1 - yarn.lock | 71 +-------------- 4 files changed, 1 insertion(+), 163 deletions(-) delete mode 100644 apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx diff --git a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx deleted file mode 100644 index 85bd352be..000000000 --- a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { ChakraProvider, defaultSystem } from '@chakra-ui/react'; -import FmCompleteRequiredActionsModal from './fmCompleteRequiredActionsModal'; -import { - DonationDetails, - DonationStatus, - RecurrenceEnum, - FoodType, -} from '../../types/types'; - -vi.mock('@api/apiClient', () => ({ - default: { - updateDonationItemDetails: vi.fn(), - bulkUpdateTrackingCostInfo: vi.fn(), - }, -})); - -const donation: DonationDetails = { - donation: { - donationId: 1, - dateDonated: '2026-01-01', - status: DonationStatus.AVAILABLE, - recurrence: RecurrenceEnum.NONE, - recurrenceFreq: null, - nextDonationDates: null, - occurrencesRemaining: null, - }, - associatedPendingOrders: [], - relevantDonationItems: [ - { - itemId: 1, - itemName: 'Granola', - foodType: FoodType.GRANOLA, - allocatedQuantity: 10, - detailsConfirmed: false, - ozPerItem: 1, - estimatedValue: 1, - foodRescue: false, - }, - { - itemId: 2, - itemName: 'Granola Bars', - foodType: FoodType.GRANOLA_BARS, - allocatedQuantity: 5, - detailsConfirmed: false, - ozPerItem: 1, - estimatedValue: 1, - foodRescue: false, - }, - ], -}; - -const renderModal = () => - render( - - - , - ); - -describe('FmCompleteRequiredActionsModal item details submit gating', () => { - it("disables Submit when a row's Oz. per item is 0", () => { - renderModal(); - const ozInputs = screen.getAllByPlaceholderText('0.00'); - // First "0.00" input for each row is Oz. per item, second is Donation Value - fireEvent.change(ozInputs[0], { target: { value: '0' } }); - - const submitButton = screen.getByRole('button', { - name: 'Submit', - }) as HTMLButtonElement; - expect(submitButton.disabled).toBe(true); - }); - - it('enables Submit once every row has Oz. per item and Value >= 0.01', () => { - renderModal(); - const submitButton = screen.getByRole('button', { - name: 'Submit', - }) as HTMLButtonElement; - // Prefilled with valid values (1) for both items already - expect(submitButton.disabled).toBe(false); - }); -}); diff --git a/apps/frontend/vite.config.mts b/apps/frontend/vite.config.mts index 37500d4e0..243b3b5ae 100644 --- a/apps/frontend/vite.config.mts +++ b/apps/frontend/vite.config.mts @@ -35,9 +35,4 @@ export default defineConfig({ '@loaders': path.resolve(__dirname, './src/loaders'), }, }, - - test: { - environment: 'jsdom', - globals: true, - }, }); diff --git a/package.json b/package.json index 19fc9e9ae..6710338ee 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,6 @@ "@nx/vite": "22.5.1", "@nx/vitest": "22.5.1", "@nx/webpack": "22.5.1", - "@testing-library/dom": "^10.4.1", "@testing-library/react": "16.1.0", "@types/jest": "30.0.0", "@types/multer": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 68e3bc1bf..6facfa833 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1080,15 +1080,6 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/code-frame@^7.10.4": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" - integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== - dependencies: - "@babel/helper-validator-identifier" "^7.29.7" - js-tokens "^4.0.0" - picocolors "^1.1.1" - "@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" @@ -1255,11 +1246,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== -"@babel/helper-validator-identifier@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" - integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== - "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" @@ -5476,20 +5462,6 @@ dependencies: tslib "^2.8.0" -"@testing-library/dom@^10.4.1": - version "10.4.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" - integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "5.3.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - picocolors "1.1.1" - pretty-format "^27.0.2" - "@testing-library/react@16.1.0": version "16.1.0" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.1.0.tgz#aa0c61398bac82eaf89776967e97de41ac742d71" @@ -5555,11 +5527,6 @@ dependencies: tslib "^2.4.0" -"@types/aria-query@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" - integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== - "@types/aws-lambda@^8.10.134": version "8.10.160" resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.160.tgz#7a3afdd919d730e4e2be7239d0af5f36a8b0ce1e" @@ -7429,13 +7396,6 @@ aria-hidden@^1.2.4: dependencies: tslib "^2.0.0" -aria-query@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - aria-query@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" @@ -8798,11 +8758,6 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -dequal@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - destroy@1.2.0, destroy@^1.2.0, destroy@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -8879,11 +8834,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.9: - version "0.5.16" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" - integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== - dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -12189,11 +12139,6 @@ luxon@^3.2.1, luxon@~3.7.0: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== -lz-string@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" - integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== - magic-string@^0.30.12, magic-string@^0.30.3: version "0.30.21" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" @@ -13091,7 +13036,7 @@ pgpass@1.0.5: dependencies: split2 "^4.1.0" -picocolors@1.1.1, picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -13511,15 +13456,6 @@ pretty-format@30.2.0, pretty-format@^30.0.0: ansi-styles "^5.2.0" react-is "^18.3.1" -pretty-format@^27.0.2: - version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== - dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" - pretty-format@^29.5.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -13686,11 +13622,6 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" From 1442447116e8220791531ff4bdd8b7a4c8add92a Mon Sep 17 00:00:00 2001 From: yurikakan Date: Sun, 13 Sep 2026 17:00:50 -0400 Subject: [PATCH 10/13] fix(frontend): bump alert id on duplicate message to reset FloatingAlert timer --- apps/frontend/src/hooks/alert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/hooks/alert.ts b/apps/frontend/src/hooks/alert.ts index c724b97ac..ef2716631 100644 --- a/apps/frontend/src/hooks/alert.ts +++ b/apps/frontend/src/hooks/alert.ts @@ -21,7 +21,7 @@ export function useAlert(): [ if (prev && prev.status === status) { const lines = prev.message.split('\n'); if (lines.includes(message)) { - return prev; + return { ...prev, id: idRef.current++ }; } return { message: `${prev.message}\n${message}`, From 870da71e99505971392a252044e33ab81e2abeb0 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Sun, 13 Sep 2026 17:08:31 -0400 Subject: [PATCH 11/13] fix(fm): clamp donation status page after fetch to prevent empty page Deleting/resubmitting the last item on a status's last page left currentPages stale, so PaginationControl hid itself (totalPages<=1) while the stale page rendered no rows. --- .../foodManufacturerDonationManagement.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index 57857ea8a..56f33588c 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -83,6 +83,18 @@ const FoodManufacturerDonationManagement: React.FC = () => { 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, From f730f603749a71d2cfcb013374ff4bd4edcf3a22 Mon Sep 17 00:00:00 2001 From: yurikakan Date: Sun, 13 Sep 2026 17:12:15 -0400 Subject: [PATCH 12/13] fix(frontend): retry dashboard stats on fetch failure, drop dead empty-state copy Admin/FM/pantry/volunteer dashboards silently showed no stats block on getUserStats failure with no way to recover without reload. Adds a retry button gated on a per-dashboard statsFetchFailed flag. --- .../src/components/sectionEmptyState.tsx | 3 - .../src/containers/adminDashboard.tsx | 48 +++++-- .../containers/foodManufacturerDashboard.tsx | 129 +++++++++++++----- .../src/containers/pantryDashboard.tsx | 45 +++++- .../src/containers/volunteerDashboard.tsx | 49 +++++-- 5 files changed, 212 insertions(+), 62 deletions(-) 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 1491df6ba..fe7779522 100644 --- a/apps/frontend/src/containers/adminDashboard.tsx +++ b/apps/frontend/src/containers/adminDashboard.tsx @@ -37,6 +37,24 @@ const AdminDashboard: React.FC = () => { useState(false); const [recentOrdersFailed, setRecentOrdersFailed] = useState(false); const [recentDonationsFailed, setRecentDonationsFailed] = useState(false); + const [statsFetchFailed, setStatsFetchFailed] = useState(false); + + 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 fetchPendingApplications = React.useCallback(async () => { setPendingApplicationsFailed(false); @@ -90,15 +108,7 @@ const AdminDashboard: React.FC = () => { return; } - try { - const userStats = await ApiClient.getUserStats(user.id); - setStats(userStats); - } catch { - setAlertMessage( - 'Error fetching dashboard statistics', - AlertStatus.ERROR, - ); - } + await fetchStats(user.id); }; const load = async () => { @@ -117,6 +127,7 @@ const AdminDashboard: React.FC = () => { load(); }, [ setAlertMessage, + fetchStats, fetchPendingApplications, fetchRecentOrders, fetchRecentDonations, @@ -146,7 +157,24 @@ const AdminDashboard: React.FC = () => { Welcome, {currentUser?.firstName} {currentUser?.lastName} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( { >([]); 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); - const fetchFmData = React.useCallback(async () => { - setDonationsFetchFailed(false); - let currentUser: User; - try { - currentUser = await ApiClient.getMe(); - const fms = await ApiClient.getMyFoodManufacturers(); - setFoodManufacturers(fms); - } catch { - setErrorMessage('Error fetching dashboard data', AlertStatus.ERROR); - return; - } finally { - setLoading(false); - } + const fetchUserAndManufacturers = + React.useCallback(async (): Promise => { + try { + 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 null; + } + }, [setErrorMessage]); + + const fetchStats = React.useCallback( + async (userId: number) => { + setStatsFetchFailed(false); + try { + const userStats = await ApiClient.getUserStats(userId); + setStats(userStats); + } catch { + setStatsFetchFailed(true); + setErrorMessage( + 'Error fetching dashboard statistics', + AlertStatus.ERROR, + ); + } + }, + [setErrorMessage], + ); + const fetchReminders = React.useCallback(async () => { + setRemindersFetchFailed(false); try { - const userStats = await ApiClient.getUserStats(currentUser.id); - setStats(userStats); + const reminders = await ApiClient.getNextTwoDonationReminders(); + setUpcomingReminders(reminders); } catch { - setErrorMessage('Error fetching dashboard statistics', AlertStatus.ERROR); - } - - const [reminders, donations] = await Promise.allSettled([ - ApiClient.getNextTwoDonationReminders(), - ApiClient.getAllDonationsByFoodManufacturer(), - ]); - - if (reminders.status === 'fulfilled') { - setUpcomingReminders(reminders.value); - } else { + setRemindersFetchFailed(true); setErrorMessage('Error fetching upcoming donations.', AlertStatus.ERROR); } + }, [setErrorMessage]); - if (donations.status === 'fulfilled') { - const sorted = donations.value + 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) => @@ -81,20 +97,36 @@ const FoodManufacturerDashboard: React.FC = () => { ) .slice(0, 2); setRecentDonations(sorted); - } else { + } catch { setDonationsFetchFailed(true); setErrorMessage('Error fetching recent donations.', AlertStatus.ERROR); } }, [setErrorMessage]); useEffect(() => { - fetchFmData(); - }, [fetchFmData]); + const load = async () => { + const user = await fetchUserAndManufacturers(); + setLoading(false); + if (!user) return; + await Promise.allSettled([ + fetchStats(user.id), + fetchReminders(), + fetchRecentDonations(), + ]); + }; + load(); + }, [ + fetchUserAndManufacturers, + fetchStats, + fetchReminders, + fetchRecentDonations, + ]); if (loading) return null; const isPageEmpty = upcomingReminders.length === 0 && + !remindersFetchFailed && recentDonations.length === 0 && !donationsFetchFailed; @@ -115,7 +147,24 @@ const FoodManufacturerDashboard: React.FC = () => { )} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( { Upcoming Email Reminders for Donations - {upcomingReminders.length === 0 ? ( + {remindersFetchFailed ? ( + + + + + + + ) : upcomingReminders.length === 0 ? ( @@ -171,7 +232,7 @@ const FoodManufacturerDashboard: React.FC = () => { subtitle="We couldn't load your recent donations. Please try again." /> - diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index 16945176b..29c904e9b 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -18,6 +18,7 @@ import { FoodRequestSummaryDto, OrderSummary, PantryWithUser, + User, } from '../types/types'; const PantryDashboard: React.FC = () => { @@ -34,6 +35,25 @@ const PantryDashboard: React.FC = () => { 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); @@ -88,8 +108,8 @@ const PantryDashboard: React.FC = () => { 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', @@ -103,7 +123,7 @@ const PantryDashboard: React.FC = () => { } }; fetchDashboardData(); - }, [setAlertMessage, fetchFoodRequests, fetchOrders]); + }, [setAlertMessage, fetchStats, fetchFoodRequests, fetchOrders]); if (loading) return null; @@ -127,7 +147,24 @@ const PantryDashboard: React.FC = () => { Welcome, {pantry?.pantryName} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( { const [recentFoodRequestsFailed, setRecentFoodRequestsFailed] = useState(false); const [recentOrdersFailed, setRecentOrdersFailed] = useState(false); + const [statsFetchFailed, setStatsFetchFailed] = useState(false); + + 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); @@ -78,21 +96,13 @@ const VolunteerDashboard: React.FC = () => { return; } - try { - const userStats = await ApiClient.getUserStats(currentUser.id); - setStats(userStats); - } catch { - setAlertMessage( - 'Error fetching dashboard statistics', - AlertStatus.ERROR, - ); - } + await fetchStats(currentUser.id); await Promise.all([fetchFoodRequests(), fetchOrders()]); setLoading(false); }; fetchDashboardData(); - }, [setAlertMessage, fetchFoodRequests, fetchOrders]); + }, [setAlertMessage, fetchStats, fetchFoodRequests, fetchOrders]); if (loading || !user) return null; @@ -116,7 +126,24 @@ const VolunteerDashboard: React.FC = () => { Welcome, {user.firstName} {user.lastName} - {stats && } + {statsFetchFailed ? ( + + + + + + + ) : ( + stats && + )} {isPageEmpty ? ( Date: Thu, 17 Sep 2026 02:34:33 -0400 Subject: [PATCH 13/13] fix(frontend): satisfy react-hooks/exhaustive-deps across fetch handlers Wraps fetch/handler functions in useCallback and fixes missing hook dependencies so lint runs clean instead of relying on suppressed rules. --- .../forms/editableFMApplication.tsx | 2 +- .../forms/editablePantryApplication.tsx | 2 +- .../forms/fmCompleteRequiredActionsModal.tsx | 2 +- .../forms/resubmitDonationModal.tsx | 17 ++- .../foodManufacturerApplicationDetails.tsx | 2 +- .../foodManufacturerDonationManagement.tsx | 144 +++++++++--------- .../containers/pantryApplicationDetails.tsx | 2 +- .../src/containers/userManagement.tsx | 8 +- 8 files changed, 93 insertions(+), 86 deletions(-) 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 ff15a11a6..824bc71f8 100644 --- a/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx +++ b/apps/frontend/src/components/forms/fmCompleteRequiredActionsModal.tsx @@ -169,7 +169,7 @@ const FmCompleteRequiredActionsModal: React.FC< 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 diff --git a/apps/frontend/src/components/forms/resubmitDonationModal.tsx b/apps/frontend/src/components/forms/resubmitDonationModal.tsx index 4bccb1b43..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); 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/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index 56f33588c..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,87 +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 (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(), - ); - }); + const fetchDonations = useCallback( + async (resetPages = false) => { + try { + const data = await ApiClient.getAllDonationsByFoodManufacturer(); + + const grouped: Record = { + [DonationStatus.AVAILABLE]: [], + [DonationStatus.FULFILLED]: [], + [DonationStatus.MATCHED]: [], + }; - setStatusDonations(grouped); + data.forEach((donationDetail: DonationDetails) => { + grouped[donationDetail.donation.status].push(donationDetail); + }); - 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), + grouped[status].sort( + (a, b) => + new Date(a.donation.dateDonated).getTime() - + new Date(b.donation.dateDonated).getTime(), ); - 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, + 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), ); - if (idx >= 0) { - initialPages[status] = Math.floor(idx / MAX_PER_STATUS) + 1; - break; + 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 { + setAlertMessage('Error fetching donations', AlertStatus.ERROR); + return; } + }, + [searchParams, setAlertMessage], + ); - return grouped; - } catch (error) { - setAlertMessage('Error fetching donations', AlertStatus.ERROR); - return; - } - }; - - 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. @@ -160,7 +164,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { } }; init(); - }, []); + }, [fetchDonations, openResubmitFromQueryParam, setAlertMessage]); useEffect(() => { if (loading) return; 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/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);