diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 486bfc29a..5af1c193e 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -17,10 +17,10 @@ import { ManufacturerModule } from '../foodManufacturers/manufacturers.module'; import { DonationItemsModule } from '../donationItems/donationItems.module'; import { Allocation } from '../allocations/allocations.entity'; import { Donation } from '../donations/donations.entity'; +import { PantriesModule } from '../pantries/pantries.module'; import { EmailsModule } from '../emails/email.module'; import { User } from '../users/users.entity'; import { UsersModule } from '../users/users.module'; -import { PantriesModule } from '../pantries/pantries.module'; @Module({ imports: [ diff --git a/apps/frontend/src/containers/adminDashboard.tsx b/apps/frontend/src/containers/adminDashboard.tsx index ef55381b2..24a148a86 100644 --- a/apps/frontend/src/containers/adminDashboard.tsx +++ b/apps/frontend/src/containers/adminDashboard.tsx @@ -1,30 +1,31 @@ -import React, { useEffect, useState } from 'react'; +import ApiClient from '@api/apiClient'; import { Box, Heading, Text } from '@chakra-ui/react'; import DashboardCard, { - ORDER_STATUS_BADGE, + DashboardCardType, DONATION_STATUS_BADGE, + ORDER_STATUS_BADGE, } from '@components/dashboardCard'; +import { FloatingAlert } from '@components/floatingAlert'; +import PageEmptyState from '@components/pageEmptyState'; +import SectionEmptyState from '@components/sectionEmptyState'; +import { DashboardStats } from '@components/dashboardStats'; +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; +import { ROUTES } from '../routes'; import { - PendingApplication, - OrderSummary, + AlertStatus, Donation, + OrderSummary, + PendingApplication, User, - AlertStatus, } from '../types/types'; -import { DashboardCardType } from '@components/dashboardCard'; -import ApiClient from '@api/apiClient'; -import { useAlert } from '../hooks/alert'; -import { FloatingAlert } from '@components/floatingAlert'; -import { useNavigate } from 'react-router-dom'; -import { ROUTES } from '../routes'; -import SectionEmptyState from '@components/sectionEmptyState'; -import PageEmptyState from '@components/pageEmptyState'; -import { DashboardStats } from '@components/dashboardStats'; const AdminDashboard: React.FC = () => { const navigate = useNavigate(); const [alertState, setAlertMessage] = useAlert(); + const [loading, setLoading] = useState(true); const [pendingApplications, setPendingApplications] = useState< PendingApplication[] >([]); @@ -33,72 +34,85 @@ const AdminDashboard: React.FC = () => { const [currentUser, setCurrentUser] = useState(null); const [stats, setStats] = useState | null>(null); - const fetchPendingApplications = async () => { - try { - const pendingApplications = - await ApiClient.getRecentPendingApplications(); - setPendingApplications(pendingApplications); - } 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(), - ); - const recentOrders = sortedOrders.slice(0, 2); - setRecentOrders(recentOrders); - } catch { - setAlertMessage('Error fetching 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(), - ); - const recentDonations = sortedDonations.slice(0, 2); - setRecentDonations(recentDonations); - } catch { - setAlertMessage('Error fetching donations', AlertStatus.ERROR); - } - }; - - const fetchMe = async () => { - let user: User; - try { - user = await ApiClient.getMe(); - setCurrentUser(user); - } catch { - setAlertMessage( - 'Authentication error. Please log in and try again.', - AlertStatus.ERROR, - ); - return; - } - - try { - const userStats = await ApiClient.getUserStats(user.id); - setStats(userStats); - } catch { - setAlertMessage('Error fetching dashboard statistics', AlertStatus.ERROR); - } - }; - useEffect(() => { - fetchMe(); - fetchRecentDonations(); - fetchRecentOrders(); - fetchPendingApplications(); + const fetchMe = async () => { + let user: User; + try { + user = await ApiClient.getMe(); + setCurrentUser(user); + } catch { + setAlertMessage('Error fetching user data', AlertStatus.ERROR); + return; + } + + try { + const userStats = await ApiClient.getUserStats(user.id); + setStats(userStats); + } catch { + setAlertMessage( + 'Error fetching dashboard statistics', + AlertStatus.ERROR, + ); + } + }; + + 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([ + fetchMe(), + fetchPendingApplications(), + fetchRecentOrders(), + fetchRecentDonations(), + ]); + } finally { + setLoading(false); + } + }; + + load(); }, [setAlertMessage]); + if (loading) return null; + const isPageEmpty = pendingApplications.length === 0 && recentOrders.length === 0 && @@ -122,7 +136,7 @@ const AdminDashboard: React.FC = () => { {isPageEmpty ? ( { {pendingApplications.length === 0 ? ( - + ) : ( { {recentOrders.length === 0 ? ( - + ) : ( { {recentDonations.length === 0 ? ( - + ) : ( { const navigate = useNavigate(); - const [alertState, setAlertMessage] = useAlert(); + const [errorAlertState, setErrorMessage] = useAlert(); + const [loading, setLoading] = useState(true); const [foodManufacturer, setFoodManufacturer] = useState(null); - const [user, setUser] = useState(null); const [upcomingReminders, setUpcomingReminders] = useState< DonationReminderDto[] >([]); @@ -33,28 +33,24 @@ const FoodManufacturerDashboard: React.FC = () => { useEffect(() => { const fetchFmData = async () => { - let fmId: number; let currentUser: User; try { currentUser = await ApiClient.getMe(); - setUser(currentUser); - - fmId = await ApiClient.getCurrentUserFoodManufacturerId(); + const fmId = await ApiClient.getCurrentUserFoodManufacturerId(); const fm = await ApiClient.getFoodManufacturer(fmId); setFoodManufacturer(fm); } catch { - setAlertMessage( - 'Error fetching your manufacturer profile.', - AlertStatus.ERROR, - ); + setErrorMessage('Error fetching dashboard data', AlertStatus.ERROR); return; + } finally { + setLoading(false); } try { const userStats = await ApiClient.getUserStats(currentUser.id); setStats(userStats); } catch { - setAlertMessage( + setErrorMessage( 'Error fetching dashboard statistics', AlertStatus.ERROR, ); @@ -68,7 +64,7 @@ const FoodManufacturerDashboard: React.FC = () => { if (reminders.status === 'fulfilled') { setUpcomingReminders(reminders.value); } else { - setAlertMessage( + setErrorMessage( 'Error fetching upcoming donations.', AlertStatus.ERROR, ); @@ -85,24 +81,24 @@ const FoodManufacturerDashboard: React.FC = () => { .slice(0, 2); setRecentDonations(sorted); } else { - setAlertMessage('Error fetching recent donations.', AlertStatus.ERROR); + setErrorMessage('Error fetching recent donations.', AlertStatus.ERROR); } }; fetchFmData(); - }, [setAlertMessage]); + }, [setErrorMessage]); - if (!foodManufacturer) return null; + if (loading) return null; const isPageEmpty = upcomingReminders.length === 0 && recentDonations.length === 0; return ( - {alertState?.status === AlertStatus.ERROR && ( + {errorAlertState && ( )} diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index ef0d3e320..4e4780874 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -1,37 +1,40 @@ -import React, { useState, useEffect } from 'react'; +import ApiClient from '@api/apiClient'; import { Box, Button, + ButtonGroup, Flex, - Table, Heading, - Pagination, IconButton, - ButtonGroup, Link, + Pagination, + Table, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft, Mail, CircleCheck } from 'lucide-react'; -import { capitalize, formatDate, DONATION_STATUS_COLORS } from '@utils/utils'; -import ApiClient from '@api/apiClient'; -import { AlertStatus, DonationDetails, DonationStatus } from '../types/types'; +import { FloatingAlert } from '@components/floatingAlert'; import DonationDetailsModal from '@components/forms/donationDetailsModal'; +import FmCompleteRequiredActionsModal from '@components/forms/fmCompleteRequiredActionsModal'; import NewDonationFormModal from '@components/forms/newDonationFormModal'; import ResubmitDonationModal from '@components/forms/resubmitDonationModal'; +import SectionEmptyState from '@components/sectionEmptyState'; +import { capitalize, DONATION_STATUS_COLORS, formatDate } from '@utils/utils'; +import { ChevronLeft, ChevronRight, Mail } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { ROUTES } from '../routes'; -import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; -import FmCompleteRequiredActionsModal from '@components/forms/fmCompleteRequiredActionsModal'; +import { ROUTES } from '../routes'; +import { AlertStatus, DonationDetails, DonationStatus } from '../types/types'; const MAX_PER_STATUS = 5; const FoodManufacturerDonationManagement: React.FC = () => { - const [alertState, setAlertMessage] = useAlert(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const resubmitDonationId: string | null = searchParams.get('resubmitDonationId'); const [isResubmitOpen, setIsResubmitOpen] = useState(false); + const [loading, setLoading] = useState(true); + const [errorAlertState, setErrorMessage] = useAlert(); + const [successAlertState, setSuccessMessage] = useAlert(); const [isLogDonationOpen, setIsLogDonationOpen] = useState(false); const [manufacturerId, setManufacturerId] = useState(null); const [selectedActionDonation, setSelectedActionDonation] = @@ -59,7 +62,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { ); // Fetch all donations on component mount and sorts them into their appropriate status lists - const fetchDonations = async () => { + const fetchDonations = async (fmId: number) => { try { const data = await ApiClient.getAllDonationsByFoodManufacturer(); @@ -106,18 +109,13 @@ const FoodManufacturerDonationManagement: React.FC = () => { setCurrentPages(initialPages); - // On page load, get the food manufacturer id and all appropriate donations return grouped; - } catch { - setAlertMessage('Error fetching donations', AlertStatus.ERROR); + } catch (error) { + setErrorMessage('Error fetching donations', AlertStatus.ERROR); + return; } }; - const handleLogNewDonationSuccess = () => { - setAlertMessage('Successfully logged new donation', AlertStatus.INFO); - if (manufacturerId !== null) fetchDonations(); - }; - const openResubmitFromQueryParam = ( grouped: Record, ) => { @@ -139,13 +137,15 @@ const FoodManufacturerDonationManagement: React.FC = () => { try { const fmId = await ApiClient.getCurrentUserFoodManufacturerId(); setManufacturerId(fmId); - const grouped = await fetchDonations(); + const grouped = await fetchDonations(fmId); if (grouped) openResubmitFromQueryParam(grouped); } catch { - setAlertMessage( + setErrorMessage( 'Error initializing donation management', AlertStatus.ERROR, ); + } finally { + setLoading(false); } }; init(); @@ -157,7 +157,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { const id = Number(donationIdParam); setSelectedDonationId(id); - }, [searchParams, setAlertMessage]); + }, [searchParams, setErrorMessage]); const handleResubmitClose = () => { setIsResubmitOpen(false); @@ -173,13 +173,23 @@ const FoodManufacturerDonationManagement: React.FC = () => { })); }; + if (loading) return null; + return ( - {alertState && ( + {errorAlertState && ( + + )} + {successAlertState && ( )} @@ -222,7 +232,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { {manufacturerId !== null && ( fetchDonations(manufacturerId)} isOpen={isLogDonationOpen} onClose={() => setIsLogDonationOpen(false)} /> @@ -232,7 +242,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { fetchDonations()} + onSuccess={() => fetchDonations(manufacturerId)} donations={Object.values(statusDonations).flat()} foodManufacturerId={manufacturerId} initialDonationId={ @@ -251,8 +261,8 @@ const FoodManufacturerDonationManagement: React.FC = () => { onClose={() => setSelectedActionDonation(null)} onSuccess={() => { setSelectedActionDonation(null); - fetchDonations(); - setAlertMessage( + if (manufacturerId !== null) fetchDonations(manufacturerId); + setSuccessMessage( 'Your details have been saved. Actions are complete once all shipment and item details are confirmed.', AlertStatus.INFO, ); @@ -363,28 +373,7 @@ const DonationStatusSection: React.FC = ({ {donations.length === 0 ? ( - - - - - - No Donations - - - You have no {status.toLowerCase()} donations at this time. - - + ) : ( <> diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index 523a2c1cf..579775078 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -1,26 +1,29 @@ -import React, { useEffect, useState } from 'react'; +import ApiClient from '@api/apiClient'; import { Box, Heading, Text } from '@chakra-ui/react'; -import DashboardCard, { ORDER_STATUS_BADGE } from '@components/dashboardCard'; +import DashboardCard, { + DashboardCardType, + ORDER_STATUS_BADGE, +} from '@components/dashboardCard'; +import { FloatingAlert } from '@components/floatingAlert'; +import PageEmptyState from '@components/pageEmptyState'; +import SectionEmptyState from '@components/sectionEmptyState'; +import { DashboardStats } from '@components/dashboardStats'; +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; +import { ROUTES } from '../routes'; import { AlertStatus, FoodRequestSummaryDto, OrderSummary, PantryWithUser, } from '../types/types'; -import { DashboardCardType } from '@components/dashboardCard'; -import ApiClient from '@api/apiClient'; -import { useAlert } from '../hooks/alert'; -import { FloatingAlert } from '@components/floatingAlert'; -import { useNavigate } from 'react-router-dom'; -import { ROUTES } from '../routes'; -import SectionEmptyState from '@components/sectionEmptyState'; -import PageEmptyState from '@components/pageEmptyState'; -import { DashboardStats } from '@components/dashboardStats'; const PantryDashboard: React.FC = () => { const navigate = useNavigate(); const [alertState, setAlertMessage] = useAlert(); + const [loading, setLoading] = useState(true); const [pantry, setPantry] = useState(null); const [recentFoodRequests, setRecentFoodRequests] = useState< FoodRequestSummaryDto[] @@ -30,72 +33,68 @@ const PantryDashboard: React.FC = () => { useEffect(() => { const fetchDashboardData = async () => { - let pantryId: number; try { - pantryId = await ApiClient.getCurrentUserPantryId(); - const pantryData = await ApiClient.getPantry(pantryId); - setPantry(pantryData); - } catch { - setAlertMessage('Error fetching pantry information', AlertStatus.ERROR); - return; - } + const pantryId = await ApiClient.getCurrentUserPantryId(); - try { - const user = await ApiClient.getMe(); - const userStats = await ApiClient.getUserStats(user.id); - setStats(userStats); - } catch { - setAlertMessage( - 'Error fetching dashboard statistics', - AlertStatus.ERROR, - ); - } + const fetchPantry = async () => { + try { + const pantryData = await ApiClient.getPantry(pantryId); + setPantry(pantryData); + } catch { + setAlertMessage('Error fetching pantry data', AlertStatus.ERROR); + } + }; - try { - const pantryFoodRequests = await ApiClient.getPantryRequests(); - const sortedFoodRequests = pantryFoodRequests.sort( - (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => - new Date(b.requestedAt).getTime() - - new Date(a.requestedAt).getTime(), - ); - setRecentFoodRequests(sortedFoodRequests.slice(0, 2)); - } catch { - setAlertMessage( - 'Error fetching pantry food requests', - AlertStatus.ERROR, - ); - } + const fetchFoodRequests = async () => { + try { + const pantryFoodRequests = await ApiClient.getPantryRequests(); + const sortedFoodRequests = pantryFoodRequests.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); + } + }; - 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)); + 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); + } catch { + setAlertMessage( + 'Error fetching dashboard statistics', + AlertStatus.ERROR, + ); + } } catch { - setAlertMessage('Error fetching orders', AlertStatus.ERROR); + setAlertMessage('Error fetching pantry ID', AlertStatus.ERROR); + } finally { + setLoading(false); } }; fetchDashboardData(); }, [setAlertMessage]); - if (!pantry) { - return ( - - - Pantry Dashboard - - - - ); - } + if (loading) return null; const isPageEmpty = recentFoodRequests.length === 0 && recentOrders.length === 0; @@ -111,14 +110,14 @@ const PantryDashboard: React.FC = () => { /> )} - Welcome, {pantry.pantryName} + Welcome, {pantry?.pantryName} {stats && } {isPageEmpty ? ( { {recentFoodRequests.length === 0 ? ( - + ) : ( { type={DashboardCardType.FOOD_REQUEST} title={`Request #${fr.requestId}`} date={fr.requestedAt} - subtitle={pantry.pantryName} + subtitle={pantry?.pantryName} linkText="View Request Details" onLinkClick={() => navigate(`${ROUTES.REQUEST_FORM}?requestId=${fr.requestId}`) @@ -161,7 +160,7 @@ const PantryDashboard: React.FC = () => { {recentOrders.length === 0 ? ( - + ) : ( { const navigate = useNavigate(); const [alertState, setAlertMessage] = useAlert(); + const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); const [recentFoodRequests, setRecentFoodRequests] = useState< FoodRequestSummaryDto[] @@ -30,48 +33,42 @@ const VolunteerDashboard: React.FC = () => { useEffect(() => { const fetchDashboardData = async () => { - let currentUser: User; try { - currentUser = await ApiClient.getMe(); + const currentUser = await ApiClient.getMe(); setUser(currentUser); - } catch { - setAlertMessage('Error fetching user information', AlertStatus.ERROR); - return; - } - try { - const userStats = await ApiClient.getUserStats(currentUser.id); - setStats(userStats); - } catch { - setAlertMessage( - 'Error fetching dashboard statistics', - AlertStatus.ERROR, - ); - } + 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(), + ]); - try { - const requests = await ApiClient.getVolunteerAssignedRequests(); const sorted = requests.sort( (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime(), ); setRecentFoodRequests(sorted.slice(0, 2)); - } catch { - setAlertMessage('Error fetching food requests', AlertStatus.ERROR); - } - - try { - const orders = await ApiClient.getVolunteerRecentOrders(); setRecentOrders(orders); } catch { - setAlertMessage('Error fetching orders', AlertStatus.ERROR); + setAlertMessage('Error fetching dashboard data', AlertStatus.ERROR); + } finally { + setLoading(false); } }; fetchDashboardData(); }, [setAlertMessage]); - if (!user) return null; + if (loading || !user) return null; const isPageEmpty = recentFoodRequests.length === 0 && recentOrders.length === 0; @@ -94,10 +91,10 @@ const VolunteerDashboard: React.FC = () => { {isPageEmpty ? ( ) : ( @@ -107,7 +104,7 @@ const VolunteerDashboard: React.FC = () => { {recentFoodRequests.length === 0 ? ( - + ) : ( { {recentOrders.length === 0 ? ( - + ) : (