Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
VolunteerAction,
FoodRequestWithoutRelations,
PendingApplication,
DonationReminderDto,
} from 'types/types';

const defaultBaseUrl =
Expand Down Expand Up @@ -460,6 +461,14 @@ export class ApiClient {
.then((response) => response.data);
}

public async getNextTwoDonationReminders(
foodManufacturerId: number,
): Promise<DonationReminderDto[]> {
return this.axiosInstance
.get(`/api/manufacturers/${foodManufacturerId}/next-two-reminders`)
.then((response) => response.data);
}

public async updateFoodManufacturerApplicationData(
manufacturerId: number,
data: UpdateFoodManufacturerApplicationDto,
Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -93,6 +94,14 @@ const router = createBrowserRouter([
</ProtectedRoute>
),
},
{
path: ROUTES.FM_DASHBOARD,
element: (
<ProtectedRoute>
<FoodManufacturerDashboard />
</ProtectedRoute>
),
},
{
path: ROUTES.APPROVE_PANTRIES,
element: (
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
131 changes: 131 additions & 0 deletions apps/frontend/src/containers/foodManufacturerDashboard.tsx
Original file line number Diff line number Diff line change
@@ -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<FoodManufacturer | null>(null);
const [upcomingReminders, setUpcomingReminders] = useState<
DonationReminderDto[]
>([]);
const [recentDonations, setRecentDonations] = useState<Donation[]>([]);

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 (
<Box p={12}>
{alertState && (
<FloatingAlert
key={alertState.id}
message={alertState.message}
status={'error'}
timeout={6000}
/>
)}
<Heading textStyle="h1" color="gray.600" mb={6}>
Welcome, {foodManufacturer?.foodManufacturerName}
</Heading>

<Text textStyle="p" color="gray.light" fontWeight={600} mb={4}>
Upcoming Donations
</Text>
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={4} mb={16}>
{upcomingReminders.map((reminder) => (
<DashboardCard
key={reminder.donation.donationId}
type={DashboardCardType.UPCOMING_DONATION}
title={`Donation #${reminder.donation.donationId}`}
date={reminder.reminderDate}
subtitle={reminder.donation.foodManufacturer?.foodManufacturerName}
badge={DONATION_STATUS_BADGE[reminder.donation.status]}
linkText="View Donation Requirements"
onLinkClick={() =>
navigate(
`/fm-donation-management?donationId=${reminder.donation.donationId}`,
)
}
/>
))}
</Box>

<Text textStyle="p" color="gray.light" fontWeight={600} mb={4}>
Recent Donations
</Text>
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={4} mb={16}>
{recentDonations.map((donation) => (
<DashboardCard
key={donation.donationId}
type={DashboardCardType.RECENT_DONATION}
title={`Donation #${donation.donationId}`}
date={donation.dateDonated}
subtitle={donation.foodManufacturer?.foodManufacturerName}
badge={DONATION_STATUS_BADGE[donation.status]}
linkText="View Donation Details"
onLinkClick={() =>
navigate(
`/fm-donation-management?donationId=${donation.donationId}`,
)
}
/>
))}
</Box>
</Box>
);
};

export default FoodManufacturerDashboard;
71 changes: 47 additions & 24 deletions apps/frontend/src/containers/foodManufacturerDonationManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Donation | null>(
null,
);
// State to hold donations grouped by status
const [statusDonations, setStatusDonations] = useState<{
[key in DonationStatus]: DonationDetails[];
Expand All @@ -26,12 +34,6 @@ const FoodManufacturerDonationManagement: React.FC = () => {
[DonationStatus.AVAILABLE]: [],
[DonationStatus.FULFILLED]: [],
});

// State to hold selected donation for details modal
const [selectedDonationId, setSelectedDonationId] = useState<number | null>(
null,
);

// State to hold current page per status
const [currentPages, setCurrentPages] = useState<
Record<DonationStatus, number>
Expand All @@ -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, DonationDetails[]> = {
[DonationStatus.AVAILABLE]: [],
Expand All @@ -68,31 +71,53 @@ const FoodManufacturerDonationManagement: React.FC = () => {

setStatusDonations(grouped);

// Initialize current page for each status
const initialPages: Record<DonationStatus, number> = {
[DonationStatus.AVAILABLE]: 1,
[DonationStatus.FULFILLED]: 1,
[DonationStatus.MATCHED]: 1,
};
setCurrentPages(initialPages);
} catch (error) {
alert('Error fetching donations: ' + error);
} catch {
setAlertMessage('Error fetching donations');
}
};

useEffect(() => {
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,
[status]: page,
}));
};

const handleCloseModal = () => {
setSelectedDonation(null);
setSearchParams({});
};

return (
<Box p={12}>
{alertState && (
<FloatingAlert
key={alertState.id}
message={alertState.message}
status={'error'}
timeout={6000}
/>
)}
<Heading textStyle="h1" color="gray.600" mb={8}>
Donation Management
</Heading>
Expand Down Expand Up @@ -122,6 +147,14 @@ const FoodManufacturerDonationManagement: React.FC = () => {
/>
)}

{selectedDonation && (
<DonationDetailsModal
donation={selectedDonation}
isOpen={true}
onClose={handleCloseModal}
/>
)}

{Object.values(DonationStatus).map((status) => {
const allDonationsByStatus = statusDonations[status] || [];

Expand All @@ -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)}
Expand All @@ -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;
Expand All @@ -166,7 +197,6 @@ const DonationStatusSection: React.FC<DonationStatusSectionProps> = ({
status,
colors,
onDonationSelect,
selectedDonationId,
totalDonations,
currentPage,
onPageChange,
Expand Down Expand Up @@ -293,17 +323,10 @@ const DonationStatusSection: React.FC<DonationStatusSectionProps> = ({
<Link
textDecorationColor="black"
variant="underline"
onClick={() => onDonationSelect(donation.donationId)}
onClick={() => onDonationSelect(donation)}
>
{donation.donationId}
</Link>
{selectedDonationId === donation.donationId && (
<DonationDetailsModal
donation={donation}
isOpen={true}
onClose={() => onDonationSelect(null)}
/>
)}
</Table.Cell>
<Table.Cell
{...tableCellStyles}
Expand Down
7 changes: 7 additions & 0 deletions apps/frontend/src/containers/homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ const Homepage: React.FC = () => {
</RouterLink>
</Link>
</ListItem>
<ListItem textAlign="center">
<Link asChild color="teal.500">
<RouterLink to="/fm-dashboard">
Food Manufacturer Dashboard
</RouterLink>
</Link>
</ListItem>
</List.Root>
</Box>

Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export const ROUTES = {
REQUEST_FORM: '/request-form',

FM_DONATION_MANAGEMENT: '/fm-donation-management',
FM_DASHBOARD: '/fm-dashboard',
};
5 changes: 5 additions & 0 deletions apps/frontend/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ export interface DonationOrderDetails {
pantryName: string;
}

export interface DonationReminderDto {
donation: Donation;
reminderDate: string;
}

export interface DonationItem {
itemId: number;
donationId: number;
Expand Down
Loading