diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index f53f25a5d..11afca8ae 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -46,6 +46,7 @@ export class OrdersController { // Called like: /?status=pending&pantryName=Test%20Pantry&pantryName=Test%20Pantry%202 // %20 is the URL encoded space character // This gets all orders where the status is pending and the pantry name is either Test Pantry or Test Pantry 2 + @Roles(Role.ADMIN) @Get('/') async getAllOrders( @Query('status') status?: string, diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index ccc9ebe5c..f8b177f4b 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -117,6 +117,7 @@ export class OrdersService { createdAt: o.createdAt, shippedAt: o.shippedAt, deliveredAt: o.deliveredAt, + pantryId: o.request.pantryId, pantryName: o.request.pantry.pantryName, assignee: o.assignee, actionCompletion, @@ -157,6 +158,7 @@ export class OrdersService { createdAt: o.createdAt, shippedAt: o.shippedAt, deliveredAt: o.deliveredAt, + pantryId: o.request.pantryId, pantryName: o.request.pantry.pantryName, assignee: o.assignee, })); diff --git a/apps/backend/src/volunteers/types.ts b/apps/backend/src/volunteers/types.ts index 4c61654fb..1f67ad033 100644 --- a/apps/backend/src/volunteers/types.ts +++ b/apps/backend/src/volunteers/types.ts @@ -9,6 +9,7 @@ export type VolunteerOrder = { createdAt: Date; shippedAt: Date | null; deliveredAt: Date | null; + pantryId: number; pantryName: string; assignee: OrderAssignee; actionCompletion?: VolunteerActionCompletion; diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index a04bd01ad..8aad71695 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -3,6 +3,7 @@ import { Pantry } from '../pantries/pantries.entity'; import { VolunteersService } from './volunteers.service'; import { Role } from '../users/types'; import { Roles } from '../auth/roles.decorator'; +import { CheckOwnership } from '../auth/ownership.decorator'; import { Assignments, VolunteerOrder } from './types'; import { AuthenticatedRequest } from '../auth/authenticated-request'; import { OrdersService } from '../orders/order.service'; @@ -49,6 +50,10 @@ export class VolunteersController { // returns all orders globally // only includes actionCompletion for orders assigned to the requesting volunteer + @CheckOwnership({ + idParam: 'id', + resolver: async ({ entityId }) => [entityId], + }) @Roles(Role.VOLUNTEER) @Get('/:id/orders') async getVolunteerOrders( diff --git a/apps/frontend/src/containers/adminOrderManagement.tsx b/apps/frontend/src/containers/adminOrderManagement.tsx index 26f56e931..f2939ffa7 100644 --- a/apps/frontend/src/containers/adminOrderManagement.tsx +++ b/apps/frontend/src/containers/adminOrderManagement.tsx @@ -157,7 +157,7 @@ const AdminOrderManagement: React.FC = () => { // Wait until orders are loaded const allOrders = Object.values(statusOrders).flat(); - if (allOrders.length === 0) return; + if (!orderIdFromUrl || allOrders.length === 0) return; const id = Number(orderIdFromUrl); const matchedOrder = allOrders.find((order) => order.orderId === id); @@ -183,6 +183,39 @@ const AdminOrderManagement: React.FC = () => { } }, [searchParams, statusOrders, navigate]); + // Pre-fill pantry filter from url param and then clear the param. + useEffect(() => { + const pantryIdFromUrl = searchParams.get('pantryId'); + + const allOrders = Object.values(statusOrders).flat(); + if (!pantryIdFromUrl || allOrders.length === 0) return; + + const matchedOrder = allOrders.find( + (order) => order.request.pantryId === Number(pantryIdFromUrl), + ); + const pantryName = matchedOrder?.request.pantry.pantryName; + + if (pantryName) { + setFilterStates((prev) => ({ + [OrderStatus.SHIPPED]: { + ...prev[OrderStatus.SHIPPED], + selectedPantries: [pantryName], + }, + [OrderStatus.PENDING]: { + ...prev[OrderStatus.PENDING], + selectedPantries: [pantryName], + }, + [OrderStatus.DELIVERED]: { + ...prev[OrderStatus.DELIVERED], + selectedPantries: [pantryName], + }, + })); + } else { + setAlertMessage('Selected pantry has no orders'); + navigate(ROUTES.ADMIN_ORDER_MANAGEMENT, { replace: true }); + } + }, [searchParams, statusOrders, navigate, setAlertMessage]); + return ( @@ -362,7 +395,7 @@ const OrderStatusSection: React.FC = ({ - {orders.length === 0 ? ( + {orders.length === 0 && filterState.selectedPantries.length === 0 ? ( = ({ )} - - - - - Order # - - - Status - - - Assignee - - - Pantry - - - Dates - - - Action Required - - - - - {orders.map((order, index) => { - const pantry = order.request.pantry; - - return ( - - + + + + + No Orders + + + You have no {ORDER_STATUS_LABELS[status].toLowerCase()} orders + at this time. + + + ) : ( + <> + + + + - onOrderSelect(order.orderId)} - > - {order.orderId} - - - + - - {ORDER_STATUS_LABELS[order.status]} - - - + - - - {getInitials( - order.assignee.firstName, - order.assignee.lastName, - )} - - - - + - {pantry.pantryName} - - + - {formatDate(order.createdAt)}- - {order.deliveredAt && formatDate(order.deliveredAt)} - - + Dates + + + Action Required + - ); - })} - - - - {totalPages > 1 && ( - - onPageChange(e.page)} - > - - + + {orders.map((order, index) => { + const pantry = order.request.pantry; + + return ( + + + onOrderSelect(order.orderId)} + > + {order.orderId} + + + + + {ORDER_STATUS_LABELS[order.status]} + + + + + + {getInitials( + order.assignee.firstName, + order.assignee.lastName, + )} + + + + + {pantry.pantryName} + + + {formatDate(order.createdAt)}- + {order.deliveredAt && formatDate(order.deliveredAt)} + + + + ); + })} + + + + {totalPages > 1 && ( + + onPageChange(e.page)} > - - - - ( - + - {page.value} - - )} - /> + + + + ( + + {page.value} + + )} + /> - - - - - - + + + + + + + )} + )} )} diff --git a/apps/frontend/src/containers/adminPantryManagement.tsx b/apps/frontend/src/containers/adminPantryManagement.tsx index e807b5ce4..f5ff8b547 100644 --- a/apps/frontend/src/containers/adminPantryManagement.tsx +++ b/apps/frontend/src/containers/adminPantryManagement.tsx @@ -22,11 +22,12 @@ import { useAlert } from '../hooks/alert'; import { getInitials, USER_ICON_COLORS } from '@utils/utils'; import { RefrigeratedDonation } from '../types/pantryEnums'; import AssignVolunteersModal from '@components/forms/assignVolunteersModal'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { ROUTES } from '../routes'; const AdminPantryManagement: React.FC = () => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [currentPage, setCurrentPage] = useState(1); const [pantries, setPantries] = useState([]); @@ -61,6 +62,40 @@ const AdminPantryManagement: React.FC = () => { fetchPantries(); }, [setAlertMessage]); + // Pre-fill pantry filter from the volunteerId url param. The param is kept on + // success so the filter is reapplied on reload/back/refresh, and only cleared + // when the volunteer has no pantries or the fetch fails. + useEffect(() => { + const volunteerIdFromUrl = searchParams.get('volunteerId'); + if (!volunteerIdFromUrl) return; + + let cancelled = false; + (async () => { + try { + const assignedPantries = await ApiClient.getVolunteerPantries( + Number(volunteerIdFromUrl), + ); + if (cancelled) return; + if (assignedPantries.length === 0) { + setIsAlertSuccess(false); + setAlertMessage('This volunteer has no assigned pantries.'); + navigate(ROUTES.PANTRY_MANAGEMENT, { replace: true }); + return; + } + setSelectedPantries(assignedPantries.map((p) => p.pantryName)); + } catch { + if (cancelled) return; + setIsAlertSuccess(false); + setAlertMessage('Error fetching volunteer pantries'); + navigate(ROUTES.PANTRY_MANAGEMENT, { replace: true }); + } + })(); + + return () => { + cancelled = true; + }; + }, [searchParams, navigate, setAlertMessage]); + const handleAssignVolunteersSuccess = () => { setIsAlertSuccess(true); setAlertMessage('Successfully assigned volunteers'); @@ -357,14 +392,14 @@ const AdminPantryManagement: React.FC = () => { fontWeight={500} fontSize="12px" bgColor={ - pantry.refrigeratedDonation === RefrigeratedDonation.YES - ? 'neutral.100' - : 'neutral.200' + pantry.refrigeratedDonation === RefrigeratedDonation.NO + ? 'neutral.200' + : 'neutral.100' } > - {pantry.refrigeratedDonation === RefrigeratedDonation.YES - ? 'Refrigerator-Friendly' - : 'Not Refrigerator-Friendly'} + {pantry.refrigeratedDonation === RefrigeratedDonation.NO + ? 'Not Refrigerator-Friendly' + : 'Refrigerator-Friendly'} @@ -374,7 +409,12 @@ const AdminPantryManagement: React.FC = () => { textStyle="p2" variant="underline" textDecorationColor="neutral.700" - // TODO href or some functionality to view orders + cursor="pointer" + onClick={() => + navigate( + `${ROUTES.ADMIN_ORDER_MANAGEMENT}?pantryId=${pantry.pantryId}`, + ) + } > View Orders diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index 53e37450a..27ec4da93 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -362,7 +362,11 @@ const AssignedPantries: React.FC = () => { > { color="neutral.700" textStyle="p2" onClick={() => - navigate(ROUTES.VOLUNTEER_REQUEST_MANAGEMENT) + navigate( + `${ROUTES.VOLUNTEER_ORDER_MANAGEMENT}?pantryId=${pantry.pantryId}`, + ) } p={0} height="auto" diff --git a/apps/frontend/src/containers/volunteerManagement.tsx b/apps/frontend/src/containers/volunteerManagement.tsx index b0a2cb681..efe5caafa 100644 --- a/apps/frontend/src/containers/volunteerManagement.tsx +++ b/apps/frontend/src/containers/volunteerManagement.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { ROUTES } from '../routes'; import { Table, @@ -22,6 +23,7 @@ import { useAlert } from '../hooks/alert'; import { getInitials, USER_ICON_COLORS } from '@utils/utils'; const VolunteerManagement: React.FC = () => { + const navigate = useNavigate(); const [currentPage, setCurrentPage] = useState(1); const [volunteers, setVolunteers] = useState([]); const [searchName, setSearchName] = useState(''); @@ -186,7 +188,12 @@ const VolunteerManagement: React.FC = () => { textStyle="p2" variant="underline" textDecorationColor="neutral.700" - href={`${ROUTES.PANTRY_MANAGEMENT}/${volunteer.id}`} + cursor="pointer" + onClick={() => + navigate( + `${ROUTES.PANTRY_MANAGEMENT}?volunteerId=${volunteer.id}`, + ) + } > View Assigned Pantries diff --git a/apps/frontend/src/containers/volunteerOrderManagement.tsx b/apps/frontend/src/containers/volunteerOrderManagement.tsx index d23677005..432eed92b 100644 --- a/apps/frontend/src/containers/volunteerOrderManagement.tsx +++ b/apps/frontend/src/containers/volunteerOrderManagement.tsx @@ -196,6 +196,39 @@ const VolunteerOrderManagement: React.FC = () => { } }, [searchParams, statusOrders, navigate]); + // Pre-fill pantry filter from url param and then clear the param. + useEffect(() => { + const pantryIdFromUrl = searchParams.get('pantryId'); + + const allOrders = Object.values(statusOrders).flat(); + if (!pantryIdFromUrl || allOrders.length === 0) return; + + const matchedOrder = allOrders.find( + (o) => o.pantryId === Number(pantryIdFromUrl), + ); + const pantryName = matchedOrder?.pantryName; + + if (pantryName) { + setFilterStates((prev) => ({ + [OrderStatus.SHIPPED]: { + ...prev[OrderStatus.SHIPPED], + selectedPantries: [pantryName], + }, + [OrderStatus.PENDING]: { + ...prev[OrderStatus.PENDING], + selectedPantries: [pantryName], + }, + [OrderStatus.DELIVERED]: { + ...prev[OrderStatus.DELIVERED], + selectedPantries: [pantryName], + }, + })); + } else { + setAlertMessage('Selected pantry has no orders'); + navigate(ROUTES.VOLUNTEER_ORDER_MANAGEMENT, { replace: true }); + } + }, [searchParams, statusOrders, navigate, setAlertMessage]); + const resetPageForStatus = (status: OrderStatus) => { setCurrentPages((prev) => ({ ...prev, [status]: 1 })); }; @@ -433,7 +466,7 @@ const OrderStatusSection: React.FC = ({ - {orders.length === 0 ? ( + {orders.length === 0 && filterState.selectedPantries.length === 0 ? ( = ({ )} - - - - - Order # - - - Status - - - Assignee - - - Pantry - - - Dates - - - Action Required - - - - - {orders.map((order, index) => { - const needsAction = hasRequiredActions(order); - - return ( - - + + + + + No Orders + + + You have no {ORDER_STATUS_LABELS[status].toLowerCase()} orders + at this time. + + + ) : ( + <> + + + + - onOrderSelect(order.orderId)} - > - {order.orderId} - - - + - - {ORDER_STATUS_LABELS[status]} - - - + - - - {getInitials( - order.assignee.firstName, - order.assignee.lastName, - )} - - - - + - {order.pantryName} - - + - {`${formatDate(String(order.createdAt))}-`} - {order.deliveredAt && - formatDate(String(order.deliveredAt))} - - + - {order.assignee?.id === currentUser?.id && - (needsAction ? ( + Action Required + + + + + {orders.map((order, index) => { + const needsAction = hasRequiredActions(order); + + return ( + + onOpenActionModal(order)} + onClick={() => onOrderSelect(order.orderId)} > - Complete Required Actions + {order.orderId} - ) : ( - 'No Action Required' - ))} - - - ); - })} - - - - {totalPages > 1 && ( - - onPageChange(e.page)} - > - - + + + {ORDER_STATUS_LABELS[status]} + + + + + + {getInitials( + order.assignee.firstName, + order.assignee.lastName, + )} + + + + + {order.pantryName} + + + {`${formatDate(String(order.createdAt))}-`} + {order.deliveredAt && + formatDate(String(order.deliveredAt))} + + + {order.assignee?.id === currentUser?.id && + (needsAction ? ( + onOpenActionModal(order)} + > + Complete Required Actions + + ) : ( + 'No Action Required' + ))} + + + ); + })} + + + + {totalPages > 1 && ( + + onPageChange(e.page)} > - - - - ( - + - {page.value} - - )} - /> + + + + ( + + {page.value} + + )} + /> - - - - - - + + + + + + + )} + )} )} diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 8addbea64..d42b0b539 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -362,6 +362,7 @@ export type VolunteerOrder = { createdAt: string; shippedAt: string | null; deliveredAt: string | null; + pantryId: number; pantryName: string; assignee: OrderAssignee; actionCompletion?: VolunteerActionCompletion;