diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 85c87ee7a..e74b8369d 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -10,6 +10,7 @@ import { ValidationPipe, UploadedFiles, UseInterceptors, + PayloadTooLargeException, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { OrdersService } from './order.service'; @@ -164,16 +165,35 @@ export class OrdersController { }, }) @UseInterceptors( - FilesInterceptor('photos', 10, { storage: multer.memoryStorage() }), + FilesInterceptor('photos', 10, { + storage: multer.memoryStorage(), + limits: { + fileSize: 5 * 1024 * 1024, // 5 MB in bytes + }, + }), ) async confirmDelivery( @Param('orderId', ParseIntPipe) orderId: number, @Body() body: ConfirmDeliveryDto, @UploadedFiles() photos?: Express.Multer.File[], ): Promise { - const uploadedPhotoUrls = - photos && photos.length > 0 ? await this.awsS3Service.upload(photos) : []; - - return this.ordersService.confirmDelivery(orderId, body, uploadedPhotoUrls); + try { + const uploadedPhotoUrls = + photos && photos.length > 0 + ? await this.awsS3Service.upload(photos) + : []; + return this.ordersService.confirmDelivery( + orderId, + body, + uploadedPhotoUrls, + ); + } catch (err: any) { + if (err.code === 'LIMIT_FILE_SIZE') { + throw new PayloadTooLargeException( + 'Each photo must be 5 MB or smaller', + ); + } + throw err; + } } } diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 37e4ed11f..9710febe2 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -22,8 +22,11 @@ import { OrderSummary, UserDto, OrderDetails, + ConfirmDeliveryDto, + OrderWithoutRelations, Assignments, FoodRequestSummaryDto, + OrderWithoutFoodManufacturer, PantryWithUser, } from 'types/types'; @@ -169,6 +172,14 @@ export class ApiClient { .then((response) => response.data); } + public async getPantryOrders( + pantryId: number, + ): Promise { + return this.axiosInstance + .get(`/api/pantries/${pantryId}/orders`) + .then((response) => response.data); + } + public async getPantry(pantryId: number): Promise { return this.get(`/api/pantries/${pantryId}`) as Promise; } @@ -226,6 +237,32 @@ export class ApiClient { .then((response) => response.data); } + public async confirmOrderDelivery( + orderId: number, + dto: ConfirmDeliveryDto, + photos: File[], + ): Promise { + const formData = new FormData(); + + // DTO fields + formData.append('dateReceived', dto.dateReceived); + if (dto.feedback) { + formData.append('feedback', dto.feedback); + } + + // files (must be key = "photos") + for (const file of photos) { + formData.append('photos', file); + } + + const { data } = await this.axiosInstance.patch( + `/api/orders/${orderId}/confirm-delivery`, + formData, + ); + + return data; + } + public async postManufacturer( data: ManufacturerApplicationDto, ): Promise> { @@ -294,31 +331,6 @@ export class ApiClient { return data as FoodRequest[]; } - public async confirmDelivery( - requestId: number, - data: FormData, - ): Promise { - try { - const response = await this.axiosInstance.post( - `/api/requests/${requestId}/confirm-delivery`, - data, - ); - - if (response.status === 200) { - alert('Delivery confirmation submitted successfully'); - if (this.navigate) { - this.navigate('/request-form'); - } else { - window.location.href = '/request-form'; - } - } else { - alert(`Failed to submit: ${response.statusText}`); - } - } catch (error) { - alert(`Error submitting delivery confirmation: ${error}`); - } - } - public async getCurrentUserPantryId(): Promise { const data = await this.get('/api/pantries/my-id'); return data as number; diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index d679ca6e4..7ca9980c2 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -5,7 +5,6 @@ import PantryPastOrders from '@containers/pantryPastOrders'; import Pantries from '@containers/pantries'; import Orders from '@containers/orders'; import PantryDashboard from '@containers/pantryDashboard'; -import { submitDeliveryConfirmationFormModal } from '@components/forms/deliveryConfirmationModal'; import FormRequests from '@containers/formRequests'; import PantryApplication from '@containers/pantryApplication'; import ApplicationSubmitted from '@containers/applicationSubmitted'; @@ -26,6 +25,7 @@ import ForgotPasswordPage from '@containers/forgotPasswordPage'; import ProtectedRoute from '@components/protectedRoute'; import Unauthorized from '@containers/unauthorized'; import { Authenticator } from '@aws-amplify/ui-react'; +import PantryOrderManagement from '@containers/pantryOrderManagement'; import FoodManufacturerApplication from '@containers/foodManufacturerApplication'; import { submitManufacturerApplicationForm } from '@components/forms/manufacturerApplicationForm'; import AssignedPantries from '@containers/volunteerAssignedPantries'; @@ -187,8 +187,12 @@ const router = createBrowserRouter([ ), }, { - path: '/confirm-delivery', - action: submitDeliveryConfirmationFormModal, + path: '/pantry-order-management', + element: ( + + + + ), }, { path: '/volunteer-assigned-pantries', diff --git a/apps/frontend/src/chakra-ui.d.ts b/apps/frontend/src/chakra-ui.d.ts index d4db3faea..175a19c79 100644 --- a/apps/frontend/src/chakra-ui.d.ts +++ b/apps/frontend/src/chakra-ui.d.ts @@ -33,6 +33,10 @@ declare module '@chakra-ui/react' { extends ComponentPropsStrictChildren {} export interface MenuRadioItemProps extends ComponentPropsLenientChildren {} + // FileUpload components + export interface FileUploadDropzoneProps + extends ComponentPropsLenientChildren {} + // Dialog components export interface DialogCloseTriggerProps extends ComponentPropsStrictChildren {} diff --git a/apps/frontend/src/components/forms/deliveryConfirmationModal.tsx b/apps/frontend/src/components/forms/deliveryConfirmationModal.tsx deleted file mode 100644 index a0bb284d4..000000000 --- a/apps/frontend/src/components/forms/deliveryConfirmationModal.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { - Box, - Field, - Input, - Button, - Textarea, - HStack, - Text, - Dialog, -} from '@chakra-ui/react'; -import { - Form, - ActionFunction, - ActionFunctionArgs, - redirect, -} from 'react-router-dom'; -import ApiClient from '@api/apiClient'; - -interface DeliveryConfirmationModalProps { - requestId: number; - isOpen: boolean; - onClose: () => void; - pantryId: number; -} - -const photoNames: string[] = []; -const globalPhotos: File[] = []; - -const DeliveryConfirmationModal: React.FC = ({ - requestId, - isOpen, - onClose, - pantryId, -}) => { - const handlePhotoChange = async ( - event: React.ChangeEvent, - ) => { - const files = event.target.files; - - if (files) { - for (const file of Array.from(files)) { - if (!photoNames.some((photo) => photo.includes(file.name))) { - try { - photoNames.push(file.name); - globalPhotos.push(file); - } catch (error) { - alert('Failed to handle ' + file.name + ': ' + error); - } - } - } - } - }; - - const renderPhotoNames = () => { - return globalPhotos.map((photo, index) => ( - - - {photo.name} - - - )); - }; - - return ( - { - if (!e.open) onClose(); - }} - size="xl" - closeOnInteractOutside - > - - - - - - Delivery Confirmation Form - - - -
- - - - - - Delivery Date - - - - - Select the delivery date. - - - - - Feedback - - -