Skip to content
Merged
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
7 changes: 7 additions & 0 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
VolunteerOrder,
VolunteerAction,
FoodRequestWithoutRelations,
PendingApplication,
} from 'types/types';

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

public async getRecentPendingApplications(): Promise<PendingApplication[]> {
return this.axiosInstance
.get(`/api/users/admin/recent-pending-applications`)
.then((response) => response.data);
}

public async completeOrderAction(
orderId: number,
action: VolunteerAction,
Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import VolunteerRequestManagement from '@containers/volunteerRequestManagement';
import AdminDonationStats from '@containers/adminDonationStats';
import ProfilePage from '@containers/profilePage';
import VolunteerOrderManagement from '@containers/volunteerOrderManagement';
import TestAdminDashboard from '@containers/testAdminDashboard';
import AdminRequestManagement from '@containers/adminRequestManagement';
import AdminDashboard from '@containers/adminDashboard';

Amplify.configure(CognitoAuthConfig);

Expand Down Expand Up @@ -150,10 +150,10 @@ const router = createBrowserRouter([
),
},
{
path: ROUTES.TEST_ADMIN_DASHBOARD,
path: ROUTES.ADMIN_DASHBOARD,
element: (
<ProtectedRoute>
<TestAdminDashboard />
<AdminDashboard />
</ProtectedRoute>
),
},
Expand Down
14 changes: 12 additions & 2 deletions apps/frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ const Navbar: React.FC = () => {
navigate(ROUTES.LOGIN, { replace: true });
};

// Should be changed once other dashboards are implmented
const ROLE_DASHBOARD_ROUTE: Record<Role, string> = {
[Role.ADMIN]: ROUTES.ADMIN_DASHBOARD,
[Role.VOLUNTEER]: ROUTES.HOME,
[Role.PANTRY]: ROUTES.HOME,
[Role.FOODMANUFACTURER]: ROUTES.HOME,
};

return (
<Flex
direction="column"
Expand Down Expand Up @@ -336,9 +344,11 @@ const Navbar: React.FC = () => {

<VStack align="stretch" gap={2} flex={1} overflowY="auto">
<NavLink
to={ROUTES.HOME}
to={ROLE_DASHBOARD_ROUTE[currentUser.role]}
label="Dashboard"
isActive={location.pathname === ROUTES.HOME}
isActive={
location.pathname === ROLE_DASHBOARD_ROUTE[currentUser.role]
}
/>

{sections.map((section) =>
Expand Down
176 changes: 176 additions & 0 deletions apps/frontend/src/containers/adminDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { useEffect, useState } from 'react';
import { Box, Heading, Text } from '@chakra-ui/react';
import DashboardCard, {
ORDER_STATUS_BADGE,
DONATION_STATUS_BADGE,
} from '@components/dashboardCard';
import {
PendingApplication,
OrderSummary,
Donation,
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';

const AdminDashboard: React.FC = () => {
const navigate = useNavigate();

const [alertState, setAlertMessage] = useAlert();
const [pendingApplications, setPendingApplications] = useState<
PendingApplication[]
>([]);
const [recentOrders, setRecentOrders] = useState<OrderSummary[]>([]);
const [recentDonations, setRecentDonations] = useState<Donation[]>([]);
const [currentUser, setCurrentUser] = useState<User | null>(null);

const fetchPendingApplications = async () => {
try {
const pendingApplications =
await ApiClient.getRecentPendingApplications();
setPendingApplications(pendingApplications);
} catch {
setAlertMessage('Error fetching pending applications');
}
};

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');
}
};

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 fetchMe = async () => {
let user: User;
try {
user = await ApiClient.getMe();
setCurrentUser(user);
} catch {
setAlertMessage('Authentication error. Please log in and try again.');
return;
}
};

useEffect(() => {
fetchMe();
fetchRecentDonations();
fetchRecentOrders();
fetchPendingApplications();
}, [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, {currentUser?.firstName} {currentUser?.lastName}
</Heading>

<Text textStyle="p" color="gray.light" fontWeight={600} mb={4}>
Pending Actions
</Text>
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={4} mb={16}>
{pendingApplications.map((application) => (
Comment thread
Juwang110 marked this conversation as resolved.
<DashboardCard
type={DashboardCardType.ACTION}
title={application.name}
date={application.dateApplied}
key={application.id}
linkText="View Application Details"
badge={{
label:
application.type === 'pantry' ? 'Pantry' : 'Food Manufacturer',
bg: 'neutral.100',
color: 'neutral.600',
}}
onLinkClick={() => {
navigate(
application.type === 'pantry'
? `/pantry-application-details/${application.id}`
: `/food-manufacturer-application-details/${application.id}`,
);
}}
/>
))}
</Box>

<Text textStyle="p" color="gray.light" fontWeight={600} mb={4}>
Recent Orders
</Text>
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={4} mb={16}>
{recentOrders.map((order) => (
<DashboardCard
key={order.orderId}
type={DashboardCardType.ORDER}
title={`Order #${order.orderId}`}
date={order.createdAt}
subtitle={order.request.pantry.pantryName}
linkText="View Order Details"
badge={ORDER_STATUS_BADGE[order.status]}
assignee={{
id: order.assignee.id,
firstName: order.assignee.firstName,
lastName: order.assignee.lastName,
}}
onLinkClick={() =>
navigate(`/admin-order-management?orderId=${order.orderId}`)
}
/>
))}
</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}
linkText="View Donation Details"
badge={DONATION_STATUS_BADGE[donation.status]}
onLinkClick={() =>
navigate(`/admin-donation?donationId=${donation.donationId}`)
}
/>
))}
</Box>
</Box>
);
};

export default AdminDashboard;
27 changes: 26 additions & 1 deletion apps/frontend/src/containers/adminDonation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ import ApiClient from '@api/apiClient';
import { formatDate } from '@utils/utils';
import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { ROUTES } from '../routes';

const AdminDonation: React.FC = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();

const [donations, setDonations] = useState<Donation[]>([]);
const [sortAsc, setSortAsc] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
Expand Down Expand Up @@ -49,6 +54,23 @@ const AdminDonation: React.FC = () => {
setCurrentPage(1);
}, [selectedManufacturers]);

