diff --git a/apps/backend/src/allocations/allocations.entity.ts b/apps/backend/src/allocations/allocations.entity.ts index 26985283..a5a3c734 100644 --- a/apps/backend/src/allocations/allocations.entity.ts +++ b/apps/backend/src/allocations/allocations.entity.ts @@ -6,20 +6,15 @@ import { JoinColumn, } from 'typeorm'; import { DonationItem } from '../donationItems/donationItems.entity'; -import { Order } from '../orders/order.entity'; @Entity('allocations') export class Allocation { @PrimaryGeneratedColumn({ name: 'allocation_id' }) allocationId: number; - @Column({ name: 'order_id', type: 'int', nullable: false }) + @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 deleted file mode 100644 index 21d360ec..00000000 --- a/apps/backend/src/foodRequests/dtos/order-details.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -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.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index 45b0d2d2..71c35ca4 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -8,9 +8,6 @@ import { Readable } from 'stream'; import { FoodRequest } from './request.entity'; import { RequestSize } from './types'; import { OrderStatus } from '../orders/types'; -import { FoodType } from '../donationItems/types'; -import { OrderDetailsDto } from './dtos/order-details.dto'; -import { Order } from '../orders/order.entity'; const mockRequestsService = mock(); const mockOrdersService = mock(); @@ -29,7 +26,6 @@ describe('RequestsController', () => { mockRequestsService.find.mockReset(); mockRequestsService.create.mockReset(); mockRequestsService.updateDeliveryDetails?.mockReset(); - mockRequestsService.getOrderDetails.mockReset(); mockAWSS3Service.upload.mockReset(); mockOrdersService.updateStatus.mockReset(); @@ -95,55 +91,6 @@ describe('RequestsController', () => { }); }); - describe('GET /all-order-details/:requestId', () => { - it('should call requestsService.getOrderDetails and return all associated orders and their details', async () => { - const mockOrderDetails: OrderDetailsDto[] = [ - { - orderId: 10, - status: OrderStatus.DELIVERED, - foodManufacturerName: 'Test Manufacturer', - items: [ - { - name: 'Rice', - quantity: 5, - foodType: FoodType.GRANOLA, - }, - { - name: 'Beans', - quantity: 3, - foodType: FoodType.DRIED_BEANS, - }, - ], - }, - { - orderId: 11, - status: OrderStatus.PENDING, - foodManufacturerName: 'Another Manufacturer', - items: [ - { - name: 'Milk', - quantity: 2, - foodType: FoodType.DAIRY_FREE_ALTERNATIVES, - }, - ], - }, - ]; - - const requestId = 1; - - mockRequestsService.getOrderDetails.mockResolvedValueOnce( - mockOrderDetails as OrderDetailsDto[], - ); - - const result = await controller.getAllOrderDetailsFromRequest(requestId); - - expect(result).toEqual(mockOrderDetails); - expect(mockRequestsService.getOrderDetails).toHaveBeenCalledWith( - requestId, - ); - }); - }); - describe('POST /create', () => { it('should call requestsService.create and return the created food request', async () => { const createBody: Partial = { @@ -160,7 +107,7 @@ describe('RequestsController', () => { requestId: 1, ...createBody, requestedAt: new Date(), - orders: null, + order: null, }; mockRequestsService.create.mockResolvedValueOnce( @@ -234,21 +181,17 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockResolvedValue({ requestId, pantryId: 1, - orders: [{ orderId: 99 }], + order: { orderId: 99 }, } as FoodRequest); mockOrdersService.updateStatus.mockResolvedValue(); - const order = new Order(); - order.orderId = 99; - const updatedRequest: Partial = { requestId, pantryId: 1, dateReceived: new Date(body.dateReceived), feedback: body.feedback, photos: uploadedUrls, - orders: [order], }; mockRequestsService.updateDeliveryDetails.mockResolvedValue( @@ -259,6 +202,8 @@ describe('RequestsController', () => { expect(mockAWSS3Service.upload).toHaveBeenCalledWith(photos); + expect(mockRequestsService.findOne).toHaveBeenCalledWith(requestId); + expect(mockOrdersService.updateStatus).toHaveBeenCalledWith( 99, OrderStatus.DELIVERED, @@ -285,21 +230,17 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockResolvedValue({ requestId, pantryId: 1, - orders: [{ orderId: 100 }], + order: { orderId: 100 }, } as FoodRequest); mockOrdersService.updateStatus.mockResolvedValue(); - const order = new Order(); - order.orderId = 100; - const updatedRequest: Partial = { requestId, pantryId: 1, dateReceived: new Date(body.dateReceived), feedback: body.feedback, photos: [], - orders: [order], }; mockRequestsService.updateDeliveryDetails.mockResolvedValue( @@ -309,6 +250,7 @@ describe('RequestsController', () => { const result = await controller.confirmDelivery(requestId, body); expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); + expect(mockRequestsService.findOne).toHaveBeenCalledWith(requestId); expect(mockOrdersService.updateStatus).toHaveBeenCalledWith( 100, OrderStatus.DELIVERED, @@ -333,21 +275,17 @@ describe('RequestsController', () => { mockRequestsService.findOne.mockResolvedValue({ requestId, pantryId: 1, - orders: [{ orderId: 101 }], + order: { orderId: 101 }, } as FoodRequest); mockOrdersService.updateStatus.mockResolvedValue(); - const order = new Order(); - order.orderId = 101; - const updatedRequest: Partial = { requestId, pantryId: 1, dateReceived: new Date(body.dateReceived), feedback: body.feedback, photos: [], - orders: [order], }; mockRequestsService.updateDeliveryDetails.mockResolvedValue( @@ -357,6 +295,7 @@ describe('RequestsController', () => { const result = await controller.confirmDelivery(requestId, body, []); expect(mockAWSS3Service.upload).not.toHaveBeenCalled(); + expect(mockRequestsService.findOne).toHaveBeenCalledWith(requestId); expect(mockOrdersService.updateStatus).toHaveBeenCalledWith( 101, OrderStatus.DELIVERED, diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index ec6dc0f0..1f449491 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -8,7 +8,6 @@ import { UploadedFiles, UseInterceptors, BadRequestException, - NotFoundException, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { RequestsService } from './request.service'; @@ -17,9 +16,9 @@ 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'; -import { OrderDetailsDto } from './dtos/order-details.dto'; @Controller('requests') // @UseInterceptors() @@ -44,13 +43,6 @@ export class RequestsController { return this.requestsService.find(pantryId); } - @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', @@ -117,7 +109,6 @@ 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', @@ -166,29 +157,17 @@ export class RequestsController { photos?.length, ); - const updatedRequest = await this.requestsService.updateDeliveryDetails( + const request = await this.requestsService.findOne(requestId); + await this.ordersService.updateStatus( + request.order.orderId, + OrderStatus.DELIVERED, + ); + + return 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 NotFoundException( - 'No associated orders found for this request', - ); - } - - await Promise.all( - updatedRequest.orders.map((order) => - this.ordersService.updateStatus(order.orderId, OrderStatus.DELIVERED), - ), - ); - - return updatedRequest; } } diff --git a/apps/backend/src/foodRequests/request.entity.ts b/apps/backend/src/foodRequests/request.entity.ts index 9864b4cc..06c2ce79 100644 --- a/apps/backend/src/foodRequests/request.entity.ts +++ b/apps/backend/src/foodRequests/request.entity.ts @@ -47,5 +47,5 @@ export class FoodRequest { photos: string[]; @OneToMany(() => Order, (order) => order.request, { nullable: true }) - orders: Order[]; + order: Order; } diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 89d43414..cc69a5a3 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -8,14 +8,9 @@ import { Pantry } from '../pantries/pantries.entity'; import { RequestSize } from './types'; import { Order } from '../orders/order.entity'; import { OrderStatus } from '../orders/types'; -import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; -import { FoodType } from '../donationItems/types'; -import { DonationItem } from '../donationItems/donationItems.entity'; -import { Allocation } from '../allocations/allocations.entity'; const mockRequestsRepository = mock>(); const mockPantryRepository = mock>(); -const mockOrdersRepository = mock>(); const mockRequest: Partial = { requestId: 1, @@ -26,7 +21,7 @@ const mockRequest: Partial = { dateReceived: null, feedback: null, photos: null, - orders: null, + order: null, }; describe('RequestsService', () => { @@ -51,10 +46,6 @@ describe('RequestsService', () => { provide: getRepositoryToken(Pantry), useValue: mockPantryRepository, }, - { - provide: getRepositoryToken(Order), - useValue: mockOrdersRepository, - }, ], }).compile(); @@ -83,7 +74,7 @@ describe('RequestsService', () => { expect(result).toEqual(mockRequest); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['orders'], + relations: ['order'], }); }); @@ -98,134 +89,7 @@ describe('RequestsService', () => { expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['orders'], - }); - }); - }); - - describe('getOrderDetails', () => { - it('should return mapped order details for a valid requestId', async () => { - const requestId = 1; - - const mockOrders: Partial[] = [ - { - orderId: 10, - status: OrderStatus.DELIVERED, - foodManufacturer: { - foodManufacturerName: 'Test Manufacturer', - } as FoodManufacturer, - allocations: [ - { - allocatedQuantity: 5, - item: { - itemName: 'Rice', - foodType: FoodType.GRANOLA, - } as DonationItem, - } as Allocation, - { - allocatedQuantity: 3, - item: { - itemName: 'Beans', - foodType: FoodType.DRIED_BEANS, - } as DonationItem, - } as Allocation, - ], - }, - { - orderId: 11, - status: OrderStatus.SHIPPED, - foodManufacturer: { - foodManufacturerName: 'Another Manufacturer', - } as FoodManufacturer, - allocations: [ - { - allocatedQuantity: 2, - item: { - itemName: 'Milk', - foodType: FoodType.DAIRY_FREE_ALTERNATIVES, - } as DonationItem, - } as Allocation, - ], - }, - ]; - - mockOrdersRepository.find.mockResolvedValueOnce(mockOrders as Order[]); - - mockRequestsRepository.findOne.mockResolvedValueOnce( - mockRequest as FoodRequest, - ); - - const result = await service.getOrderDetails(requestId); - - expect(result).toEqual([ - { - orderId: 10, - status: OrderStatus.DELIVERED, - foodManufacturerName: 'Test Manufacturer', - items: [ - { - name: 'Rice', - quantity: 5, - foodType: FoodType.GRANOLA, - }, - { - name: 'Beans', - quantity: 3, - foodType: FoodType.DRIED_BEANS, - }, - ], - }, - { - orderId: 11, - status: OrderStatus.SHIPPED, - foodManufacturerName: 'Another Manufacturer', - items: [ - { - name: 'Milk', - quantity: 2, - foodType: FoodType.DAIRY_FREE_ALTERNATIVES, - }, - ], - }, - ]); - - expect(mockOrdersRepository.find).toHaveBeenCalledWith({ - where: { requestId }, - relations: { - foodManufacturer: true, - allocations: { - item: true, - }, - }, - }); - }); - - it('should throw an error if the request id is not found', async () => { - const requestId = 999; - - await expect(service.getOrderDetails(requestId)).rejects.toThrow( - `Request ${requestId} not found`, - ); - }); - - it('should return empty list if no associated orders', async () => { - const requestId = 1; - - mockRequestsRepository.findOne.mockResolvedValueOnce( - mockRequest as FoodRequest, - ); - mockOrdersRepository.find.mockResolvedValueOnce([]); - - const result = await service.getOrderDetails(requestId); - expect(result).toEqual([]); - expect(mockOrdersRepository.find).toHaveBeenCalledWith({ - where: { requestId }, - relations: { - foodManufacturer: true, - allocations: { - item: true, - }, - }, + relations: ['order'], }); }); }); @@ -302,7 +166,7 @@ describe('RequestsService', () => { dateReceived: null, feedback: null, photos: null, - orders: null, + order: null, }, { requestId: 3, @@ -314,7 +178,7 @@ describe('RequestsService', () => { dateReceived: null, feedback: null, photos: null, - orders: null, + order: null, }, ]; const pantryId = 1; @@ -327,7 +191,7 @@ describe('RequestsService', () => { expect(result).toEqual(mockRequests.slice(0, 2)); expect(mockRequestsRepository.find).toHaveBeenCalledWith({ where: { pantryId }, - relations: ['orders'], + relations: ['order'], }); }); }); @@ -349,7 +213,7 @@ describe('RequestsService', () => { const mockRequest2: Partial = { ...mockRequest, - orders: [mockOrder] as Order[], + order: mockOrder as Order, }; const requestId = 1; @@ -360,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, - orders: [updatedOrder], + order: { + ...(mockOrder as Order), + status: OrderStatus.DELIVERED, + } as Order, } as FoodRequest); const result = await service.updateDeliveryDetails( @@ -383,12 +247,12 @@ describe('RequestsService', () => { dateReceived: deliveryDate, feedback, photos, - orders: [updatedOrder], + order: { ...mockOrder, status: 'delivered' }, }); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['orders'], + relations: ['order'], }); expect(mockRequestsRepository.save).toHaveBeenCalledWith({ @@ -396,7 +260,7 @@ describe('RequestsService', () => { dateReceived: deliveryDate, feedback, photos, - orders: [mockOrder], + order: { ...mockOrder, status: 'delivered' }, }); }); @@ -419,7 +283,7 @@ describe('RequestsService', () => { expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['orders'], + relations: ['order'], }); }); @@ -440,11 +304,11 @@ describe('RequestsService', () => { feedback, photos, ), - ).rejects.toThrow('No associated orders found for this request'); + ).rejects.toThrow('No associated order found for this request'); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['orders'], + relations: ['order'], }); }); @@ -463,7 +327,7 @@ describe('RequestsService', () => { }; const mockRequest2: Partial = { ...mockRequest, - orders: [mockOrder] as Order[], + order: mockOrder as Order, }; const requestId = 1; @@ -482,13 +346,11 @@ describe('RequestsService', () => { feedback, photos, ), - ).rejects.toThrow( - 'No associated food manufacturer found for an associated order', - ); + ).rejects.toThrow('No associated food manufacturer found for this order'); expect(mockRequestsRepository.findOne).toHaveBeenCalledWith({ where: { requestId }, - relations: ['orders'], + relations: ['order'], }); }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 80093c58..32e600ba 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -1,19 +1,21 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { FoodRequest } from './request.entity'; 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 { @@ -21,7 +23,7 @@ export class RequestsService { const request = await this.repo.findOne({ where: { requestId }, - relations: ['orders'], + relations: ['order'], }); if (!request) { @@ -30,49 +32,6 @@ export class RequestsService { return request; } - async getOrderDetails(requestId: number): Promise { - validateId(requestId, 'Request'); - - const requestExists = await this.repo.findOne({ - where: { requestId }, - }); - - if (!requestExists) { - throw new NotFoundException(`Request ${requestId} not found`); - } - - const orders = await this.orderRepo.find({ - where: { requestId }, - relations: { - foodManufacturer: true, - allocations: { - item: true, - }, - }, - }); - - if (!orders) { - throw new NotFoundException( - 'No associated orders found for this request', - ); - } - - if (!orders.length) { - return []; - } - - 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, @@ -107,7 +66,7 @@ export class RequestsService { return await this.repo.find({ where: { pantryId }, - relations: ['orders'], + relations: ['order'], }); } @@ -121,32 +80,29 @@ export class RequestsService { const request = await this.repo.findOne({ where: { requestId }, - relations: ['orders'], + relations: ['order'], }); if (!request) { throw new NotFoundException('Invalid request ID'); } - if (!request.orders || request.orders.length == 0) { - throw new NotFoundException( - 'No associated orders found for this request', - ); + if (!request.order) { + throw new ConflictException('No associated order found for this request'); } - const orders = request.orders; + const order = request.order; - for (const order of orders) { - if (!order.shippedBy) { - throw new NotFoundException( - 'No associated food manufacturer found for an associated order', - ); - } + if (!order.shippedBy) { + throw new ConflictException( + 'No associated food manufacturer found for this order', + ); } request.feedback = feedback; request.dateReceived = deliveryDate; request.photos = photos; + request.order.status = OrderStatus.DELIVERED; return await this.repo.save(request); } diff --git a/apps/backend/src/orders/order.entity.ts b/apps/backend/src/orders/order.entity.ts index 7c40fdb4..4c38457b 100644 --- a/apps/backend/src/orders/order.entity.ts +++ b/apps/backend/src/orders/order.entity.ts @@ -5,13 +5,11 @@ 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 { @@ -74,7 +72,4 @@ 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 4066cb27..e20724bb 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -12,7 +12,6 @@ import { PantryApplicationDto, OrderSummary, UserDto, - OrderDetails, } from 'types/types'; const defaultBaseUrl = @@ -192,20 +191,12 @@ export class ApiClient { } public async getOrder(orderId: number): Promise { - return this.axiosInstance.get(`/api/orders/${orderId}`) as Promise; - } - - public async getOrderDetailsListFromRequest( - requestId: number, - ): Promise { - return this.axiosInstance - .get(`/api/requests/all-order-details/${requestId}`) - .then((response) => response.data) as Promise; + return this.axiosInstance.get(`api/orders/${orderId}`) as Promise; } 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/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx deleted file mode 100644 index 88153e95..00000000 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ /dev/null @@ -1,356 +0,0 @@ -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< - string, - OrderItemDetails[] - >, - ).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; diff --git a/apps/frontend/src/components/forms/requestFormModal.tsx b/apps/frontend/src/components/forms/requestFormModal.tsx index f54e625e..348cb247 100644 --- a/apps/frontend/src/components/forms/requestFormModal.tsx +++ b/apps/frontend/src/components/forms/requestFormModal.tsx @@ -14,9 +14,11 @@ import { import { Form, ActionFunction, ActionFunctionArgs } from 'react-router-dom'; import { FoodRequest, FoodTypes, RequestSize } from '../../types/types'; import { ChevronDownIcon } from 'lucide-react'; +import apiClient from '@api/apiClient'; interface FoodRequestFormModalProps { previousRequest?: FoodRequest; + readOnly?: boolean; isOpen: boolean; onClose: () => void; pantryId: number; @@ -24,6 +26,7 @@ interface FoodRequestFormModalProps { const FoodRequestFormModal: React.FC = ({ previousRequest, + readOnly = false, isOpen, onClose, pantryId, @@ -31,6 +34,7 @@ const FoodRequestFormModal: React.FC = ({ const [selectedItems, setSelectedItems] = useState([]); const [requestedSize, setRequestedSize] = useState(''); const [additionalNotes, setAdditionalNotes] = useState(''); + const [pantryName, setPantryName] = useState(''); const isFormValid = requestedSize !== '' && selectedItems.length > 0; @@ -45,6 +49,18 @@ const FoodRequestFormModal: React.FC = ({ } }, [isOpen, previousRequest]); + useEffect(() => { + const fetchData = async () => { + try { + const pantry = await apiClient.getPantry(pantryId); + setPantryName(pantry.pantryName); + } catch (error) { + console.error('Error fetching pantry data', error); + } + }; + fetchData(); + }); + return ( = ({ - {previousRequest ? 'Resubmit Latest Request' : 'New Food Request'} + {readOnly + ? `Order ${previousRequest?.requestId}` + : previousRequest + ? 'Resubmit Latest Order' + : 'New Food Request'} + {readOnly && ( + + {pantryName} + + )} = ({ pt={0} mt={0} > - {previousRequest - ? 'Confirm request details.' + {readOnly && previousRequest + ? `Requested ${new Date( + previousRequest.requestedAt, + ).toLocaleDateString()}` + : previousRequest + ? 'Confirm order details.' : `Please keep in mind that we may not be able to accommodate specific food requests at all times, but we will do our best to match your preferences.`} @@ -101,6 +130,7 @@ const FoodRequestFormModal: React.FC = ({ @@ -156,69 +186,73 @@ const FoodRequestFormModal: React.FC = ({ /> ))} - - - - + {!readOnly && ( + + + + - - - {FoodTypes.map((allergen) => { - const isChecked = selectedItems.includes(allergen); - return ( - { - setSelectedItems((prev) => - checked - ? [...prev, allergen] - : prev.filter((i) => i !== allergen), - ); - }} - display="flex" - alignItems="center" - > - - - + + {FoodTypes.map((allergen) => { + const isChecked = selectedItems.includes(allergen); + return ( + { + setSelectedItems((prev) => + checked + ? [...prev, allergen] + : prev.filter((i) => i !== allergen), + ); + }} + disabled={readOnly} + display="flex" + alignItems="center" > - {allergen} - - - ); - })} - - - + + + + {allergen} + + + ); + })} + + + + )} {selectedItems.length > 0 && ( @@ -227,26 +261,27 @@ const FoodRequestFormModal: React.FC = ({ key={item} size="xl" variant="solid" - bg={'neutral.100'} + bg={!readOnly ? '#E9F4F6' : 'neutral.100'} color="neutral.800" borderRadius="4px" - borderColor={'neutral.300'} + borderColor={!readOnly ? 'teal.400' : 'neutral.300'} borderWidth="1px" fontFamily="Inter" fontWeight={500} > {item} - - - - setSelectedItems((prev) => - prev.filter((i) => i !== item), - ) - } - /> - + {!readOnly && ( + + + setSelectedItems((prev) => + prev.filter((i) => i !== item), + ) + } + /> + + )} ))} @@ -282,36 +317,41 @@ const FoodRequestFormModal: React.FC = ({ alert('Exceeded word limit'); } }} + disabled={readOnly} /> - - - Max 250 words - + {!readOnly && ( + + Max 250 words + + )} - - - + {!readOnly && ( + + )} + {!readOnly && ( + + )} - + {readOnly && } diff --git a/apps/frontend/src/containers/FormRequests.tsx b/apps/frontend/src/containers/FormRequests.tsx index 17d2fea5..50d20601 100644 --- a/apps/frontend/src/containers/FormRequests.tsx +++ b/apps/frontend/src/containers/FormRequests.tsx @@ -1,28 +1,22 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { - Box, + Center, Table, Text, Button, HStack, useDisclosure, - Link, - Badge, - Pagination, - ButtonGroup, - IconButton, - Flex, + NativeSelect, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft } from 'lucide-react'; import FoodRequestFormModal from '@components/forms/requestFormModal'; -import { OrderStatus, FoodRequest } from '../types/types'; -import RequestDetailsModal from '@components/forms/requestDetailsModal'; -import { formatDate } from '@utils/utils'; +import DeliveryConfirmationModal from '@components/forms/deliveryConfirmationModal'; +import OrderInformationModal from '@components/forms/orderInformationModal'; +import { FoodRequest } from 'types/types'; +import { formatDate, formatReceivedDate } from '@utils/utils'; import ApiClient from '@api/apiClient'; const FormRequests: React.FC = () => { - const [currentPage, setCurrentPage] = useState(1); const newRequestDisclosure = useDisclosure(); const previousRequestDisclosure = useDisclosure(); @@ -30,27 +24,34 @@ const FormRequests: React.FC = () => { const [previousRequest, setPreviousRequest] = useState< FoodRequest | undefined >(undefined); + const [sortBy, setSortBy] = useState<'mostRecent' | 'oldest' | 'confirmed'>( + 'mostRecent', + ); const { pantryId: pantryIdParam } = useParams<{ pantryId: string }>(); const pantryId = parseInt(pantryIdParam!, 10); + const [allConfirmed, setAllConfirmed] = useState(false); + const [openDeliveryRequestId, setOpenDeliveryRequestId] = useState< + number | null + >(null); const [openReadOnlyRequest, setOpenReadOnlyRequest] = useState(null); - - const pageSize = 8; + const [openOrderId, setOpenOrderId] = useState(null); useEffect(() => { const fetchRequests = async () => { if (pantryId) { try { const data = await ApiClient.getPantryRequests(pantryId); - const sortedData = data - .slice() - .sort((a, b) => b.requestId - a.requestId); - setRequests(sortedData); + setRequests(data); - if (sortedData.length > 0) { - setPreviousRequest(sortedData[0]); + if (data.length > 0) { + setPreviousRequest( + data.reduce((prev, current) => + prev.requestId > current.requestId ? prev : current, + ), + ); } } catch (error) { alert('Error fetching requests: ' + error); @@ -61,24 +62,33 @@ const FormRequests: React.FC = () => { fetchRequests(); }, [pantryId]); - const paginatedRequests = requests.slice( - (currentPage - 1) * pageSize, - currentPage * pageSize, - ); + useEffect(() => { + setAllConfirmed(requests.every((request) => request.dateReceived !== null)); + }, [requests]); + + const sortedRequests = [...requests].sort((a, b) => { + if (sortBy === 'mostRecent') + return ( + new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime() + ); + if (sortBy === 'oldest') + return ( + new Date(a.requestedAt).getTime() - new Date(b.requestedAt).getTime() + ); + if (sortBy === 'confirmed') + return ( + new Date(b.dateReceived || 0).getTime() - + new Date(a.dateReceived || 0).getTime() + ); + + return 0; + }); return ( - - - Food Request Management - - - { <> { )} - + + + + setSortBy(e.target.value as 'mostRecent' | 'oldest' | 'confirmed') + } + > + + + + + + + + - - Request # - - - Status - - - Date Requested - + Request ID + Order ID + Date Requested + Status + Shipped By + Date Fulfilled + Actions - {paginatedRequests.map((request) => ( + {sortedRequests.map((request) => ( - - setOpenReadOnlyRequest(request)} - > + + - {request.orders?.every( - (order) => order.status === OrderStatus.DELIVERED, - ) ? ( - + setOpenOrderId(request.order?.orderId ?? null) + } > - Closed - + {request.order?.orderId} + ) : ( - - Active - + 'N/A' )} - - {formatDate(request.requestedAt)} + {formatDate(request.requestedAt)} + {request.order?.status ?? 'pending'} + + {request.order?.status === 'pending' + ? 'N/A' + : request.order?.shippedBy ?? 'N/A'} + + + {formatReceivedDate(request.dateReceived)} + + + {!request.order || request.order?.status === 'pending' ? ( + Awaiting Order Assignment + ) : request.order?.status === 'delivered' ? ( + Food Request is Already Delivered + ) : ( + + )} ))} {openReadOnlyRequest && ( - setOpenReadOnlyRequest(null)} pantryId={pantryId} /> )} + {openOrderId && ( + setOpenOrderId(null)} + /> + )} + {openDeliveryRequestId && ( + setOpenDeliveryRequestId(null)} + pantryId={pantryId} + /> + )} - - 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)), - ) - } - > - - - - - - - + ); }; diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 1139e62a..3f9bf3a8 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -135,23 +135,6 @@ 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; @@ -178,7 +161,7 @@ export interface FoodRequest { requestedItems: string[]; additionalInformation: string; orderId: number; - orders?: Order[]; + order?: Order; } export interface Order { @@ -194,19 +177,6 @@ export interface Order { deliveredAt: string | null; } -export interface OrderItemDetails { - name: string; - quantity: number; - foodType: FoodType; -} - -export interface OrderDetails { - orderId: number; - status: OrderStatus; - foodManufacturerName: string; - items: OrderItemDetails[]; -} - export interface FoodManufacturer { foodManufacturerId: number; foodManufacturerName: string;