Skip to content

Commit

Permalink
Hotfix for partially fulfilled events (#252)
Browse files Browse the repository at this point in the history
* Hotfix for partially fulfilled events

* Fix counting bug for fulfilled items using sean's code, add a comment

* Update user-facing style to be aligned with Amy's design

* Fix bug
  • Loading branch information
alexzhang1618 authored May 10, 2024
1 parent 958f5e5 commit 5eb7795
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 71 deletions.
18 changes: 9 additions & 9 deletions src/components/admin/store/PickupOrdersPrepareDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Typography } from '@/components/common';
import { PublicOrderItemWithQuantity, PublicOrderWithItems } from '@/lib/types/apiResponses';
import { getOrderItemQuantities } from '@/lib/utils';
import { PublicOrderWithItems } from '@/lib/types/apiResponses';
import { OrderItemQuantity, getOrderItemQuantities } from '@/lib/utils';
import { useMemo } from 'react';
import styles from './style.module.scss';

interface PickupOrdersDisplayPrepareProps {
orders: PublicOrderWithItems[];
}

const itemToString = (item: PublicOrderItemWithQuantity): string => {
const itemToString = (item: OrderItemQuantity): string => {
if (item.option.metadata !== null)
return `${item.option.item.itemName} (${item.option.metadata.type}: ${item.option.metadata.value})`;
return item.option.item.itemName;
};

const PickupOrdersPrepareDisplay = ({ orders }: PickupOrdersDisplayPrepareProps) => {
const itemBreakdown: PublicOrderItemWithQuantity[] = useMemo(() => {
const itemBreakdown: OrderItemQuantity[] = useMemo(() => {
// Concatenate all items together into one large order to display the item breakdown.
const allItems = orders.flatMap(a => a.items);
return getOrderItemQuantities(allItems);
Expand All @@ -34,7 +34,7 @@ const PickupOrdersPrepareDisplay = ({ orders }: PickupOrdersDisplayPrepareProps)
</th>
{itemBreakdown.map(item => {
return (
<tr key={item.uuid}>
<tr key={item.uuids[0]}>
<td>
<Typography variant="h5/regular">{item.quantity}</Typography>
</td>
Expand Down Expand Up @@ -65,10 +65,10 @@ const PickupOrdersPrepareDisplay = ({ orders }: PickupOrdersDisplayPrepareProps)
<td>
<ul className={styles.itemList}>
{itemQuantities.map(item => (
<li key={item.uuid}>
<Typography variant="h5/regular">{`${item.quantity} x ${itemToString(
item
)}`}</Typography>
<li key={item.uuids[0]}>
<Typography variant="h5/regular">{`${item.fulfilled ? '✅' : '❌'} ${
item.quantity
} x ${itemToString(item)}`}</Typography>
</li>
))}
</ul>
Expand Down
158 changes: 104 additions & 54 deletions src/components/store/OrderSummary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@ import PickupEventPreviewModal from '@/components/store/PickupEventPreviewModal'
import { config } from '@/lib';
import { PublicOrderPickupEvent, PublicOrderWithItems } from '@/lib/types/apiResponses';
import { OrderStatus } from '@/lib/types/enums';
import { capitalize, getDefaultOrderItemPhoto, getOrderItemQuantities } from '@/lib/utils';
import {
OrderItemQuantity,
capitalize,
getDefaultOrderItemPhoto,
getOrderItemQuantities,
} from '@/lib/utils';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import styles from './style.module.scss';

interface OrderSummaryProps {
order: PublicOrderWithItems;
orderStatus: OrderStatus;
futurePickupEvents: PublicOrderPickupEvent[];
pickupEvent: PublicOrderPickupEvent;
reschedulePickupEvent: (pickup: PublicOrderPickupEvent) => Promise<void>;
cancelOrder: () => Promise<void>;
}

const isOrderActionable = (status: OrderStatus, pickupEvent: PublicOrderPickupEvent): boolean => {
if (status === OrderStatus.CANCELLED || status === OrderStatus.FULFILLED) {
// If the order is cancelled by the user or fulfilled, no further action can be taken.
Expand All @@ -30,16 +26,108 @@ const isOrderActionable = (status: OrderStatus, pickupEvent: PublicOrderPickupEv
const now = new Date();
const eventStart = new Date(pickupEvent.start);
eventStart.setDate(eventStart.getDate() - 2);
if (
now > eventStart &&
status !== OrderStatus.PICKUP_MISSED &&
status !== OrderStatus.PICKUP_CANCELLED
) {
if (status === OrderStatus.PLACED && now > eventStart) {
return false;
}
return true;
};

interface OrderItemPreviewProps {
item: OrderItemQuantity;
orderStatus: OrderStatus;
}

const OrderItemPreview = ({ item, orderStatus }: OrderItemPreviewProps) => {
if (item.fulfilled && orderStatus === OrderStatus.PLACED) {
/*
* When a partially fulfilled order is rescheduled,
* the status is changed to PLACED, but all items remain (some as fulfilled).
* These items should be hidden.
*/
return null;
}

return (
<Link href={`${config.store.itemRoute}${item.option.item.uuid}`} className={styles.itemInfo}>
<div className={styles.image}>
<Image
src={getDefaultOrderItemPhoto(item)}
style={{ objectFit: 'cover' }}
sizes="9.375rem"
alt="Store item picture"
fill
/>
</div>
<div className={styles.itemSummary}>
<Typography variant="h4/bold">{item.option.item.itemName}</Typography>
<div className={styles.label}>
<Typography variant="h5/bold">Price: </Typography>
<Diamonds count={item.option.price * item.quantity} discount={item.salePriceAtPurchase} />
</div>
<div className={styles.label}>
<Typography variant="h5/bold">Quantity: </Typography>
<Typography variant="h5/regular">{item.quantity}</Typography>
</div>
{item.option.metadata ? (
<div className={styles.label}>
<Typography variant="h5/bold">{`${capitalize(
item.option.metadata.type
)}: `}</Typography>
<Typography variant="h5/regular">{item.option.metadata.value}</Typography>
</div>
) : null}
</div>
</Link>
);
};

interface OrderItemsSummaryProps {
items: OrderItemQuantity[];
orderStatus: OrderStatus;
}

const OrderItemsSummary = ({ items, orderStatus }: OrderItemsSummaryProps) => {
if (orderStatus !== OrderStatus.PARTIALLY_FULFILLED) {
return (
<>
{items.map(item => (
<OrderItemPreview item={item} orderStatus={orderStatus} key={item.uuids[0]} />
))}
</>
);
}
const pickedUpItems = items.filter(item => item.fulfilled);
const notPickedUpItems = items.filter(item => !item.fulfilled);

return (
<div>
<Typography variant="h3/bold" className={styles.partiallyFulfilledText}>
Items Picked Up:
</Typography>
{pickedUpItems.map(item => (
<OrderItemPreview item={item} orderStatus={orderStatus} key={item.uuids[0]} />
))}

<hr className={styles.divider} />
<Typography variant="h3/bold" className={styles.partiallyFulfilledText}>
Awaiting Pickup:
</Typography>
{notPickedUpItems.map(item => (
<OrderItemPreview item={item} orderStatus={orderStatus} key={item.uuids[0]} />
))}
</div>
);
};

interface OrderSummaryProps {
order: PublicOrderWithItems;
orderStatus: OrderStatus;
futurePickupEvents: PublicOrderPickupEvent[];
pickupEvent: PublicOrderPickupEvent;
reschedulePickupEvent: (pickup: PublicOrderPickupEvent) => Promise<void>;
cancelOrder: () => Promise<void>;
}

const OrderSummary = ({
order,
orderStatus,
Expand Down Expand Up @@ -73,45 +161,7 @@ const OrderSummary = ({
onClose={() => setCancelModalOpen(false)}
cancelOrder={cancelOrder}
/>
{items.map(item => (
<Link
href={`${config.store.itemRoute}${item.option.item.uuid}`}
key={item.uuid}
className={styles.itemInfo}
>
<div className={styles.image}>
<Image
src={getDefaultOrderItemPhoto(item)}
style={{ objectFit: 'cover' }}
sizes="9.375rem"
alt="Store item picture"
fill
/>
</div>
<div className={styles.itemSummary}>
<Typography variant="h4/bold">{item.option.item.itemName}</Typography>
<div className={styles.label}>
<Typography variant="h5/bold">Price: </Typography>
<Diamonds
count={item.option.price * item.quantity}
discount={item.salePriceAtPurchase}
/>
</div>
<div className={styles.label}>
<Typography variant="h5/bold">Quantity: </Typography>
<Typography variant="h5/regular">{item.quantity}</Typography>
</div>
{item.option.metadata ? (
<div className={styles.label}>
<Typography variant="h5/bold">{`${capitalize(
item.option.metadata.type
)}: `}</Typography>
<Typography variant="h5/regular">{item.option.metadata.value}</Typography>
</div>
) : null}
</div>
</Link>
))}
<OrderItemsSummary items={items} orderStatus={orderStatus} />
<hr className={styles.divider} />
<div className={styles.footer}>
<div className={styles.buttons}>
Expand Down
7 changes: 5 additions & 2 deletions src/components/store/OrderSummary/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
.container {
margin-top: 0.75rem;

.partiallyFulfilledText {
padding: 0.75rem 1.5rem;
}

.itemInfo {
display: flex;
flex-direction: row;
Expand Down Expand Up @@ -36,8 +40,7 @@
.divider {
border-top: 1px solid var(--theme-elevated-stroke);
height: 0.1rem;
margin: 0 1rem;
margin-top: 0.75rem;
margin: 0.75rem 1rem;
width: auto;
}

Expand Down
1 change: 1 addition & 0 deletions src/components/store/OrderSummary/style.module.scss.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type Styles = {
itemInfo: string;
itemSummary: string;
label: string;
partiallyFulfilledText: string;
totalDiamonds: string;
totalPrice: string;
};
Expand Down
16 changes: 10 additions & 6 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import defaultProfilePictures from '@/lib/constants/profilePictures';
import ranks from '@/lib/constants/ranks';
import showToast from '@/lib/showToast';
import type { URL } from '@/lib/types';
import type { URL, UUID } from '@/lib/types';
import type {
ApiResponse,
CustomErrorBody,
Expand Down Expand Up @@ -314,7 +314,7 @@ export const getDefaultMerchItemPhoto = (
return NoImage.src;
};

export const getDefaultOrderItemPhoto = (item: PublicOrderItem): string => {
export const getDefaultOrderItemPhoto = (item: Pick<PublicOrderItem, 'option'>): string => {
if (item.option.item.uploadedPhoto) {
return item.option.item.uploadedPhoto;
}
Expand Down Expand Up @@ -385,18 +385,22 @@ export const isOrderPickupEvent = (
event: PublicOrderPickupEvent | PublicEvent
): event is PublicOrderPickupEvent => 'status' in event;

export type OrderItemQuantity = Omit<PublicOrderItemWithQuantity, 'uuid'> & { uuids: UUID[] };

/**
* Condenses a list of ordered items into unique items with quantities.
*/
export const getOrderItemQuantities = (items: PublicOrderItem[]): PublicOrderItemWithQuantity[] => {
const itemMap = new Map<string, PublicOrderItemWithQuantity>();
export const getOrderItemQuantities = (items: PublicOrderItem[]): OrderItemQuantity[] => {
const itemMap = new Map<string, OrderItemQuantity>();

items.forEach(item => {
const existingItem = itemMap.get(item.option.uuid);
const hash = `${item.option.uuid} ${item.fulfilled}`;
const existingItem = itemMap.get(hash);
if (existingItem) {
existingItem.quantity += 1;
existingItem.uuids.push(item.uuid);
} else {
itemMap.set(item.option.uuid, { ...item, quantity: 1 });
itemMap.set(hash, { ...item, quantity: 1, uuids: [item.uuid] });
}
});

Expand Down

0 comments on commit 5eb7795

Please sign in to comment.