From af1269dcc8efc95c29c297371c3a8bb852bbae86 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 14 Jan 2026 20:06:20 -0500 Subject: [PATCH 1/9] refactoring backend for request entity having multiple orders --- .../foodRequests/request.controller.spec.ts | 8 +++--- .../src/foodRequests/request.controller.ts | 9 +++--- .../src/foodRequests/request.entity.ts | 4 +-- .../src/foodRequests/request.service.spec.ts | 28 ++++++++++--------- .../src/foodRequests/request.service.ts | 22 +++++++++------ 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index 71c35ca4..4e356837 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -107,7 +107,7 @@ describe('RequestsController', () => { requestId: 1, ...createBody, requestedAt: new Date(), - order: null, + orders: null, }; mockRequestsService.create.mockResolvedValueOnce( @@ -181,7 +181,7 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockResolvedValue({ requestId, pantryId: 1, - order: { orderId: 99 }, + orders: [{ orderId: 99 }], } as FoodRequest); mockOrdersService.updateStatus.mockResolvedValue(); @@ -230,7 +230,7 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockResolvedValue({ requestId, pantryId: 1, - order: { orderId: 100 }, + orders: [{ orderId: 100 }], } as FoodRequest); mockOrdersService.updateStatus.mockResolvedValue(); @@ -275,7 +275,7 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockResolvedValue({ requestId, pantryId: 1, - order: { orderId: 101 }, + orders: [{ orderId: 101 }], } as FoodRequest); mockOrdersService.updateStatus.mockResolvedValue(); diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 1f449491..0abf1922 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -16,7 +16,6 @@ import { AWSS3Service } from '../aws/aws-s3.service'; import { FilesInterceptor } from '@nestjs/platform-express'; import * as multer from 'multer'; import { OrdersService } from '../orders/order.service'; -import { Order } from '../orders/order.entity'; import { RequestSize } from './types'; import { OrderStatus } from '../orders/types'; @@ -158,9 +157,11 @@ export class RequestsController { ); const request = await this.requestsService.findOne(requestId); - await this.ordersService.updateStatus( - request.order.orderId, - OrderStatus.DELIVERED, + + await Promise.all( + request.orders.map((order) => + this.ordersService.updateStatus(order.orderId, OrderStatus.DELIVERED), + ), ); return this.requestsService.updateDeliveryDetails( diff --git a/apps/backend/src/foodRequests/request.entity.ts b/apps/backend/src/foodRequests/request.entity.ts index 06c2ce79..f745b804 100644 --- a/apps/backend/src/foodRequests/request.entity.ts +++ b/apps/backend/src/foodRequests/request.entity.ts @@ -46,6 +46,6 @@ export class FoodRequest { @Column({ name: 'photos', type: 'text', array: true, nullable: true }) photos: string[]; - @OneToMany(() => Order, (order) => order.request, { nullable: true }) - order: Order; + @OneToMany(() => Order, (order) => order.request) + orders: Order[]; } diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index cc69a5a3..b05013d0 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -21,7 +21,7 @@ const mockRequest: Partial = { dateReceived: null, feedback: null, photos: null, - order: null, + orders: null, }; describe('RequestsService', () => { @@ -166,7 +166,7 @@ describe('RequestsService', () => { dateReceived: null, feedback: null, photos: null, - order: null, + orders: null, }, { requestId: 3, @@ -178,7 +178,7 @@ describe('RequestsService', () => { dateReceived: null, feedback: null, photos: null, - order: null, + orders: null, }, ]; const pantryId = 1; @@ -213,7 +213,7 @@ describe('RequestsService', () => { const mockRequest2: Partial = { ...mockRequest, - order: mockOrder as Order, + orders: [mockOrder] as Order[], }; const requestId = 1; @@ -224,15 +224,15 @@ describe('RequestsService', () => { mockRequestsRepository.findOne.mockResolvedValueOnce( mockRequest2 as FoodRequest, ); + + const updatedOrder = { ...mockOrder, status: OrderStatus.DELIVERED }; + mockRequestsRepository.save.mockResolvedValueOnce({ ...mockRequest, dateReceived: deliveryDate, feedback, photos, - order: { - ...(mockOrder as Order), - status: OrderStatus.DELIVERED, - } as Order, + orders: [updatedOrder], } as FoodRequest); const result = await service.updateDeliveryDetails( @@ -247,7 +247,7 @@ describe('RequestsService', () => { dateReceived: deliveryDate, feedback, photos, - order: { ...mockOrder, status: 'delivered' }, + orders: [updatedOrder], }); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ @@ -260,7 +260,7 @@ describe('RequestsService', () => { dateReceived: deliveryDate, feedback, photos, - order: { ...mockOrder, status: 'delivered' }, + orders: [updatedOrder], }); }); @@ -304,7 +304,7 @@ describe('RequestsService', () => { feedback, photos, ), - ).rejects.toThrow('No associated order found for this request'); + ).rejects.toThrow('No associated orders found for this request'); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, @@ -327,7 +327,7 @@ describe('RequestsService', () => { }; const mockRequest2: Partial = { ...mockRequest, - order: mockOrder as Order, + orders: [mockOrder] as Order[], }; const requestId = 1; @@ -346,7 +346,9 @@ describe('RequestsService', () => { feedback, photos, ), - ).rejects.toThrow('No associated food manufacturer found for this order'); + ).rejects.toThrow( + 'No associated food manufacturer found for an associated order', + ); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 32e600ba..fe5d7f1c 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -87,22 +87,28 @@ export class RequestsService { throw new NotFoundException('Invalid request ID'); } - if (!request.order) { - throw new ConflictException('No associated order found for this request'); + if (!request.orders || request.orders.length == 0) { + throw new ConflictException( + 'No associated orders found for this request', + ); } - const order = request.order; + const orders = request.orders; - if (!order.shippedBy) { - throw new ConflictException( - 'No associated food manufacturer found for this order', - ); + for (const order of orders) { + if (!order.shippedBy) { + throw new ConflictException( + 'No associated food manufacturer found for an associated order', + ); + } } request.feedback = feedback; request.dateReceived = deliveryDate; request.photos = photos; - request.order.status = OrderStatus.DELIVERED; + request.orders.forEach((order) => { + order.status = OrderStatus.DELIVERED; + }); return await this.repo.save(request); } From 75a466191001b13be70ef670212078e1e8aa2876 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 14 Jan 2026 22:36:41 -0500 Subject: [PATCH 2/9] adding order details endpoint and placeholder order values for outdated request management frontend --- .../src/allocations/allocations.entity.ts | 5 +++ .../foodRequests/dtos/order-details.dto.ts | 15 ++++++++ .../src/foodRequests/request.controller.ts | 8 +++++ .../src/foodRequests/request.service.spec.ts | 20 +++++++---- .../src/foodRequests/request.service.ts | 35 +++++++++++++++++-- apps/backend/src/orders/order.entity.ts | 5 +++ apps/frontend/src/api/apiClient.ts | 5 +++ apps/frontend/src/containers/FormRequests.tsx | 16 ++++----- apps/frontend/src/types/types.ts | 15 +++++++- 9 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 apps/backend/src/foodRequests/dtos/order-details.dto.ts diff --git a/apps/backend/src/allocations/allocations.entity.ts b/apps/backend/src/allocations/allocations.entity.ts index a5a3c734..5a031df1 100644 --- a/apps/backend/src/allocations/allocations.entity.ts +++ b/apps/backend/src/allocations/allocations.entity.ts @@ -6,6 +6,7 @@ import { JoinColumn, } from 'typeorm'; import { DonationItem } from '../donationItems/donationItems.entity'; +import { Order } from '../orders/order.entity'; @Entity('allocations') export class Allocation { @@ -15,6 +16,10 @@ export class Allocation { @Column({ name: 'order_id', type: 'int' }) orderId: number; + @ManyToOne(() => Order, (order) => order.allocations) + @JoinColumn({ name: 'order_id' }) + order: Order; + @Column({ name: 'item_id', type: 'int', nullable: false }) itemId: number; diff --git a/apps/backend/src/foodRequests/dtos/order-details.dto.ts b/apps/backend/src/foodRequests/dtos/order-details.dto.ts new file mode 100644 index 00000000..2a0537df --- /dev/null +++ b/apps/backend/src/foodRequests/dtos/order-details.dto.ts @@ -0,0 +1,15 @@ +import { FoodType } from "../../donationItems/types"; +import { OrderStatus } from "../../orders/types"; + +export class OrderItemDetailsDto { + name: string; + quantity: number; + foodType: FoodType; +} + +export class OrderDetailsDto { + orderId: number; + status: OrderStatus; + foodManufacturerName: string; + items: OrderItemDetailsDto[]; +} diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 0abf1922..0336b5a1 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -18,6 +18,7 @@ import * as multer from 'multer'; import { OrdersService } from '../orders/order.service'; import { RequestSize } from './types'; import { OrderStatus } from '../orders/types'; +import { OrderDetailsDto } from './dtos/order-details.dto'; @Controller('requests') // @UseInterceptors() @@ -42,6 +43,13 @@ export class RequestsController { return this.requestsService.find(pantryId); } + @Get('/get-all-order-details/:requestId') + async getAllOrderDetailsFromRequest( + @Param('requestId', ParseIntPipe) requestId: number, + ) : Promise { + return this.requestsService.getOrderDetails(requestId); + } + @Post('/create') @ApiBody({ description: 'Details for creating a food request', diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index b05013d0..d691908d 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -11,6 +11,8 @@ import { OrderStatus } from '../orders/types'; const mockRequestsRepository = mock>(); const mockPantryRepository = mock>(); +const mockOrdersRepository = mock>(); + const mockRequest: Partial = { requestId: 1, @@ -46,6 +48,10 @@ describe('RequestsService', () => { provide: getRepositoryToken(Pantry), useValue: mockPantryRepository, }, + { + provide: getRepositoryToken(Order), + useValue: mockOrdersRepository, + }, ], }).compile(); @@ -74,7 +80,7 @@ describe('RequestsService', () => { expect(result).toEqual(mockRequest); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); }); @@ -89,7 +95,7 @@ describe('RequestsService', () => { expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); }); }); @@ -191,7 +197,7 @@ describe('RequestsService', () => { expect(result).toEqual(mockRequests.slice(0, 2)); expect(mockRequestsRepository.find).toHaveBeenCalledWith({ where: { pantryId }, - relations: ['order'], + relations: ['orders'], }); }); }); @@ -252,7 +258,7 @@ describe('RequestsService', () => { expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); expect(mockRequestsRepository.save).toHaveBeenCalledWith({ @@ -283,7 +289,7 @@ describe('RequestsService', () => { expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); }); @@ -308,7 +314,7 @@ describe('RequestsService', () => { expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); }); @@ -352,7 +358,7 @@ describe('RequestsService', () => { expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index fe5d7f1c..d00aca84 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -10,12 +10,15 @@ import { validateId } from '../utils/validation.utils'; import { RequestSize } from './types'; import { OrderStatus } from '../orders/types'; import { Pantry } from '../pantries/pantries.entity'; +import { Order } from '../orders/order.entity'; +import { OrderDetailsDto } from './dtos/order-details.dto'; @Injectable() export class RequestsService { constructor( @InjectRepository(FoodRequest) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, + @InjectRepository(Order) private orderRepo: Repository, ) {} async findOne(requestId: number): Promise { @@ -23,7 +26,7 @@ export class RequestsService { const request = await this.repo.findOne({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); if (!request) { @@ -32,6 +35,32 @@ export class RequestsService { return request; } + async getOrderDetails( + requestId: number, + ): Promise { + const orders = await this.orderRepo.find({ + where: { requestId }, + relations: { + foodManufacturer: true, + allocations: { + item: true, + }, + }, + }); + + return orders.map((order) => ({ + orderId: order.orderId, + status: order.status, + foodManufacturerName: order.foodManufacturer.foodManufacturerName, + items: order.allocations.map((allocation) => ({ + name: allocation.item.itemName, + quantity: allocation.allocatedQuantity, + foodType: allocation.item.foodType, + })), + })); + } + + async create( pantryId: number, requestedSize: RequestSize, @@ -66,7 +95,7 @@ export class RequestsService { return await this.repo.find({ where: { pantryId }, - relations: ['order'], + relations: ['orders'], }); } @@ -80,7 +109,7 @@ export class RequestsService { const request = await this.repo.findOne({ where: { requestId }, - relations: ['order'], + relations: ['orders'], }); if (!request) { diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 4c38457b..7c40fdb4 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -5,11 +5,13 @@ import { CreateDateColumn, ManyToOne, JoinColumn, + OneToMany, } from 'typeorm'; import { FoodRequest } from '../foodRequests/request.entity'; import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; import { OrderStatus } from './types'; +import { Allocation } from '../allocations/allocations.entity'; @Entity('orders') export class Order { @@ -72,4 +74,7 @@ export class Order { nullable: true, }) deliveredAt: Date | null; + + @OneToMany(() => Allocation, (allocation) => allocation.order) + allocations: Allocation[]; } diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index f62efd8f..a677189a 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -11,6 +11,7 @@ import { Pantry, PantryApplicationDto, UserDto, + OrderDetails, } from 'types/types'; const defaultBaseUrl = @@ -193,6 +194,10 @@ export class ApiClient { return this.axiosInstance.get(`api/orders/${orderId}`) as Promise; } + public async getOrderDetailsListFromRequest(requestId: number): Promise { + return this.axiosInstance.get(`api/requests/get-all-order-details/${requestId}`) as Promise; + } + async getAllAllocationsByOrder(orderId: number): Promise { return this.axiosInstance .get(`api/orders/${orderId}/allocations`) diff --git a/apps/frontend/src/containers/FormRequests.tsx b/apps/frontend/src/containers/FormRequests.tsx index 50d20601..e747a55b 100644 --- a/apps/frontend/src/containers/FormRequests.tsx +++ b/apps/frontend/src/containers/FormRequests.tsx @@ -150,32 +150,32 @@ const FormRequests: React.FC = () => { - {request.order?.orderId ? ( + {request.orders?.[0]?.orderId ? ( ) : ( 'N/A' )} {formatDate(request.requestedAt)} - {request.order?.status ?? 'pending'} + {request.orders?.[0]?.status ?? 'pending'} - {request.order?.status === 'pending' + {request.orders?.[0]?.status === 'pending' ? 'N/A' - : request.order?.shippedBy ?? 'N/A'} + : request.orders?.[0]?.shippedBy ?? 'N/A'} {formatReceivedDate(request.dateReceived)} - {!request.order || request.order?.status === 'pending' ? ( + {!request.orders?.[0] || request.orders?.[0]?.status === 'pending' ? ( Awaiting Order Assignment - ) : request.order?.status === 'delivered' ? ( + ) : request.orders?.[0]?.status === 'delivered' ? ( Food Request is Already Delivered ) : ( - - {previousRequest && ( - <> - - - - )} - - - - - setSortBy(e.target.value as 'mostRecent' | 'oldest' | 'confirmed') - } - > - - - - - - - - + + Food Request Management + + + + {previousRequest && ( + <> + + + + )} + + - Request ID - Order ID - Date Requested - Status - Shipped By - Date Fulfilled - Actions + Request # + Status + Date Requested - {sortedRequests.map((request) => ( + {paginatedRequests.map((request) => ( - - + - {request.orders?.[0]?.orderId ? ( - + Closed + ) : ( - 'N/A' - )} - - {formatDate(request.requestedAt)} - - {request.orders?.[0]?.status ?? 'pending'} - - - {request.orders?.[0]?.status === 'pending' - ? 'N/A' - : request.orders?.[0]?.shippedBy ?? 'N/A'} - - - {formatReceivedDate(request.dateReceived)} - - - {!request.orders?.[0] || - request.orders?.[0]?.status === 'pending' ? ( - Awaiting Order Assignment - ) : request.orders?.[0]?.status === 'delivered' ? ( - Food Request is Already Delivered - ) : ( - + Active + )} + {formatDate(request.requestedAt)} ))} {openReadOnlyRequest && ( @@ -216,7 +175,35 @@ const FormRequests: React.FC = () => { )} - + + setCurrentPage(page)}> + + + setCurrentPage((prev) => Math.max(prev - 1, 1))}> + + + + + ( + setCurrentPage(page.value)} + > + {page.value} + + )} + /> + + + setCurrentPage((prev) => Math.min(prev + 1, Math.ceil(requests.length / pageSize)))}> + + + + + + + ); }; From 9387f5b423e14ee3194246a9fa7e7b8c699c2317 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 20 Jan 2026 10:49:45 -0500 Subject: [PATCH 6/9] frontend for request details and associated order details --- apps/frontend/src/api/apiClient.ts | 2 +- .../components/forms/requestDetailsModal.tsx | 270 ++++++++++++++++++ apps/frontend/src/containers/FormRequests.tsx | 31 +- 3 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 apps/frontend/src/components/forms/requestDetailsModal.tsx diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 97f997a1..a5b60fb9 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -199,7 +199,7 @@ export class ApiClient { ): Promise { return this.axiosInstance.get( `api/requests/get-all-order-details/${requestId}`, - ) as Promise; + ).then((response) => response.data) as Promise; } async getAllAllocationsByOrder(orderId: number): Promise { diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx new file mode 100644 index 00000000..a6f3cc7e --- /dev/null +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -0,0 +1,270 @@ +import apiClient from "@api/apiClient" +import { FoodRequest, FoodTypes, OrderDetails, OrderItemDetails } from "types/types" +import { OrderStatus } from "../../types/types"; +import React, { useState, useEffect, useMemo } from 'react'; +import { + Flex, + Box, + Menu, + Text, + Dialog, + Tag, + Field, + CloseButton, + Tabs, + Badge, + Pagination, + ButtonGroup, + IconButton, +} from '@chakra-ui/react'; +import { ChevronRight, ChevronLeft } from 'lucide-react'; + +interface RequestDetailsModalProps { + request: FoodRequest; + isOpen: boolean; + onClose: () => void; + pantryId: number; +} + +const RequestDetailsModal: React.FC = ({ + request, + isOpen, + onClose, + pantryId +}) => { + const [orderDetailsList, setOrderDetailsList] = useState([]); + const [pantryName, setPantryName] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + + const requestedSize = request.requestedSize; + const selectedItems = request.requestedItems; + const additionalNotes = request.additionalInformation; + + useEffect(() => { + const fetchRequestOrderDetails = async () => { + try { + const orderDetailsList = await apiClient.getOrderDetailsListFromRequest(request.requestId); + const sortedData = orderDetailsList.slice().sort((a, b) => b.orderId - a.orderId); + setOrderDetailsList(sortedData); + } catch (error) { + console.error('Error fetching order details', error); + } + }; + fetchRequestOrderDetails(); + }, [isOpen, request.requestId]); + + useEffect(() => { + const fetchPantryData = async () => { + try { + const pantry = await apiClient.getPantry(pantryId); + setPantryName(pantry.pantryName); + } catch (error) { + console.error('Error fetching pantry data', error); + } + }; + fetchPantryData(); + }, [pantryId]); + + const currentOrder = orderDetailsList[currentPage - 1]; + + const groupedOrderItemsByType = useMemo(() => { + if (!currentOrder) return {}; + + return currentOrder.items.reduce((acc: Record<(typeof FoodTypes)[number], OrderItemDetails[]>, item) => { + if (!acc[item.foodType]) acc[item.foodType] = []; + acc[item.foodType].push(item); + return acc; + }, {} as Record<(typeof FoodTypes)[number], OrderItemDetails[]>); + }, [currentOrder]); + + return ( + { + if (!e.open) onClose(); + }} + closeOnInteractOutside + > + + + + + + Food Request #{request.requestId} + + + + + + {pantryName} + + + + + + Request Details + + + Associated Orders + + + + + + + Size of Shipment + + + + + {requestedSize} + + + + + + + + Food Type(s) + + + + + {selectedItems.length > 0 && ( + + {selectedItems.map((item) => ( + + {item} + + + ))} + + )} + + + + + + Additional Information + + + + {additionalNotes} + + + + + + {currentOrder && ( + + + + Order {currentOrder.orderId} - + + {" "} Fulfilled by {currentOrder.foodManufacturerName} + + + {currentOrder.status === OrderStatus.DELIVERED ? ( + Received + ) : ( + In Progress + )} + + {Object.entries(groupedOrderItemsByType as Record).map(([foodType, items]) => ( + + + {foodType} + + {items.map((item) => ( + + {item.name} + + + + {item.quantity} + + + + ))} + + ))} + Tracking + No tracking link available at this time + + )} + + + setCurrentPage(page)}> + + + setCurrentPage((prev) => Math.max(prev - 1, 1))}> + + + + + ( + setCurrentPage(page.value)} + > + {page.value} + + )} + /> + + + setCurrentPage((prev) => Math.min(prev + 1, Math.ceil(orderDetailsList.length)))}> + + + + + + + + + + + + + + + + + + + + ); +}; + + +export default RequestDetailsModal; \ No newline at end of file diff --git a/apps/frontend/src/containers/FormRequests.tsx b/apps/frontend/src/containers/FormRequests.tsx index 8180d0e2..b423348b 100644 --- a/apps/frontend/src/containers/FormRequests.tsx +++ b/apps/frontend/src/containers/FormRequests.tsx @@ -16,9 +16,8 @@ import { } from '@chakra-ui/react'; import { ChevronRight, ChevronLeft } from 'lucide-react'; import FoodRequestFormModal from '@components/forms/requestFormModal'; -import DeliveryConfirmationModal from '@components/forms/deliveryConfirmationModal'; -import OrderInformationModal from '@components/forms/orderInformationModal'; import { OrderStatus, FoodRequest } from '../types/types'; +import RequestDetailsModal from '@components/forms/requestDetailsModal'; import { formatDate } from '@utils/utils'; import ApiClient from '@api/apiClient'; @@ -35,19 +34,11 @@ const FormRequests: React.FC = () => { const { pantryId: pantryIdParam } = useParams<{ pantryId: string }>(); const pantryId = parseInt(pantryIdParam!, 10); - const [openDeliveryRequestId, setOpenDeliveryRequestId] = useState< - number | null - >(null); const [openReadOnlyRequest, setOpenReadOnlyRequest] = useState(null); - const [openOrderId, setOpenOrderId] = useState(null); const pageSize = 8; - useEffect(() => { - setCurrentPage(1); - }, []); - useEffect(() => { const fetchRequests = async () => { if (pantryId) { @@ -150,29 +141,13 @@ const FormRequests: React.FC = () => { ))} {openReadOnlyRequest && ( - setOpenReadOnlyRequest(null)} pantryId={pantryId} /> )} - {openOrderId && ( - setOpenOrderId(null)} - /> - )} - {openDeliveryRequestId && ( - setOpenDeliveryRequestId(null)} - pantryId={pantryId} - /> - )} From bbf92eb831c474a07c7a4d3deaba2917db07168a Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 20 Jan 2026 12:01:50 -0500 Subject: [PATCH 7/9] prettier --- apps/frontend/src/api/apiClient.ts | 6 +- .../components/forms/requestDetailsModal.tsx | 546 ++++++++++-------- apps/frontend/src/containers/FormRequests.tsx | 181 +++--- 3 files changed, 435 insertions(+), 298 deletions(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index a5b60fb9..f9388bc1 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -197,9 +197,9 @@ export class ApiClient { public async getOrderDetailsListFromRequest( requestId: number, ): Promise { - return this.axiosInstance.get( - `api/requests/get-all-order-details/${requestId}`, - ).then((response) => response.data) as Promise; + return this.axiosInstance + .get(`api/requests/get-all-order-details/${requestId}`) + .then((response) => response.data) as Promise; } async getAllAllocationsByOrder(orderId: number): Promise { diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index a6f3cc7e..88153e95 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -1,6 +1,11 @@ -import apiClient from "@api/apiClient" -import { FoodRequest, FoodTypes, OrderDetails, OrderItemDetails } from "types/types" -import { OrderStatus } from "../../types/types"; +import apiClient from '@api/apiClient'; +import { + FoodRequest, + FoodTypes, + OrderDetails, + OrderItemDetails, +} from 'types/types'; +import { OrderStatus } from '../../types/types'; import React, { useState, useEffect, useMemo } from 'react'; import { Flex, @@ -20,251 +25,332 @@ import { import { ChevronRight, ChevronLeft } from 'lucide-react'; interface RequestDetailsModalProps { - request: FoodRequest; - isOpen: boolean; - onClose: () => void; - pantryId: number; + request: FoodRequest; + isOpen: boolean; + onClose: () => void; + pantryId: number; } const RequestDetailsModal: React.FC = ({ - request, - isOpen, - onClose, - pantryId + request, + isOpen, + onClose, + pantryId, }) => { - const [orderDetailsList, setOrderDetailsList] = useState([]); - const [pantryName, setPantryName] = useState(''); - const [currentPage, setCurrentPage] = useState(1); + const [orderDetailsList, setOrderDetailsList] = useState([]); + const [pantryName, setPantryName] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + + const requestedSize = request.requestedSize; + const selectedItems = request.requestedItems; + const additionalNotes = request.additionalInformation; + + useEffect(() => { + const fetchRequestOrderDetails = async () => { + try { + const orderDetailsList = await apiClient.getOrderDetailsListFromRequest( + request.requestId, + ); + const sortedData = orderDetailsList + .slice() + .sort((a, b) => b.orderId - a.orderId); + setOrderDetailsList(sortedData); + } catch (error) { + console.error('Error fetching order details', error); + } + }; + fetchRequestOrderDetails(); + }, [isOpen, request.requestId]); - const requestedSize = request.requestedSize; - const selectedItems = request.requestedItems; - const additionalNotes = request.additionalInformation; - - useEffect(() => { - const fetchRequestOrderDetails = async () => { - try { - const orderDetailsList = await apiClient.getOrderDetailsListFromRequest(request.requestId); - const sortedData = orderDetailsList.slice().sort((a, b) => b.orderId - a.orderId); - setOrderDetailsList(sortedData); - } catch (error) { - console.error('Error fetching order details', error); - } - }; - fetchRequestOrderDetails(); - }, [isOpen, request.requestId]); + useEffect(() => { + const fetchPantryData = async () => { + try { + const pantry = await apiClient.getPantry(pantryId); + setPantryName(pantry.pantryName); + } catch (error) { + console.error('Error fetching pantry data', error); + } + }; + fetchPantryData(); + }, [pantryId]); - useEffect(() => { - const fetchPantryData = async () => { - try { - const pantry = await apiClient.getPantry(pantryId); - setPantryName(pantry.pantryName); - } catch (error) { - console.error('Error fetching pantry data', error); - } - }; - fetchPantryData(); - }, [pantryId]); + const currentOrder = orderDetailsList[currentPage - 1]; - const currentOrder = orderDetailsList[currentPage - 1]; + const groupedOrderItemsByType = useMemo(() => { + if (!currentOrder) return {}; + + return currentOrder.items.reduce( + (acc: Record<(typeof FoodTypes)[number], OrderItemDetails[]>, item) => { + if (!acc[item.foodType]) acc[item.foodType] = []; + acc[item.foodType].push(item); + return acc; + }, + {} as Record<(typeof FoodTypes)[number], OrderItemDetails[]>, + ); + }, [currentOrder]); - const groupedOrderItemsByType = useMemo(() => { - if (!currentOrder) return {}; + return ( + { + if (!e.open) onClose(); + }} + closeOnInteractOutside + > + + + + + + Food Request #{request.requestId} + + + + + {pantryName} + - return currentOrder.items.reduce((acc: Record<(typeof FoodTypes)[number], OrderItemDetails[]>, item) => { - if (!acc[item.foodType]) acc[item.foodType] = []; - acc[item.foodType].push(item); - return acc; - }, {} as Record<(typeof FoodTypes)[number], OrderItemDetails[]>); - }, [currentOrder]); - - return ( - { - if (!e.open) onClose(); - }} - closeOnInteractOutside - > - - - - - - Food Request #{request.requestId} - - - - - - {pantryName} + + + + Request Details + + + Associated Orders + + + + + + + Size of Shipment - - - - - Request Details - - - Associated Orders - - - - - - - Size of Shipment - - - - - {requestedSize} - - - - - - - - Food Type(s) - - - - - {selectedItems.length > 0 && ( - - {selectedItems.map((item) => ( - - {item} - - - ))} - - )} - - - - - - Additional Information - - - - {additionalNotes} - - - + + + + {requestedSize} + + + - - {currentOrder && ( - - - - Order {currentOrder.orderId} - - - {" "} Fulfilled by {currentOrder.foodManufacturerName} - - - {currentOrder.status === OrderStatus.DELIVERED ? ( - Received - ) : ( - In Progress - )} - - {Object.entries(groupedOrderItemsByType as Record).map(([foodType, items]) => ( - - - {foodType} - - {items.map((item) => ( - - {item.name} + + + + Food Type(s) + + - + {selectedItems.length > 0 && ( + + {selectedItems.map((item) => ( + + {item} + + ))} + + )} + - {item.quantity} - - - - ))} - - ))} - Tracking - No tracking link available at this time - - )} + + + + Additional Information + + + + {additionalNotes} + + + - - setCurrentPage(page)}> - - - setCurrentPage((prev) => Math.max(prev - 1, 1))}> - - - + + {currentOrder && ( + + + + Order {currentOrder.orderId} - + + {' '} + Fulfilled by {currentOrder.foodManufacturerName} + + + {currentOrder.status === OrderStatus.DELIVERED ? ( + + Received + + ) : ( + + In Progress + + )} + + {Object.entries( + groupedOrderItemsByType as Record< + string, + OrderItemDetails[] + >, + ).map(([foodType, items]) => ( + + + {foodType} + + {items.map((item) => ( + + + {item.name} + - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> + - - setCurrentPage((prev) => Math.min(prev + 1, Math.ceil(orderDetailsList.length)))}> - - - - - - - - + + {item.quantity} + + + ))} + + ))} + + Tracking + + + No tracking link available at this time + + + )} + + setCurrentPage(page)} + > + + + + setCurrentPage((prev) => Math.max(prev - 1, 1)) + } + > + + + - - - - - - - - + ( + setCurrentPage(page.value)} + > + {page.value} + + )} + /> - ); + + + setCurrentPage((prev) => + Math.min( + prev + 1, + Math.ceil(orderDetailsList.length), + ), + ) + } + > + + + + + + + + + + + + + + + + ); }; - -export default RequestDetailsModal; \ No newline at end of file +export default RequestDetailsModal; diff --git a/apps/frontend/src/containers/FormRequests.tsx b/apps/frontend/src/containers/FormRequests.tsx index b423348b..502e0cd7 100644 --- a/apps/frontend/src/containers/FormRequests.tsx +++ b/apps/frontend/src/containers/FormRequests.tsx @@ -44,7 +44,9 @@ const FormRequests: React.FC = () => { if (pantryId) { try { const data = await ApiClient.getPantryRequests(pantryId); - const sortedData = data.slice().sort((a, b) => b.requestId - a.requestId); + const sortedData = data + .slice() + .sort((a, b) => b.requestId - a.requestId); setRequests(sortedData); if (sortedData.length > 0) { @@ -61,61 +63,93 @@ const FormRequests: React.FC = () => { const paginatedRequests = requests.slice( (currentPage - 1) * pageSize, - currentPage * pageSize + currentPage * pageSize, ); return ( - Food Request Management - - - - {previousRequest && ( - <> - - - - )} - - + + + {previousRequest && ( + <> + + + + )} + + - Request # - Status - Date Requested + + Request # + + + Status + + + Date Requested + {paginatedRequests.map((request) => ( - setOpenReadOnlyRequest(request)}> + setOpenReadOnlyRequest(request)} + > {request.requestId} - {request.orders?.every(order => order.status === OrderStatus.DELIVERED) ? ( + {request.orders?.every( + (order) => order.status === OrderStatus.DELIVERED, + ) ? ( { )} - {formatDate(request.requestedAt)} + + {formatDate(request.requestedAt)} + ))} {openReadOnlyRequest && ( @@ -151,33 +187,48 @@ const FormRequests: React.FC = () => { - setCurrentPage(page)}> - - - setCurrentPage((prev) => Math.max(prev - 1, 1))}> - - - - - ( - setCurrentPage(page.value)} - > - {page.value} - - )} - /> + setCurrentPage(page)} + > + + + setCurrentPage((prev) => Math.max(prev - 1, 1))} + > + + + - - setCurrentPage((prev) => Math.min(prev + 1, Math.ceil(requests.length / pageSize)))}> - + ( + setCurrentPage(page.value)} + > + {page.value} - - - - + )} + /> + + + + setCurrentPage((prev) => + Math.min(prev + 1, Math.ceil(requests.length / pageSize)), + ) + } + > + + + + + + ); }; From d5a07ed8dc6110e745c695be5a5562d01a2e522d Mon Sep 17 00:00:00 2001 From: Justin Wang <74576640+Juwang110@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:04:43 -0500 Subject: [PATCH 8/9] Making frontend request management up to date with backend pr (#86) * minor refactoring based on comments * minor changes * prettier --- .../src/allocations/allocations.entity.ts | 2 +- .../foodRequests/request.controller.spec.ts | 4 +-- .../src/foodRequests/request.controller.ts | 31 +++++++++++++------ .../src/foodRequests/request.entity.ts | 2 +- .../src/foodRequests/request.service.ts | 5 +-- apps/frontend/src/api/apiClient.ts | 2 +- apps/frontend/src/types/types.ts | 19 +++++++++++- 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/apps/backend/src/allocations/allocations.entity.ts b/apps/backend/src/allocations/allocations.entity.ts index 5a031df1..26985283 100644 --- a/apps/backend/src/allocations/allocations.entity.ts +++ b/apps/backend/src/allocations/allocations.entity.ts @@ -13,7 +13,7 @@ export class Allocation { @PrimaryGeneratedColumn({ name: 'allocation_id' }) allocationId: number; - @Column({ name: 'order_id', type: 'int' }) + @Column({ name: 'order_id', type: 'int', nullable: false }) orderId: number; @ManyToOne(() => Order, (order) => order.allocations) diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index 51f8d987..334f4019 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -94,9 +94,9 @@ describe('RequestsController', () => { }); }); - describe('GET /get-all-order-details/:requestId', () => { + describe('GET /all-order-details/:requestId', () => { it('should call requestsService.getOrderDetails and return all associated orders and their details', async () => { - const mockOrderDetails = [ + const mockOrderDetails: OrderDetailsDto[] = [ { orderId: 10, status: OrderStatus.DELIVERED, diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 1fc402aa..84cc1060 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -8,6 +8,8 @@ import { UploadedFiles, UseInterceptors, BadRequestException, + NotFoundException, + ConflictException, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { RequestsService } from './request.service'; @@ -43,7 +45,7 @@ export class RequestsController { return this.requestsService.find(pantryId); } - @Get('/get-all-order-details/:requestId') + @Get('/all-order-details/:requestId') async getAllOrderDetailsFromRequest( @Param('requestId', ParseIntPipe) requestId: number, ): Promise { @@ -116,6 +118,7 @@ export class RequestsController { ); } + //TODO: delete endpoint, here temporarily as a logic reference for order status impl. @Post('/:requestId/confirm-delivery') @ApiBody({ description: 'Details for a confirmation form', @@ -164,19 +167,29 @@ export class RequestsController { photos?.length, ); - const request = await this.requestsService.findOne(requestId); + const updatedRequest = await this.requestsService.updateDeliveryDetails( + requestId, + formattedDate, + body.feedback, + uploadedPhotoUrls, + ); + + if (!updatedRequest) { + throw new NotFoundException('Invalid request ID'); + } + + if (!updatedRequest.orders || updatedRequest.orders.length == 0) { + throw new ConflictException( + 'No associated orders found for this request', + ); + } await Promise.all( - request.orders.map((order) => + updatedRequest.orders.map((order) => this.ordersService.updateStatus(order.orderId, OrderStatus.DELIVERED), ), ); - return this.requestsService.updateDeliveryDetails( - requestId, - formattedDate, - body.feedback, - uploadedPhotoUrls, - ); + return updatedRequest; } } diff --git a/apps/backend/src/foodRequests/request.entity.ts b/apps/backend/src/foodRequests/request.entity.ts index f745b804..9864b4cc 100644 --- a/apps/backend/src/foodRequests/request.entity.ts +++ b/apps/backend/src/foodRequests/request.entity.ts @@ -46,6 +46,6 @@ export class FoodRequest { @Column({ name: 'photos', type: 'text', array: true, nullable: true }) photos: string[]; - @OneToMany(() => Order, (order) => order.request) + @OneToMany(() => Order, (order) => order.request, { nullable: true }) orders: Order[]; } diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 51b726dd..82eb06f7 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -43,7 +43,7 @@ export class RequestsService { }); if (!requestExists) { - throw new Error(`Request ${requestId} not found`); + throw new NotFoundException(`Request ${requestId} not found`); } const orders = await this.orderRepo.find({ @@ -146,9 +146,6 @@ export class RequestsService { request.feedback = feedback; request.dateReceived = deliveryDate; request.photos = photos; - request.orders.forEach((order) => { - order.status = OrderStatus.DELIVERED; - }); return await this.repo.save(request); } diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index f9388bc1..1c87bb48 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -204,7 +204,7 @@ export class ApiClient { async getAllAllocationsByOrder(orderId: number): Promise { return this.axiosInstance - .get(`api/orders/${orderId}/allocations`) + .get(`/api/orders/${orderId}/allocations`) .then((response) => response.data); } diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index f50ad540..84bab5e5 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -135,6 +135,23 @@ export const FoodTypes = [ 'Quinoa', ] as const; +export enum FoodType { + DAIRY_FREE_ALTERNATIVES = 'Dairy-Free Alternatives', + DRIED_BEANS = 'Dried Beans (Gluten-Free, Nut-Free)', + GLUTEN_FREE_BAKING_PANCAKE_MIXES = 'Gluten-Free Baking/Pancake Mixes', + GLUTEN_FREE_BREAD = 'Gluten-Free Bread', + GLUTEN_FREE_TORTILLAS = 'Gluten-Free Tortillas', + GRANOLA = 'Granola', + MASA_HARINA_FLOUR = 'Masa Harina Flour', + NUT_FREE_GRANOLA_BARS = 'Nut-Free Granola Bars', + OLIVE_OIL = 'Olive Oil', + REFRIGERATED_MEALS = 'Refrigerated Meals', + RICE_NOODLES = 'Rice Noodles', + SEED_BUTTERS = 'Seed Butters (Peanut Butter Alternative)', + WHOLE_GRAIN_COOKIES = 'Whole-Grain Cookies', + QUINOA = 'Quinoa', +} + export interface User { id: number; role: string; @@ -180,7 +197,7 @@ export interface Order { export interface OrderItemDetails { name: string; quantity: number; - foodType: (typeof FoodTypes)[number]; + foodType: FoodType; } export interface OrderDetails { From 3bdc25406598ea803fb52fc1027b9d36608dedf6 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sat, 24 Jan 2026 13:06:47 -0500 Subject: [PATCH 9/9] fixing apiclient route naming --- apps/frontend/src/api/apiClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 1c87bb48..4eccd675 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -198,7 +198,7 @@ export class ApiClient { requestId: number, ): Promise { return this.axiosInstance - .get(`api/requests/get-all-order-details/${requestId}`) + .get(`api/requests/all-order-details/${requestId}`) .then((response) => response.data) as Promise; }