diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 4066cb27..3da59bfe 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -203,6 +203,14 @@ export class ApiClient { .then((response) => response.data) as Promise; } + public async getOrderDetailsListFromRequest( + requestId: number, + ): Promise { + return this.axiosInstance + .get(`api/requests/all-order-details/${requestId}`) + .then((response) => response.data) as Promise; + } + async getAllAllocationsByOrder(orderId: number): Promise { return this.axiosInstance .get(`/api/orders/${orderId}/allocations`) diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx new file mode 100644 index 00000000..88153e95 --- /dev/null +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -0,0 +1,356 @@ +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/containers/FormRequests.tsx b/apps/frontend/src/containers/FormRequests.tsx index 65d000cd..17d2fea5 100644 --- a/apps/frontend/src/containers/FormRequests.tsx +++ b/apps/frontend/src/containers/FormRequests.tsx @@ -1,22 +1,28 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { - Center, + Box, Table, Text, Button, HStack, useDisclosure, - NativeSelect, + Link, + Badge, + Pagination, + ButtonGroup, + IconButton, + Flex, } 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 { FoodRequest } from 'types/types'; -import { formatDate, formatReceivedDate } from '@utils/utils'; +import { OrderStatus, FoodRequest } from '../types/types'; +import RequestDetailsModal from '@components/forms/requestDetailsModal'; +import { formatDate } from '@utils/utils'; import ApiClient from '@api/apiClient'; const FormRequests: React.FC = () => { + const [currentPage, setCurrentPage] = useState(1); const newRequestDisclosure = useDisclosure(); const previousRequestDisclosure = useDisclosure(); @@ -24,34 +30,27 @@ 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 [openOrderId, setOpenOrderId] = useState(null); + + const pageSize = 8; useEffect(() => { const fetchRequests = async () => { if (pantryId) { try { const data = await ApiClient.getPantryRequests(pantryId); - setRequests(data); + const sortedData = data + .slice() + .sort((a, b) => b.requestId - a.requestId); + setRequests(sortedData); - if (data.length > 0) { - setPreviousRequest( - data.reduce((prev, current) => - prev.requestId > current.requestId ? prev : current, - ), - ); + if (sortedData.length > 0) { + setPreviousRequest(sortedData[0]); } } catch (error) { alert('Error fetching requests: ' + error); @@ -62,33 +61,24 @@ const FormRequests: React.FC = () => { fetchRequests(); }, [pantryId]); - 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; - }); + const paginatedRequests = requests.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize, + ); return ( -
- - { <> { )} - - - - setSortBy(e.target.value as 'mostRecent' | 'oldest' | 'confirmed') - } - > - - - - - - - - + - 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 ? ( - - ) : ( - '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 + Closed + ) : ( - + Active + )} + + {formatDate(request.requestedAt)} + ))} {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..89cc70b6 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -207,6 +207,19 @@ export interface OrderDetails { items: OrderItemDetails[]; } +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;