useEffect(() => {
const donationIdFromUrl = searchParams.get('donationId');
Comment thread
Juwang110 marked this conversation as resolved.

if (!donationIdFromUrl) return;
if (donations.length === 0) return;

const matchedDonation = donations.find(
(donation) => donation.donationId === Number(donationIdFromUrl),
);

if (matchedDonation) {
setSelectedDonation(matchedDonation);
} else {
navigate(ROUTES.ADMIN_DONATION, { replace: true });
}
}, [searchParams, donations, navigate]);

const manufacturerOptions = [
...new Set(
donations
Expand Down Expand Up @@ -258,7 +280,10 @@ const AdminDonation: React.FC = () => {
<DonationDetailsModal
donation={selectedDonation}
isOpen={selectedDonation !== null}
onClose={() => setSelectedDonation(null)}
onClose={() => {
setSelectedDonation(null);
navigate(ROUTES.ADMIN_DONATION, { replace: true });
}}
/>
)}

Expand Down
43 changes: 33 additions & 10 deletions apps/frontend/src/containers/adminOrderManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { OrderStatus, OrderSummary } from '../types/types';
import OrderDetailsModal from '@components/forms/orderDetailsModal';
import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { ROUTES } from '../routes';

// Extending the OrderSummary type to include assignee color for display
type OrderWithColor = OrderSummary & { assigneeColor?: string };
Expand All @@ -59,7 +61,9 @@ const AdminOrderManagement: React.FC = () => {
},
);

const [searchParams] = useSearchParams();
const [alertState, setAlertMessage] = useAlert();
const navigate = useNavigate();

// State to hold filter state per status
type FilterState = {
Expand Down Expand Up @@ -148,6 +152,24 @@ const AdminOrderManagement: React.FC = () => {
}));
};

useEffect(() => {
const orderIdFromUrl = searchParams.get('orderId');

// Wait until orders are loaded
const allOrders = Object.values(statusOrders).flat();
if (allOrders.length === 0) return;

const matchedOrder = allOrders.find(
(order) => order.orderId === Number(orderIdFromUrl),
);

if (matchedOrder) {
setSelectedOrderId(Number(orderIdFromUrl));
Comment thread
Juwang110 marked this conversation as resolved.
} else {
navigate(ROUTES.ADMIN_ORDER_MANAGEMENT, { replace: true });
}
}, [searchParams, statusOrders, navigate]);

return (
<Box p={12}>
<Heading textStyle="h1" color="gray.600" mb={8}>
Expand Down Expand Up @@ -200,7 +222,6 @@ const AdminOrderManagement: React.FC = () => {
orders={displayedOrders}
status={status}
colors={ORDER_STATUS_COLORS[status]}
selectedOrderId={selectedOrderId}
onOrderSelect={setSelectedOrderId}
totalOrders={totalFiltered}
currentPage={currentPage}
Expand All @@ -226,6 +247,17 @@ const AdminOrderManagement: React.FC = () => {
</Box>
);
})}

{selectedOrderId && (
<OrderDetailsModal
orderId={selectedOrderId}
isOpen={true}
onClose={() => {
setSelectedOrderId(null);
navigate(ROUTES.ADMIN_ORDER_MANAGEMENT, { replace: true });
}}
/>
)}
</Box>
);
};
Expand All @@ -235,7 +267,6 @@ interface OrderStatusSectionProps {
status: OrderStatus;
colors: string[];
onOrderSelect: (orderId: number | null) => void;
selectedOrderId: number | null;
totalOrders: number;
currentPage: number;
onPageChange: (page: number) => void;
Expand All @@ -257,7 +288,6 @@ const OrderStatusSection: React.FC<OrderStatusSectionProps> = ({
status,
colors,
onOrderSelect,
selectedOrderId,
totalOrders,
currentPage,
onPageChange,
Expand Down Expand Up @@ -696,13 +726,6 @@ const OrderStatusSection: React.FC<OrderStatusSectionProps> = ({
})}
</Table.Body>
</Table.Root>
{selectedOrderId && (
<OrderDetailsModal
orderId={selectedOrderId}
isOpen={true}
onClose={() => onOrderSelect(null)}
/>
)}

{totalPages > 1 && (
<Box mt={4}>
Expand Down
Loading
Loading