diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 0e3367fb0..57bf8bc0e 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -17,6 +17,7 @@ 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'; @Module({ @@ -38,6 +39,7 @@ import { EmailsModule } from '../emails/email.module'; ManufacturerModule, DonationItemsModule, DonationModule, + forwardRef(() => PantriesModule), EmailsModule, ], controllers: [OrdersController], diff --git a/apps/frontend/src/components/pageEmptyState.tsx b/apps/frontend/src/components/pageEmptyState.tsx new file mode 100644 index 000000000..a6476f4b5 --- /dev/null +++ b/apps/frontend/src/components/pageEmptyState.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Box, Button, Text } from '@chakra-ui/react'; +import { CircleCheck } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; + +interface PageEmptyStateProps { + entity?: string; + subtitle?: string; + primaryButtonText: string; + primaryButtonLink: string; + secondaryButtonText: string; + secondaryButtonLink: string; +} + +const PageEmptyState: React.FC = ({ + entity, + subtitle, + primaryButtonText, + primaryButtonLink, + secondaryButtonText, + secondaryButtonLink, +}) => { + const navigate = useNavigate(); + const message = subtitle ?? `You have no ${entity} at this time`; + + return ( + + + + + + Nothing to see here! + + + {message} + + + + + + + ); +}; + +export default PageEmptyState; diff --git a/apps/frontend/src/components/sectionEmptyState.tsx b/apps/frontend/src/components/sectionEmptyState.tsx new file mode 100644 index 000000000..96a91773c --- /dev/null +++ b/apps/frontend/src/components/sectionEmptyState.tsx @@ -0,0 +1,30 @@ +import { Box, Text } from '@chakra-ui/react'; + +interface EmptyStateProps { + entity?: string; + subtitle?: string; +} + +const SectionEmptyState: React.FC = ({ entity, subtitle }) => { + const message = subtitle ?? `You have no ${entity} at this time`; + return ( + + + Nothing to see here! + + + {message} + + + ); +}; + +export default SectionEmptyState; diff --git a/apps/frontend/src/containers/adminDashboard.tsx b/apps/frontend/src/containers/adminDashboard.tsx index 7ce01914e..89c3f802b 100644 --- a/apps/frontend/src/containers/adminDashboard.tsx +++ b/apps/frontend/src/containers/adminDashboard.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, + 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 React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; +import { ROUTES } from '../routes'; import { - PendingApplication, - OrderSummary, Donation, + OrderSummary, + PendingApplication, User, } 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'; const AdminDashboard: React.FC = () => { const navigate = useNavigate(); const [alertState, setAlertMessage] = useAlert(); + const [loading, setLoading] = useState(true); const [pendingApplications, setPendingApplications] = useState< PendingApplication[] >([]); @@ -28,62 +31,50 @@ const AdminDashboard: React.FC = () => { const [recentDonations, setRecentDonations] = useState([]); const [currentUser, setCurrentUser] = useState(null); - const fetchPendingApplications = async () => { - try { - const pendingApplications = - await ApiClient.getRecentPendingApplications(); - setPendingApplications(pendingApplications); - } catch { - setAlertMessage('Error fetching pending applications'); - } - }; + useEffect(() => { + const fetchDashboardData = async () => { + try { + const [user, applications, allOrders, allDonations] = await Promise.all( + [ + ApiClient.getMe(), + ApiClient.getRecentPendingApplications(), + ApiClient.getAllOrders(), + ApiClient.getAllDonations(), + ], + ); - 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'); - } - }; + setCurrentUser(user); + setPendingApplications(applications); - 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'); - } - }; + const sortedOrders = allOrders.sort( + (a: OrderSummary, b: OrderSummary) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + setRecentOrders(sortedOrders.slice(0, 2)); - const fetchMe = async () => { - let user: User; - try { - user = await ApiClient.getMe(); - setCurrentUser(user); - } catch { - setAlertMessage('Authentication error. Please log in and try again.'); - return; - } - }; + 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 dashboard data'); + } finally { + setLoading(false); + } + }; - useEffect(() => { - fetchMe(); - fetchRecentDonations(); - fetchRecentOrders(); - fetchPendingApplications(); + fetchDashboardData(); }, [setAlertMessage]); + if (loading) return null; + + const isPageEmpty = + pendingApplications.length === 0 && + recentOrders.length === 0 && + recentDonations.length === 0; + return ( {alertState && ( @@ -98,84 +89,133 @@ const AdminDashboard: React.FC = () => { Welcome, {currentUser?.firstName} {currentUser?.lastName} - - Pending Actions - - - {pendingApplications.map((application) => ( - { - navigate( - application.type === 'pantry' - ? ROUTES.PANTRY_MANAGEMENT_DETAILS.replace( - ':pantryId', - application.id.toString(), - ) - : ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace( - ':applicationId', - application.id.toString(), - ), - ); - }} - /> - ))} - + {isPageEmpty ? ( + + ) : ( + <> + + Pending Actions + + {pendingApplications.length === 0 ? ( + + + + ) : ( + + {pendingApplications.map((application) => ( + { + navigate( + application.type === 'pantry' + ? ROUTES.PANTRY_MANAGEMENT_DETAILS.replace( + ':pantryId', + application.id.toString(), + ) + : ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace( + ':applicationId', + application.id.toString(), + ), + ); + }} + /> + ))} + + )} - - Recent Orders - - - {recentOrders.map((order) => ( - - navigate(`/admin-order-management?orderId=${order.orderId}`) - } - /> - ))} - + + Recent Orders + + {recentOrders.length === 0 ? ( + + + + ) : ( + + {recentOrders.map((order) => ( + + navigate(`/admin-order-management?orderId=${order.orderId}`) + } + /> + ))} + + )} - - Recent Donations - - - {recentDonations.map((donation) => ( - - navigate(`/admin-donation?donationId=${donation.donationId}`) - } - /> - ))} - + + Recent Donations + + {recentDonations.length === 0 ? ( + + + + ) : ( + + {recentDonations.map((donation) => ( + + navigate( + `/admin-donation?donationId=${donation.donationId}`, + ) + } + /> + ))} + + )} + + )} ); }; diff --git a/apps/frontend/src/containers/foodManufacturerDashboard.tsx b/apps/frontend/src/containers/foodManufacturerDashboard.tsx index 93dd02e0d..ef55bb2ac 100644 --- a/apps/frontend/src/containers/foodManufacturerDashboard.tsx +++ b/apps/frontend/src/containers/foodManufacturerDashboard.tsx @@ -1,56 +1,35 @@ -import React, { useEffect, useState } from 'react'; +import ApiClient from '@api/apiClient'; import { Box, Heading, Text } from '@chakra-ui/react'; import DashboardCard, { DashboardCardType } from '@components/dashboardCard'; -import { - Donation, - DonationDetails, - DonationReminderDto, - FoodManufacturer, -} from '../types/types'; -import ApiClient from '@api/apiClient'; -import { useAlert } from '../hooks/alert'; import { FloatingAlert } from '@components/floatingAlert'; +import SectionEmptyState from '@components/sectionEmptyState'; +import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; import { ROUTES } from '../routes'; +import { Donation, DonationDetails, FoodManufacturer } from '../types/types'; const FoodManufacturerDashboard: React.FC = () => { const navigate = useNavigate(); const [errorAlertState, setErrorMessage] = useAlert(); + const [loading, setLoading] = useState(true); const [foodManufacturer, setFoodManufacturer] = useState(null); - const [upcomingReminders, setUpcomingReminders] = useState< - DonationReminderDto[] - >([]); const [recentDonations, setRecentDonations] = useState([]); useEffect(() => { const fetchFmData = async () => { - let fmId: number; try { - fmId = await ApiClient.getCurrentUserFoodManufacturerId(); - const fm = await ApiClient.getFoodManufacturer(fmId); - setFoodManufacturer(fm); - } catch { - setErrorMessage('Error fetching your manufacturer profile.'); - return; - } + const fmId = await ApiClient.getCurrentUserFoodManufacturerId(); + const [fm, donations] = await Promise.all([ + ApiClient.getFoodManufacturer(fmId), + ApiClient.getAllDonationsByFoodManufacturer(fmId), + ]); - const [reminders, donations] = await Promise.allSettled([ - ApiClient.getNextTwoDonationReminders(fmId), - ApiClient.getAllDonationsByFoodManufacturer(fmId), - ]); - - // If reminders is successfully retrieved from API with the Promise.allSettled - if (reminders.status === 'fulfilled') { - setUpcomingReminders(reminders.value); - } else { - setErrorMessage('Error fetching upcoming donations.'); - } + setFoodManufacturer(fm); - // If donations is successfully retrieved from API with the Promise.allSettled - if (donations.status === 'fulfilled') { - const sorted = donations.value + const sorted = donations .map((d: DonationDetails) => d.donation) .sort( (a: Donation, b: Donation) => @@ -59,13 +38,17 @@ const FoodManufacturerDashboard: React.FC = () => { ) .slice(0, 2); setRecentDonations(sorted); - } else { - setErrorMessage('Error fetching recent donations.'); + } catch { + setErrorMessage('Error fetching dashboard data'); + } finally { + setLoading(false); } }; fetchFmData(); }, [setErrorMessage]); + if (loading) return null; + return ( {errorAlertState && ( @@ -80,47 +63,35 @@ const FoodManufacturerDashboard: React.FC = () => { Welcome, {foodManufacturer?.foodManufacturerName} - - Upcoming Donations - - - {upcomingReminders.map((reminder) => ( - - navigate( - `${ROUTES.FM_DONATION_MANAGEMENT}?donationId=${reminder.donation.donationId}`, - ) - } - /> - ))} - - Recent Donations - - {recentDonations.map((donation) => ( - - navigate( - `${ROUTES.FM_DONATION_MANAGEMENT}?donationId=${donation.donationId}`, - ) - } - /> - ))} - + {recentDonations.length === 0 ? ( + + ) : ( + + {recentDonations.map((donation) => ( + + navigate( + `${ROUTES.FM_DONATION_MANAGEMENT}?donationId=${donation.donationId}`, + ) + } + /> + ))} + + )} ); }; diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index 8005e28eb..c8aa6b537 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -1,27 +1,28 @@ -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 { 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 DonationDetailsModal from '@components/forms/donationDetailsModal'; -import FmCompleteRequiredActionsModal from '@components/forms/fmCompleteRequiredActionsModal'; +import { ROUTES } from '../routes'; +import { DonationDetails, DonationStatus } from '../types/types'; const MAX_PER_STATUS = 5; @@ -364,28 +365,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 182c9a9ef..57e5f70cb 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -1,22 +1,27 @@ -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 React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; +import { ROUTES } from '../routes'; import { 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'; 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[] @@ -25,43 +30,42 @@ const PantryDashboard: React.FC = () => { useEffect(() => { const fetchDashboardData = async () => { - let pantryId: number; try { - pantryId = await ApiClient.getCurrentUserPantryId(); - const pantryData = await ApiClient.getPantry(pantryId); + const pantryId = await ApiClient.getCurrentUserPantryId(); + const [pantryData, pantryFoodRequests, pantryOrders] = + await Promise.all([ + ApiClient.getPantry(pantryId), + ApiClient.getPantryRequests(pantryId), + ApiClient.getPantryOrders(pantryId), + ]); + setPantry(pantryData); - } catch { - setAlertMessage('Error fetching pantry information'); - return; - } - try { - const pantryFoodRequests = await ApiClient.getPantryRequests(pantryId); 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'); - } - try { - const pantryOrders = await ApiClient.getPantryOrders(pantryId); 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'); + setAlertMessage('Error fetching dashboard data'); + } finally { + setLoading(false); } }; fetchDashboardData(); }, [setAlertMessage]); - if (!pantry) return null; + if (loading || !pantry) return null; + + const isPageEmpty = + recentFoodRequests.length === 0 && recentOrders.length === 0; return ( @@ -77,51 +81,85 @@ const PantryDashboard: React.FC = () => { Welcome, {pantry.pantryName} - - Recent Food Requests - - - {recentFoodRequests.map((fr) => ( - - navigate(`${ROUTES.REQUEST_FORM}?requestId=${fr.requestId}`) - } - /> - ))} - + {isPageEmpty ? ( + + ) : ( + <> + + Recent Food Requests + + {recentFoodRequests.length === 0 ? ( + + + + ) : ( + + {recentFoodRequests.map((fr) => ( + + navigate(`${ROUTES.REQUEST_FORM}?requestId=${fr.requestId}`) + } + /> + ))} + + )} - - Recent Orders - - - {recentOrders.map((order) => ( - - navigate( - `${ROUTES.PANTRY_ORDER_MANAGEMENT}?orderId=${order.orderId}`, - ) - } - /> - ))} - + + Recent Orders + + {recentOrders.length === 0 ? ( + + + + ) : ( + + {recentOrders.map((order) => ( + + navigate( + `${ROUTES.PANTRY_ORDER_MANAGEMENT}?orderId=${order.orderId}`, + ) + } + /> + ))} + + )} + + )} ); }; diff --git a/apps/frontend/src/containers/volunteerDashboard.tsx b/apps/frontend/src/containers/volunteerDashboard.tsx index c33843670..ed0d7fe07 100644 --- a/apps/frontend/src/containers/volunteerDashboard.tsx +++ b/apps/frontend/src/containers/volunteerDashboard.tsx @@ -1,18 +1,23 @@ -import React, { useEffect, useState } from 'react'; -import { Box, Heading, Text } from '@chakra-ui/react'; -import DashboardCard, { ORDER_STATUS_BADGE } from '@components/dashboardCard'; -import { FoodRequestSummaryDto, User, VolunteerOrder } from '../types/types'; -import { DashboardCardType } from '@components/dashboardCard'; import ApiClient from '@api/apiClient'; -import { useAlert } from '../hooks/alert'; +import { Box, Heading, Text } from '@chakra-ui/react'; +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 React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; import { ROUTES } from '../routes'; +import { FoodRequestSummaryDto, User, VolunteerOrder } from '../types/types'; const VolunteerDashboard: React.FC = () => { const navigate = useNavigate(); const [alertState, setAlertMessage] = useAlert(); + const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); const [recentFoodRequests, setRecentFoodRequests] = useState< FoodRequestSummaryDto[] @@ -24,34 +29,32 @@ const VolunteerDashboard: React.FC = () => { try { const currentUser = await ApiClient.getMe(); setUser(currentUser); - } catch { - setAlertMessage('Error fetching user information'); - return; - } - try { - const requests = await ApiClient.getVolunteerAssignedRequests(); + const [requests, orders] = await Promise.all([ + ApiClient.getVolunteerAssignedRequests(), + ApiClient.getVolunteerRecentOrders(), + ]); + 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'); - } - - try { - const orders = await ApiClient.getVolunteerRecentOrders(); setRecentOrders(orders); } catch { - setAlertMessage('Error fetching orders'); + setAlertMessage('Error fetching dashboard data'); + } finally { + setLoading(false); } }; fetchDashboardData(); }, [setAlertMessage]); - if (!user) return null; + if (loading || !user) return null; + + const isPageEmpty = + recentFoodRequests.length === 0 && recentOrders.length === 0; return ( @@ -67,53 +70,87 @@ const VolunteerDashboard: React.FC = () => { Welcome, {user.firstName} {user.lastName} - - Recent Food Requests - - - {recentFoodRequests.map((fr) => ( - - navigate( - `${ROUTES.VOLUNTEER_REQUEST_MANAGEMENT}?requestId=${fr.requestId}`, - ) - } - /> - ))} - + {isPageEmpty ? ( + + ) : ( + <> + + Recent Food Requests + + {recentFoodRequests.length === 0 ? ( + + + + ) : ( + + {recentFoodRequests.map((fr) => ( + + navigate( + `${ROUTES.VOLUNTEER_REQUEST_MANAGEMENT}?requestId=${fr.requestId}`, + ) + } + /> + ))} + + )} - - My Orders - - - {recentOrders.map((order) => ( - - navigate( - `${ROUTES.VOLUNTEER_ORDER_MANAGEMENT}?orderId=${order.orderId}`, - ) - } - /> - ))} - + + My Orders + + {recentOrders.length === 0 ? ( + + + + ) : ( + + {recentOrders.map((order) => ( + + navigate( + `${ROUTES.VOLUNTEER_ORDER_MANAGEMENT}?orderId=${order.orderId}`, + ) + } + /> + ))} + + )} + + )} ); };