diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 9820681e..9ea8ac13 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -41,6 +41,7 @@ import { VolunteerAction, FoodRequestWithoutRelations, PendingApplication, + DonationReminderDto, } from 'types/types'; const defaultBaseUrl = @@ -460,6 +461,14 @@ export class ApiClient { .then((response) => response.data); } + public async getNextTwoDonationReminders( + foodManufacturerId: number, + ): Promise { + return this.axiosInstance + .get(`/api/manufacturers/${foodManufacturerId}/next-two-reminders`) + .then((response) => response.data); + } + public async updateFoodManufacturerApplicationData( manufacturerId: number, data: UpdateFoodManufacturerApplicationDto, diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index be6b3c9a..ba4eb055 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -33,6 +33,7 @@ import ProfilePage from '@containers/profilePage'; import VolunteerOrderManagement from '@containers/volunteerOrderManagement'; import AdminRequestManagement from '@containers/adminRequestManagement'; import AdminDashboard from '@containers/adminDashboard'; +import FoodManufacturerDashboard from '@containers/foodManufacturerDashboard'; Amplify.configure(CognitoAuthConfig); @@ -93,6 +94,14 @@ const router = createBrowserRouter([ ), }, + { + path: ROUTES.FM_DASHBOARD, + element: ( + + + + ), + }, { path: ROUTES.APPROVE_PANTRIES, element: ( diff --git a/apps/frontend/src/components/Navbar.tsx b/apps/frontend/src/components/Navbar.tsx index ef77f564..1a35e63b 100644 --- a/apps/frontend/src/components/Navbar.tsx +++ b/apps/frontend/src/components/Navbar.tsx @@ -274,7 +274,7 @@ const Navbar: React.FC = () => { [Role.ADMIN]: ROUTES.ADMIN_DASHBOARD, [Role.VOLUNTEER]: ROUTES.HOME, [Role.PANTRY]: ROUTES.HOME, - [Role.FOODMANUFACTURER]: ROUTES.HOME, + [Role.FOODMANUFACTURER]: ROUTES.FM_DASHBOARD, }; return ( diff --git a/apps/frontend/src/containers/foodManufacturerDashboard.tsx b/apps/frontend/src/containers/foodManufacturerDashboard.tsx new file mode 100644 index 00000000..d424963c --- /dev/null +++ b/apps/frontend/src/containers/foodManufacturerDashboard.tsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Heading, Text } from '@chakra-ui/react'; +import DashboardCard, { + DONATION_STATUS_BADGE, + DashboardCardType, +} from '@components/dashboardCard'; +import { + Donation, + DonationDetails, + DonationReminderDto, + FoodManufacturer, + User, +} from '../types/types'; +import ApiClient from '@api/apiClient'; +import { useAlert } from '../hooks/alert'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useNavigate } from 'react-router-dom'; + +const FoodManufacturerDashboard: React.FC = () => { + const navigate = useNavigate(); + + const [alertState, setAlertMessage] = useAlert(); + 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 { + setAlertMessage('Error fetching your manufacturer profile.'); + return; + } + + const [reminders, donations] = await Promise.allSettled([ + ApiClient.getNextTwoDonationReminders(fmId), + ApiClient.getAllDonationsByFoodManufacturer(fmId), + ]); + + if (reminders.status === 'fulfilled') { + setUpcomingReminders(reminders.value); + } else { + setAlertMessage('Error fetching upcoming donations.'); + } + + 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 { + setAlertMessage('Error fetching recent donations.'); + } + }; + fetchFmData(); + }, [setAlertMessage]); + + return ( + + {alertState && ( + + )} + + Welcome, {foodManufacturer?.foodManufacturerName} + + + + Upcoming Donations + + + {upcomingReminders.map((reminder) => ( + + navigate( + `/fm-donation-management?donationId=${reminder.donation.donationId}`, + ) + } + /> + ))} + + + + Recent Donations + + + {recentDonations.map((donation) => ( + + navigate( + `/fm-donation-management?donationId=${donation.donationId}`, + ) + } + /> + ))} + + + ); +}; + +export default FoodManufacturerDashboard; diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index f60109c2..ae4d157e 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -12,12 +12,20 @@ import { 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 { Donation, DonationDetails, DonationStatus } from '../types/types'; import DonationDetailsModal from '@components/forms/donationDetailsModal'; import NewDonationFormModal from '@components/forms/newDonationFormModal'; +import { useSearchParams } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; +import { FloatingAlert } from '@components/floatingAlert'; const FoodManufacturerDonationManagement: React.FC = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const [alertState, setAlertMessage] = useAlert(); const [isLogDonationOpen, setIsLogDonationOpen] = useState(false); + const [selectedDonation, setSelectedDonation] = useState( + null, + ); // State to hold donations grouped by status const [statusDonations, setStatusDonations] = useState<{ [key in DonationStatus]: DonationDetails[]; @@ -26,12 +34,6 @@ const FoodManufacturerDonationManagement: React.FC = () => { [DonationStatus.AVAILABLE]: [], [DonationStatus.FULFILLED]: [], }); - - // State to hold selected donation for details modal - const [selectedDonationId, setSelectedDonationId] = useState( - null, - ); - // State to hold current page per status const [currentPages, setCurrentPages] = useState< Record @@ -46,7 +48,8 @@ const FoodManufacturerDonationManagement: React.FC = () => { // Fetch all donations on component mount and sorts them into their appropriate status lists const fetchDonations = async () => { try { - const data = await ApiClient.getAllDonationsByFoodManufacturer(1); // Replace with actual food manufacturer ID + const fmId = await ApiClient.getCurrentUserFoodManufacturerId(); + const data = await ApiClient.getAllDonationsByFoodManufacturer(fmId); const grouped: Record = { [DonationStatus.AVAILABLE]: [], @@ -68,15 +71,14 @@ const FoodManufacturerDonationManagement: React.FC = () => { setStatusDonations(grouped); - // Initialize current page for each status const initialPages: Record = { [DonationStatus.AVAILABLE]: 1, [DonationStatus.FULFILLED]: 1, [DonationStatus.MATCHED]: 1, }; setCurrentPages(initialPages); - } catch (error) { - alert('Error fetching donations: ' + error); + } catch { + setAlertMessage('Error fetching donations'); } }; @@ -84,6 +86,16 @@ const FoodManufacturerDonationManagement: React.FC = () => { fetchDonations(); }, []); + useEffect(() => { + const donationIdParam = searchParams.get('donationId'); + if (!donationIdParam) return; + + const id = Number(donationIdParam); + ApiClient.getDonation(id) + .then(setSelectedDonation) + .catch(() => setAlertMessage('Error loading donation')); + }, [searchParams, setAlertMessage]); + const handlePageChange = (status: DonationStatus, page: number) => { setCurrentPages((prev) => ({ ...prev, @@ -91,8 +103,21 @@ const FoodManufacturerDonationManagement: React.FC = () => { })); }; + const handleCloseModal = () => { + setSelectedDonation(null); + setSearchParams({}); + }; + return ( + {alertState && ( + + )} Donation Management @@ -122,6 +147,14 @@ const FoodManufacturerDonationManagement: React.FC = () => { /> )} + {selectedDonation && ( + + )} + {Object.values(DonationStatus).map((status) => { const allDonationsByStatus = statusDonations[status] || []; @@ -137,8 +170,7 @@ const FoodManufacturerDonationManagement: React.FC = () => { donations={displayedDonations} status={status} colors={DONATION_STATUS_COLORS[status]} - selectedDonationId={selectedDonationId} - onDonationSelect={setSelectedDonationId} + onDonationSelect={setSelectedDonation} totalDonations={allDonationsByStatus.length} currentPage={currentPage} onPageChange={(page) => handlePageChange(status, page)} @@ -154,8 +186,7 @@ interface DonationStatusSectionProps { donations: DonationDetails[]; status: DonationStatus; colors: string[]; - onDonationSelect: (donationId: number | null) => void; - selectedDonationId: number | null; + onDonationSelect: (donation: Donation | null) => void; totalDonations: number; currentPage: number; onPageChange: (page: number) => void; @@ -166,7 +197,6 @@ const DonationStatusSection: React.FC = ({ status, colors, onDonationSelect, - selectedDonationId, totalDonations, currentPage, onPageChange, @@ -293,17 +323,10 @@ const DonationStatusSection: React.FC = ({ onDonationSelect(donation.donationId)} + onClick={() => onDonationSelect(donation)} > {donation.donationId} - {selectedDonationId === donation.donationId && ( - onDonationSelect(null)} - /> - )} { + + + + Food Manufacturer Dashboard + + + diff --git a/apps/frontend/src/routes.ts b/apps/frontend/src/routes.ts index 19da091e..572106a7 100644 --- a/apps/frontend/src/routes.ts +++ b/apps/frontend/src/routes.ts @@ -34,4 +34,5 @@ export const ROUTES = { REQUEST_FORM: '/request-form', FM_DONATION_MANAGEMENT: '/fm-donation-management', + FM_DASHBOARD: '/fm-dashboard', }; diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 2a15a058..cf49f1f3 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -213,6 +213,11 @@ export interface DonationOrderDetails { pantryName: string; } +export interface DonationReminderDto { + donation: Donation; + reminderDate: string; +} + export interface DonationItem { itemId: number; donationId: number;