From f82c01c6b59877eb6ac36fb0562eae103519ada7 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Fri, 5 Jun 2026 00:00:56 -0700 Subject: [PATCH 1/2] Made close order endpoint --- .../allocations/allocations.service.spec.ts | 96 ++++++++++ .../src/allocations/allocations.service.ts | 33 ++++ apps/backend/src/config/migrations.ts | 2 + .../1780562894014-AddOrderStatusClosed.ts | 64 +++++++ apps/backend/src/orders/order.controller.ts | 37 +++- apps/backend/src/orders/order.service.spec.ts | 175 +++++++++++++++++- apps/backend/src/orders/order.service.ts | 34 ++++ apps/backend/src/orders/types.ts | 1 + .../frontend/src/components/dashboardCard.tsx | 28 +-- .../components/forms/requestDetailsModal.tsx | 4 +- .../src/containers/adminOrderManagement.tsx | 58 +++--- .../src/containers/pantryOrderManagement.tsx | 62 +++---- .../containers/volunteerOrderManagement.tsx | 62 +++---- apps/frontend/src/types/types.ts | 10 +- apps/frontend/src/utils/utils.ts | 18 +- 15 files changed, 561 insertions(+), 123 deletions(-) create mode 100644 apps/backend/src/migrations/1780562894014-AddOrderStatusClosed.ts diff --git a/apps/backend/src/allocations/allocations.service.spec.ts b/apps/backend/src/allocations/allocations.service.spec.ts index 395c88ecb..df0939aaf 100644 --- a/apps/backend/src/allocations/allocations.service.spec.ts +++ b/apps/backend/src/allocations/allocations.service.spec.ts @@ -222,4 +222,100 @@ describe('AllocationsService', () => { expect(Number(allocationCountAfter)).toBe(Number(allocationCountBefore)); }); }); + + describe('freeAllByOrder', () => { + // Order 2 is seeded with 3 allocations (see getAllAllocationsByOrder above). + const orderId = 2; + + it('should remove all allocations for an order and decrement each reservedQuantity by the allocated amount', async () => { + const allocationRepo = testDataSource.getRepository(Allocation); + const donationItemRepo = testDataSource.getRepository(DonationItem); + + const allocations = await allocationRepo.find({ where: { orderId } }); + expect(allocations.length).toBeGreaterThan(0); + + // Sum the allocated quantity per item and capture reserved-before + const allocatedByItem = new Map(); + const reservedBefore = new Map(); + for (const allocation of allocations) { + allocatedByItem.set( + allocation.itemId, + (allocatedByItem.get(allocation.itemId) ?? 0) + + allocation.allocatedQuantity, + ); + if (!reservedBefore.has(allocation.itemId)) { + const item = (await donationItemRepo.findOne({ + where: { itemId: allocation.itemId }, + })) as DonationItem; + reservedBefore.set(allocation.itemId, item.reservedQuantity); + } + } + + await service.freeAllByOrder(orderId); + + expect(await allocationRepo.find({ where: { orderId } })).toHaveLength(0); + + for (const [itemId, allocated] of allocatedByItem) { + const item = (await donationItemRepo.findOne({ + where: { itemId }, + })) as DonationItem; + expect(item.reservedQuantity).toBe( + (reservedBefore.get(itemId) as number) - allocated, + ); + } + }); + + it('should work with a given transaction manager', async () => { + const allocationRepo = testDataSource.getRepository(Allocation); + + expect( + (await allocationRepo.find({ where: { orderId } })).length, + ).toBeGreaterThan(0); + + await testDataSource.transaction(async (manager) => { + await service.freeAllByOrder(orderId, manager); + }); + + expect(await allocationRepo.find({ where: { orderId } })).toHaveLength(0); + }); + + it('should rollback all changes if an error occurs during the transaction', async () => { + const allocationRepo = testDataSource.getRepository(Allocation); + const donationItemRepo = testDataSource.getRepository(DonationItem); + + const allocationsBefore = await allocationRepo.find({ + where: { orderId }, + }); + const allocationCountBefore = await allocationRepo.count(); + const itemIds = [...new Set(allocationsBefore.map((a) => a.itemId))]; + const reservedBefore = new Map(); + for (const itemId of itemIds) { + const item = (await donationItemRepo.findOne({ + where: { itemId }, + })) as DonationItem; + reservedBefore.set(itemId, item.reservedQuantity); + } + + await expect( + testDataSource.transaction(async (manager) => { + await service.freeAllByOrder(orderId, manager); + throw new Error('Simulated failure'); + }), + ).rejects.toThrow('Simulated failure'); + + // Nothing was removed and no reservedQuantity changed. + expect(await allocationRepo.count()).toBe(allocationCountBefore); + expect(await allocationRepo.find({ where: { orderId } })).toHaveLength( + allocationsBefore.length, + ); + for (const itemId of itemIds) { + const item = (await donationItemRepo.findOne({ + where: { itemId }, + })) as DonationItem; + expect(item.reservedQuantity).toBe( + reservedBefore.get(itemId) as number, + ); + } + }); + }); }); diff --git a/apps/backend/src/allocations/allocations.service.ts b/apps/backend/src/allocations/allocations.service.ts index bd951892c..1e4bc5401 100644 --- a/apps/backend/src/allocations/allocations.service.ts +++ b/apps/backend/src/allocations/allocations.service.ts @@ -65,4 +65,37 @@ export class AllocationsService { return targetAllocationRepo.save(allocations); } + + async freeAllByOrder( + orderId: number, + transactionManager?: EntityManager, + ): Promise { + const allocationTransactionRepo = transactionManager + ? transactionManager.getRepository(Allocation) + : undefined; + const itemTransactionRepo = transactionManager + ? transactionManager.getRepository(DonationItem) + : undefined; + const targetAllocationRepo = allocationTransactionRepo + ? allocationTransactionRepo + : this.repo; + const targetItemRepo = itemTransactionRepo + ? itemTransactionRepo + : this.donationItemRepo; + + validateId(orderId, 'Order'); + + // All orders have allocations so this will have something. + const allocations = await targetAllocationRepo.find({ where: { orderId } }); + + for (const allocation of allocations) { + await targetItemRepo.decrement( + { itemId: allocation.itemId }, + 'reservedQuantity', + allocation.allocatedQuantity, + ); + } + + await targetAllocationRepo.remove(allocations); + } } diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 65a88e352..a7fe305a7 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -41,6 +41,7 @@ import { MakeFoodRescueRequired1773889925002 } from '../migrations/1773889925002 import { AddDonationItemConfirmation1774140453305 } from '../migrations/1774140453305-AddDonationItemConfirmation'; import { DonationItemsOnDeleteCascade1774214910101 } from '../migrations/1774214910101-DonationItemsOnDeleteCascade'; import { OrdersVolunteerActions1774883880543 } from '../migrations/1774883880543-OrdersVolunteerActions'; +import { AddOrderStatusClosed1780562894014 } from '../migrations/1780562894014-AddOrderStatusClosed'; const schemaMigrations = [ User1725726359198, @@ -86,6 +87,7 @@ const schemaMigrations = [ AddDonationItemConfirmation1774140453305, DonationItemsOnDeleteCascade1774214910101, OrdersVolunteerActions1774883880543, + AddOrderStatusClosed1780562894014, ]; export default schemaMigrations; diff --git a/apps/backend/src/migrations/1780562894014-AddOrderStatusClosed.ts b/apps/backend/src/migrations/1780562894014-AddOrderStatusClosed.ts new file mode 100644 index 000000000..2621779b5 --- /dev/null +++ b/apps/backend/src/migrations/1780562894014-AddOrderStatusClosed.ts @@ -0,0 +1,64 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOrderStatusClosed1780562894014 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + ALTER COLUMN status DROP DEFAULT; + + CREATE TYPE orders_status_enum_new AS ENUM ( + 'delivered', + 'pending', + 'shipped', + 'closed' + ); + + ALTER TABLE orders + ALTER COLUMN status + TYPE orders_status_enum_new + USING status::text::orders_status_enum_new; + + DROP TYPE orders_status_enum; + + ALTER TYPE orders_status_enum_new + RENAME TO orders_status_enum; + + ALTER TABLE orders + ALTER COLUMN status + SET DEFAULT 'pending'; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE orders + ALTER COLUMN status DROP DEFAULT; + + CREATE TYPE orders_status_enum_old AS ENUM ( + 'delivered', + 'pending', + 'shipped' + ); + + ALTER TABLE orders + ALTER COLUMN status + TYPE orders_status_enum_old + USING ( + CASE + WHEN status = 'closed' + THEN 'pending' + ELSE status::text + END + )::orders_status_enum_old; + + DROP TYPE orders_status_enum; + + ALTER TYPE orders_status_enum_old + RENAME TO orders_status_enum; + + ALTER TABLE orders + ALTER COLUMN status + SET DEFAULT 'pending'; + `); + } +} diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index f53f25a5d..c576db063 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -19,7 +19,11 @@ import { OrdersService } from './order.service'; import { Order } from './order.entity'; import { Pantry } from '../pantries/pantries.entity'; import { AllocationsService } from '../allocations/allocations.service'; -import { CheckOwnership, pipeNullable } from '../auth/ownership.decorator'; +import { + CheckOwnership, + OwnerIdResolver, + pipeNullable, +} from '../auth/ownership.decorator'; import { PantriesService } from '../pantries/pantries.service'; import { BulkUpdateTrackingCostDto } from './dtos/bulk-update-tracking-cost.dto'; import { OrderDetailsDto } from './dtos/order-details.dto'; @@ -35,6 +39,25 @@ import { Roles } from '../auth/roles.decorator'; import { Role } from '../users/types'; import { OrderStatus } from './types'; +const resolveOrderAuthorizedUserIds: OwnerIdResolver = ({ + entityId, + services, + user, +}) => { + if (user?.role === Role.VOLUNTEER) { + return pipeNullable( + () => services.get(OrdersService).findOne(entityId), + (order: Order) => [order.assigneeId], + ); + } + return pipeNullable( + () => services.get(OrdersService).findOrderFoodRequest(entityId), + (request: FoodRequestSummaryDto) => + services.get(PantriesService).findOne(request.pantry.pantryId), + (pantry: Pantry) => [pantry.pantryUser.id], + ); +}; + @Controller('orders') export class OrdersController { constructor( @@ -271,4 +294,16 @@ export class OrdersController { ): Promise { await this.ordersService.completeVolunteerAction(orderId, dto.action); } + + @CheckOwnership({ + idParam: 'orderId', + resolver: resolveOrderAuthorizedUserIds, + }) + @Roles(Role.VOLUNTEER) + @Patch('/:orderId/close') + async closeOrder( + @Param('orderId', ParseIntPipe) orderId: number, + ): Promise { + await this.ordersService.closeOrder(orderId); + } } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 2aee388f1..4c97575cf 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -30,7 +30,13 @@ import { AuthService } from '../auth/auth.service'; import { DonationService } from '../donations/donations.service'; import { PantriesService } from '../pantries/pantries.service'; import { CreateOrderDto } from './dtos/create-order.dto'; -import { DataSource, EntityManager, In } from 'typeorm'; +import { + DataSource, + EntityManager, + In, + ObjectLiteral, + Repository, +} from 'typeorm'; import { EmailsService } from '../emails/email.service'; import { Allocation } from '../allocations/allocations.entity'; import { mock } from 'jest-mock-extended'; @@ -1169,6 +1175,173 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(donationItem1?.reservedQuantity).toBe(10); }); }); + + describe('closeOrder', () => { + const userId = 3; + let validCreateOrderDto: CreateOrderDto; + let parsedAllocations: Map; + + beforeEach(() => { + validCreateOrderDto = { + foodRequestId: 1, + manufacturerId: 1, + itemAllocations: { + 1: 10, + 2: 3, + }, + }; + + parsedAllocations = new Map([ + [1, 10], + [2, 3], + ]); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + // Creates a pending order (reserving items 1 and 2) and returns the + // post-create state so rollback tests can assert nothing changed. + const createPendingOrder = async () => { + const allocationRepo = testDataSource.getRepository(Allocation); + const donationItemRepo = testDataSource.getRepository(DonationItem); + + const createdOrder = await service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ); + + const allocationsBefore = await allocationRepo.find({ + where: { orderId: createdOrder.orderId }, + }); + const item1Before = (await donationItemRepo.findOne({ + where: { itemId: 1 }, + })) as DonationItem; + const item2Before = (await donationItemRepo.findOne({ + where: { itemId: 2 }, + })) as DonationItem; + + return { + orderId: createdOrder.orderId, + allocationRepo, + donationItemRepo, + allocationsBefore, + item1Before, + item2Before, + }; + }; + + it('sets the order status to CLOSED when everything succeeds', async () => { + const { orderId } = await createPendingOrder(); + + await service.closeOrder(orderId); + + expect((await service.findOne(orderId)).status).toBe(OrderStatus.CLOSED); + }); + + it('throws NotFoundException if the order does not exist', async () => { + const nonExistentOrderId = 999; + await expect(service.closeOrder(nonExistentOrderId)).rejects.toThrow( + new NotFoundException(`Order ${nonExistentOrderId} not found`), + ); + }); + + it('throws BadRequestException if the order is not pending', async () => { + const orderRepo = testDataSource.getRepository(Order); + const { orderId } = await createPendingOrder(); + + await orderRepo.update({ orderId }, { status: OrderStatus.SHIPPED }); + + await expect(service.closeOrder(orderId)).rejects.toThrow( + new BadRequestException(`Order ${orderId} must be pending`), + ); + + expect((await service.findOne(orderId)).status).not.toBe( + OrderStatus.CLOSED, + ); + }); + + it('rolls back all changes when matchAll fails', async () => { + const { + orderId, + allocationRepo, + donationItemRepo, + allocationsBefore, + item1Before, + item2Before, + } = await createPendingOrder(); + + jest + .spyOn((service as any).donationService as DonationService, 'matchAll') + .mockRejectedValueOnce(new Error('DB error')); + + await expect(service.closeOrder(orderId)).rejects.toThrow('DB error'); + + const orderAfter = await service.findOne(orderId); + expect(orderAfter.status).not.toBe(OrderStatus.CLOSED); + expect(orderAfter.status).toBe(OrderStatus.PENDING); + expect(await allocationRepo.find({ where: { orderId } })).toHaveLength( + allocationsBefore.length, + ); + + const item1After = (await donationItemRepo.findOne({ + where: { itemId: 1 }, + })) as DonationItem; + const item2After = (await donationItemRepo.findOne({ + where: { itemId: 2 }, + })) as DonationItem; + expect(item1After.reservedQuantity).toBe(item1Before.reservedQuantity); + expect(item2After.reservedQuantity).toBe(item2Before.reservedQuantity); + }); + + it('rolls back all changes when the order status update fails', async () => { + const { + orderId, + allocationRepo, + donationItemRepo, + allocationsBefore, + item1Before, + item2Before, + } = await createPendingOrder(); + + const originalUpdate = Repository.prototype.update; + jest + .spyOn(Repository.prototype, 'update') + .mockImplementation(function ( + this: Repository, + ...args: Parameters + ) { + if (this.metadata.target === Order) { + return Promise.reject(new Error('DB error')); + } + return originalUpdate.apply(this, args); + }); + + await expect(service.closeOrder(orderId)).rejects.toThrow('DB error'); + + jest.restoreAllMocks(); + + const orderAfter = await service.findOne(orderId); + expect(orderAfter.status).not.toBe(OrderStatus.CLOSED); + expect(orderAfter.status).toBe(OrderStatus.PENDING); + expect(await allocationRepo.find({ where: { orderId } })).toHaveLength( + allocationsBefore.length, + ); + + const item1After = (await donationItemRepo.findOne({ + where: { itemId: 1 }, + })) as DonationItem; + const item2After = (await donationItemRepo.findOne({ + where: { itemId: 2 }, + })) as DonationItem; + expect(item1After.reservedQuantity).toBe(item1Before.reservedQuantity); + expect(item2After.reservedQuantity).toBe(item2Before.reservedQuantity); + }); + }); + describe('getAllOrdersForVolunteer', () => { it('should return all orders across all pantries and assignees, with required actions for assigned orders', async () => { const volunteerId = 6; diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 1018c940c..fdd5b052f 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -23,6 +23,7 @@ import { FoodRequestStatus } from '../foodRequests/types'; import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; import { DonationItemsService } from '../donationItems/donationItems.service'; import { AllocationsService } from '../allocations/allocations.service'; +import { Allocation } from '../allocations/allocations.entity'; import { ApplicationStatus } from '../shared/types'; import { VolunteerOrder } from '../volunteers/types'; import { EmailsService } from '../emails/email.service'; @@ -749,4 +750,37 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ await this.repo.save(order); } + + async closeOrder(orderId: number): Promise { + validateId(orderId, 'Order'); + + const order = await this.repo.findOneBy({ orderId }); + + if (!order) { + throw new NotFoundException(`Order ${orderId} not found`); + } + + if (order.status !== OrderStatus.PENDING) { + throw new BadRequestException(`Order ${orderId} must be pending`); + } + + await this.dataSource.transaction(async (transactionManager) => { + // Capture which donations are affected before allocations are removed + const allocations = await transactionManager + .getRepository(Allocation) + .find({ where: { orderId }, relations: ['item'] }); + const donationIds = [ + ...new Set(allocations.map((allocation) => allocation.item.donationId)), + ]; + + await this.allocationsService.freeAllByOrder(orderId, transactionManager); + + // Donation should always have items matched to it + await this.donationService.matchAll(donationIds, transactionManager); + + await transactionManager + .getRepository(Order) + .update({ orderId }, { status: OrderStatus.CLOSED }); + }); + } } diff --git a/apps/backend/src/orders/types.ts b/apps/backend/src/orders/types.ts index 2966034d7..7eb899ca3 100644 --- a/apps/backend/src/orders/types.ts +++ b/apps/backend/src/orders/types.ts @@ -2,6 +2,7 @@ export enum OrderStatus { DELIVERED = 'delivered', PENDING = 'pending', SHIPPED = 'shipped', + CLOSED = 'closed', } export enum VolunteerAction { diff --git a/apps/frontend/src/components/dashboardCard.tsx b/apps/frontend/src/components/dashboardCard.tsx index ad8208d6d..3c09215ac 100644 --- a/apps/frontend/src/components/dashboardCard.tsx +++ b/apps/frontend/src/components/dashboardCard.tsx @@ -9,7 +9,7 @@ import { DONATION_STATUS_COLORS, USER_ICON_COLORS, } from '@utils/utils'; -import { OrderAssignee, OrderStatus, DonationStatus } from '../types/types'; +import { OrderAssignee, OpenOrderStatus, DonationStatus } from '../types/types'; export enum DashboardCardType { UPCOMING_DONATION, @@ -41,21 +41,21 @@ export interface DashboardCardBadge { color: string; } -export const ORDER_STATUS_BADGE: Record = { - [OrderStatus.PENDING]: { - label: ORDER_STATUS_LABELS[OrderStatus.PENDING], - bg: ORDER_STATUS_COLORS[OrderStatus.PENDING][0], - color: ORDER_STATUS_COLORS[OrderStatus.PENDING][1], +export const ORDER_STATUS_BADGE: Record = { + [OpenOrderStatus.PENDING]: { + label: ORDER_STATUS_LABELS[OpenOrderStatus.PENDING], + bg: ORDER_STATUS_COLORS[OpenOrderStatus.PENDING][0], + color: ORDER_STATUS_COLORS[OpenOrderStatus.PENDING][1], }, - [OrderStatus.SHIPPED]: { - label: ORDER_STATUS_LABELS[OrderStatus.SHIPPED], - bg: ORDER_STATUS_COLORS[OrderStatus.SHIPPED][0], - color: ORDER_STATUS_COLORS[OrderStatus.SHIPPED][1], + [OpenOrderStatus.SHIPPED]: { + label: ORDER_STATUS_LABELS[OpenOrderStatus.SHIPPED], + bg: ORDER_STATUS_COLORS[OpenOrderStatus.SHIPPED][0], + color: ORDER_STATUS_COLORS[OpenOrderStatus.SHIPPED][1], }, - [OrderStatus.DELIVERED]: { - label: ORDER_STATUS_LABELS[OrderStatus.DELIVERED], - bg: ORDER_STATUS_COLORS[OrderStatus.DELIVERED][0], - color: ORDER_STATUS_COLORS[OrderStatus.DELIVERED][1], + [OpenOrderStatus.DELIVERED]: { + label: ORDER_STATUS_LABELS[OpenOrderStatus.DELIVERED], + bg: ORDER_STATUS_COLORS[OpenOrderStatus.DELIVERED][0], + color: ORDER_STATUS_COLORS[OpenOrderStatus.DELIVERED][1], }, }; diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index 23c4e9e80..0e2f149bb 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -4,7 +4,7 @@ import { OrderDetails, FoodRequestSummaryDto, } from 'types/types'; -import { OrderStatus } from '../../types/types'; +import { OpenOrderStatus } from '../../types/types'; import { ORDER_STATUS_LABELS } from '@utils/utils'; import React, { useState, useEffect } from 'react'; import { @@ -194,7 +194,7 @@ const RequestDetailsModal: React.FC = ({ Fulfilled by {currentOrder.foodManufacturerName} - {currentOrder.status === OrderStatus.DELIVERED ? ( + {currentOrder.status === OpenOrderStatus.DELIVERED ? ( { // State to hold orders grouped by status const [statusOrders, setStatusOrders] = useState< - Record + Record >({ - [OrderStatus.SHIPPED]: [], - [OrderStatus.PENDING]: [], - [OrderStatus.DELIVERED]: [], + [OpenOrderStatus.SHIPPED]: [], + [OpenOrderStatus.PENDING]: [], + [OpenOrderStatus.DELIVERED]: [], }); // State to hold selected order for details modal const [selectedOrderId, setSelectedOrderId] = useState(null); // State to hold current page per status - const [currentPages, setCurrentPages] = useState>( - { - [OrderStatus.SHIPPED]: 1, - [OrderStatus.PENDING]: 1, - [OrderStatus.DELIVERED]: 1, - }, - ); + const [currentPages, setCurrentPages] = useState< + Record + >({ + [OpenOrderStatus.SHIPPED]: 1, + [OpenOrderStatus.PENDING]: 1, + [OpenOrderStatus.DELIVERED]: 1, + }); const [searchParams] = useSearchParams(); const [alertState, setAlertMessage] = useAlert(); @@ -78,19 +78,19 @@ const AdminOrderManagement: React.FC = () => { // sortAsc indicates whether the sorting is ascending (oldest first) or descending (newest first) // We store all these here to determine what orders to display for each status const [filterStates, setFilterStates] = useState< - Record + Record >({ - [OrderStatus.SHIPPED]: { + [OpenOrderStatus.SHIPPED]: { selectedPantries: [], searchPantry: '', sortAsc: false, }, - [OrderStatus.PENDING]: { + [OpenOrderStatus.PENDING]: { selectedPantries: [], searchPantry: '', sortAsc: false, }, - [OrderStatus.DELIVERED]: { + [OpenOrderStatus.DELIVERED]: { selectedPantries: [], searchPantry: '', sortAsc: false, @@ -105,10 +105,10 @@ const AdminOrderManagement: React.FC = () => { try { const data = await ApiClient.getAllOrders(); - const grouped: Record = { - [OrderStatus.SHIPPED]: [], - [OrderStatus.PENDING]: [], - [OrderStatus.DELIVERED]: [], + const grouped: Record = { + [OpenOrderStatus.SHIPPED]: [], + [OpenOrderStatus.PENDING]: [], + [OpenOrderStatus.DELIVERED]: [], }; for (const order of data) { @@ -126,10 +126,10 @@ const AdminOrderManagement: React.FC = () => { setStatusOrders(grouped); // Initialize current page for each status - const initialPages: Record = { - [OrderStatus.SHIPPED]: 1, - [OrderStatus.PENDING]: 1, - [OrderStatus.DELIVERED]: 1, + const initialPages: Record = { + [OpenOrderStatus.SHIPPED]: 1, + [OpenOrderStatus.PENDING]: 1, + [OpenOrderStatus.DELIVERED]: 1, }; setCurrentPages(initialPages); } catch { @@ -141,11 +141,11 @@ const AdminOrderManagement: React.FC = () => { }, [setAlertMessage]); // Helper to reset page for a specific status - const resetPageForStatus = (status: OrderStatus) => { + const resetPageForStatus = (status: OpenOrderStatus) => { setCurrentPages((prev) => ({ ...prev, [status]: 1 })); }; - const handlePageChange = (status: OrderStatus, page: number) => { + const handlePageChange = (status: OpenOrderStatus, page: number) => { setCurrentPages((prev) => ({ ...prev, [status]: page, @@ -165,7 +165,7 @@ const AdminOrderManagement: React.FC = () => { if (matchedOrder) { setSelectedOrderId(id); // Paginate the containing status to the page that holds this order. - for (const status of Object.values(OrderStatus)) { + for (const status of Object.values(OpenOrderStatus)) { const sorted = [...statusOrders[status]].sort((a, b) => b.createdAt.localeCompare(a.createdAt), ); @@ -198,7 +198,7 @@ const AdminOrderManagement: React.FC = () => { /> )} - {Object.values(OrderStatus).map((status) => { + {Object.values(OpenOrderStatus).map((status) => { const allOrders = statusOrders[status] || []; const filterState = filterStates[status]; @@ -277,7 +277,7 @@ const AdminOrderManagement: React.FC = () => { interface OrderStatusSectionProps { orders: OrderWithColor[]; - status: OrderStatus; + status: OpenOrderStatus; colors: string[]; onOrderSelect: (orderId: number | null) => void; totalOrders: number; diff --git a/apps/frontend/src/containers/pantryOrderManagement.tsx b/apps/frontend/src/containers/pantryOrderManagement.tsx index 5ee5339c2..057cb5f5b 100644 --- a/apps/frontend/src/containers/pantryOrderManagement.tsx +++ b/apps/frontend/src/containers/pantryOrderManagement.tsx @@ -24,7 +24,7 @@ import { USER_ICON_COLORS, } from '@utils/utils'; import ApiClient from '@api/apiClient'; -import { OrderStatus, OrderSummary } from '../types/types'; +import { OpenOrderStatus, OrderSummary } from '../types/types'; import OrderReceivedActionModal from '@components/forms/orderReceivedActionModal'; import OrderDetailsModal from '@components/forms/orderDetailsModal'; import { FloatingAlert } from '@components/floatingAlert'; @@ -38,11 +38,11 @@ const MAX_PER_STATUS = 5; const PantryOrderManagement: React.FC = () => { // State to hold orders grouped by status const [statusOrders, setStatusOrders] = useState< - Record + Record >({ - [OrderStatus.SHIPPED]: [], - [OrderStatus.PENDING]: [], - [OrderStatus.DELIVERED]: [], + [OpenOrderStatus.SHIPPED]: [], + [OpenOrderStatus.PENDING]: [], + [OpenOrderStatus.DELIVERED]: [], }); // State to hold selected order for details modal @@ -52,13 +52,13 @@ const PantryOrderManagement: React.FC = () => { useState(null); // State to hold current page per status - const [currentPages, setCurrentPages] = useState>( - { - [OrderStatus.SHIPPED]: 1, - [OrderStatus.PENDING]: 1, - [OrderStatus.DELIVERED]: 1, - }, - ); + const [currentPages, setCurrentPages] = useState< + Record + >({ + [OpenOrderStatus.SHIPPED]: 1, + [OpenOrderStatus.PENDING]: 1, + [OpenOrderStatus.DELIVERED]: 1, + }); const [searchParams] = useSearchParams(); const navigate = useNavigate(); @@ -74,15 +74,15 @@ const PantryOrderManagement: React.FC = () => { // sortAsc indicates whether the sorting is ascending (oldest first) or descending (newest first) // We store all these here to determine what orders to display for each status const [filterStates, setFilterStates] = useState< - Record + Record >({ - [OrderStatus.SHIPPED]: { + [OpenOrderStatus.SHIPPED]: { sortAsc: false, }, - [OrderStatus.PENDING]: { + [OpenOrderStatus.PENDING]: { sortAsc: false, }, - [OrderStatus.DELIVERED]: { + [OpenOrderStatus.DELIVERED]: { sortAsc: false, }, }); @@ -92,10 +92,10 @@ const PantryOrderManagement: React.FC = () => { const pantryId = await ApiClient.getCurrentUserPantryId(); const data = await ApiClient.getPantryOrders(pantryId); - const grouped: Record = { - [OrderStatus.SHIPPED]: [], - [OrderStatus.PENDING]: [], - [OrderStatus.DELIVERED]: [], + const grouped: Record = { + [OpenOrderStatus.SHIPPED]: [], + [OpenOrderStatus.PENDING]: [], + [OpenOrderStatus.DELIVERED]: [], }; for (const order of data) { @@ -109,10 +109,10 @@ const PantryOrderManagement: React.FC = () => { setStatusOrders(grouped); // Initialize current page for each status - const initialPages: Record = { - [OrderStatus.SHIPPED]: 1, - [OrderStatus.PENDING]: 1, - [OrderStatus.DELIVERED]: 1, + const initialPages: Record = { + [OpenOrderStatus.SHIPPED]: 1, + [OpenOrderStatus.PENDING]: 1, + [OpenOrderStatus.DELIVERED]: 1, }; setCurrentPages(initialPages); } catch { @@ -134,7 +134,7 @@ const PantryOrderManagement: React.FC = () => { if (match) { setSelectedOrderId(match.orderId); // Paginate the containing status to the page that holds this order. - for (const status of Object.values(OrderStatus)) { + for (const status of Object.values(OpenOrderStatus)) { const sorted = [...statusOrders[status]].sort((a, b) => b.createdAt.localeCompare(a.createdAt), ); @@ -153,11 +153,11 @@ const PantryOrderManagement: React.FC = () => { }, [searchParams, statusOrders, navigate]); // Helper to reset page for a specific status - const resetPageForStatus = (status: OrderStatus) => { + const resetPageForStatus = (status: OpenOrderStatus) => { setCurrentPages((prev) => ({ ...prev, [status]: 1 })); }; - const handlePageChange = (status: OrderStatus, page: number) => { + const handlePageChange = (status: OpenOrderStatus, page: number) => { setCurrentPages((prev) => ({ ...prev, [status]: page, @@ -187,7 +187,7 @@ const PantryOrderManagement: React.FC = () => { /> )} - {Object.values(OrderStatus).map((status) => { + {Object.values(OpenOrderStatus).map((status) => { const allOrders = statusOrders[status] || []; const filterState = filterStates[status]; @@ -262,7 +262,7 @@ const PantryOrderManagement: React.FC = () => { interface OrderStatusSectionProps { orders: OrderWithColor[]; - status: OrderStatus; + status: OpenOrderStatus; colors: [string, string]; onOrderSelect: (orderId: number | null) => void; onOrderSelectForAction: (order: OrderWithColor | null) => void; @@ -574,13 +574,13 @@ const OrderStatusSection: React.FC = ({ textAlign="right" color="neutral.700" bgColor={ - order.status !== OrderStatus.SHIPPED + order.status !== OpenOrderStatus.SHIPPED ? 'neutral.50' : 'white' } pr={0} > - {order.status === OrderStatus.SHIPPED && ( + {order.status === OpenOrderStatus.SHIPPED && (