Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
OrderSummary,
UserDto,
OrderDetails,
ConfirmDeliveryDto,
} from 'types/types';

const defaultBaseUrl =
Expand Down Expand Up @@ -160,6 +161,12 @@ export class ApiClient {
.then((response) => response.data);
}

public async getPantryOrders(pantryId: number): Promise<OrderSummary[]> {
return this.axiosInstance
.get(`/api/pantries/${pantryId}/orders`)
.then((response) => response.data);
}

public async getPantry(pantryId: number): Promise<Pantry> {
return this.get(`/api/pantries/${pantryId}`) as Promise<Pantry>;
}
Expand Down Expand Up @@ -213,6 +220,32 @@ export class ApiClient {
.then((response) => response.data);
}

public async confirmOrderDelivery(
orderId: number,
dto: ConfirmDeliveryDto,
photos: File[],
): Promise<Order> {
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<AxiosResponse<void>> {
Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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';

Expand Down Expand Up @@ -198,6 +199,14 @@ const router = createBrowserRouter([
</ProtectedRoute>
),
},
{
path: '/pantry-order-management',
element: (
<ProtectedRoute>
<PantryOrderManagement />
</ProtectedRoute>
),
},
{
path: '/confirm-delivery',
action: submitDeliveryConfirmationFormModal,
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/chakra-ui.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
228 changes: 228 additions & 0 deletions apps/frontend/src/components/forms/orderReceivedActionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import React, { useState } from 'react';
import {
Flex,
Button,
Textarea,
Text,
Dialog,
Box,
Field,
CloseButton,
Input,
FileUpload,
Icon,
} from '@chakra-ui/react';
import { Upload } from 'lucide-react';
import { ConfirmDeliveryDto } from 'types/types';
import apiClient from '@api/apiClient';

interface OrderReceivedActionModalProps {
orderId: number;
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}

const MAX_FILES = 10;

const OrderReceivedActionModal: React.FC<OrderReceivedActionModalProps> = ({
orderId,
isOpen,
onClose,
onSuccess,
}) => {
const [alertMessage, setAlertMessage] = useState<string>('');
const [feedback, setFeedback] = useState<string>('');
const [dateReceived, setDateReceived] = useState<string>('');
const [photos, setPhotos] = useState<File[]>([]);

const isFormValid = dateReceived !== '';

const resetForm = () => {
setAlertMessage('');
setFeedback('');
setDateReceived('');
setPhotos([]);
};

const handleSubmit = async () => {
try {
const dto: ConfirmDeliveryDto = {
dateReceived: new Date(dateReceived).toISOString(),
feedback: feedback,
};

await apiClient.confirmOrderDelivery(orderId, dto, photos);

setAlertMessage('Delivery Confirmed');
resetForm();
onSuccess();
onClose();
} catch (err) {
setAlertMessage('Delivery could not be confirmed.');
resetForm();
onClose();
}
};

return (
<Dialog.Root
open={isOpen}
size="xl"
onOpenChange={(e: { open: boolean }) => {
if (!e.open) onClose();
}}
closeOnInteractOutside
>
{alertMessage && (
// TODO: add Justin's alert component/uncomment below out and remove text component
// <FloatingAlert message={alertMessage} status="error" timeout={6000} />
<Text>{alertMessage}</Text>
)}
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content maxW={650}>
<Dialog.Header pb={0} mt={2}>
<Dialog.Title
color="black"
fontSize="lg"
fontWeight={700}
fontFamily="inter"
>
Action Required
</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<Text
mb={5}
color="#52525B"
textStyle="p2"
pt={0}
mt={2}
textAlign={'left'}
>
As this order arrives, please confirm the donation receipt and
delivery of the order by filling out the details below.
</Text>
<Box>
<Field.Root required mb={4}>
<Field.Label>
<Text textStyle="p2" fontWeight={600} color="neutral.800">
Date Received
</Text>
</Field.Label>
<Input
type="date"
textStyle="p2"
w="full"
bg="white"
borderColor="neutral.100"
color="neutral.700"
borderWidth="1px"
borderRadius="4px"
onChange={(e) => setDateReceived(e.target.value)}
value={dateReceived}
/>
</Field.Root>

<Field.Root mb={4}>
<Field.Label>
<Text textStyle="p2" fontWeight={600} color="neutral.800">
Feedback
</Text>
</Field.Label>
<Textarea
pl={2.5}
size="lg"
textStyle="p2"
color="neutral.800"
borderColor="neutral.100"
minH={150}
value={feedback}
onChange={(e) => {
const inputText = e.target.value;
const words = inputText.trim().split(/\s+/);

if (words.length <= 250) {
setFeedback(e.target.value);
} else {
alert('Exceeded word limit');
}
}}
/>

<Field.HelperText color="neutral.600">
Max 250 words
</Field.HelperText>
</Field.Root>

<Field.Root mb={4}>
<Field.Label>
<Text textStyle="p2" fontWeight={600} color="neutral.800">
Photos
</Text>
</Field.Label>
<FileUpload.Root
accept={['image/png', 'image/jpeg', 'image/jpg']}
alignItems="stretch"
maxFiles={MAX_FILES}
onFileChange={(e: any) => {
const files: File[] = e?.acceptedFiles ?? [];
setPhotos(files);
}}
>
<FileUpload.HiddenInput />
<FileUpload.Dropzone
borderColor="neutral.100"
borderRadius="4px"
borderStyle="solid"
borderWidth="1px"
minH="150px"
>
<Icon size="md" color="fg.muted">
<Upload />
</Icon>
<FileUpload.DropzoneContent>
<Box textStyle="p2" fontWeight={600}>
Drag and drop here to upload
</Box>
<Box textStyle="p2" color="neutral.800">
.png, .jpg up to 5MB
</Box>
</FileUpload.DropzoneContent>
</FileUpload.Dropzone>
<FileUpload.List clearable />
</FileUpload.Root>
</Field.Root>

<Flex justifyContent="flex-end" mt={4} gap={2}>
<Button
onClick={onClose}
bg={'white'}
color={'black'}
borderColor="neutral.100"
>
Cancel
</Button>

<Button
onClick={handleSubmit}
bg={isFormValid ? '#213C4A' : 'neutral.400'}
color={'white'}
disabled={!isFormValid}
>
Continue
</Button>
</Flex>
</Box>
</Dialog.Body>
<Dialog.CloseTrigger asChild>
<CloseButton size="lg" />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
);
};

export default OrderReceivedActionModal;
7 changes: 7 additions & 0 deletions apps/frontend/src/containers/homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ const Homepage: React.FC = () => {
</RouterLink>
</Link>
</ListItem>
<ListItem textAlign="center">
<Link asChild color="teal.500">
<RouterLink to="/pantry-order-management">
Pantry Order Management
</RouterLink>
</Link>
</ListItem>
</List.Root>
</Box>

Expand Down
Loading