Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fulfill #244

Merged
merged 24 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3c4be9a
Add fulfill button
SheepTester Apr 29, 2024
f21f8f1
Remove Fulfill view and move fulfill buttons to prepare view
SheepTester Apr 29, 2024
b39c58e
Do not show fulfill button if it's already fulfilled
SheepTester Apr 29, 2024
6fd90be
make order status component
SheepTester Apr 29, 2024
85e3484
hide fulfill button if pickup event hasnt started yet
SheepTester Apr 29, 2024
4aa2379
Apparently partially fulfilled means they are missing some items
SheepTester Apr 29, 2024
7629d92
move order row to separate component
SheepTester Apr 30, 2024
f93f5d1
fulfill selected
SheepTester May 4, 2024
31432e7
Merge branch 'main' into sean/fulfill
SheepTester May 5, 2024
070291d
fix type errors
SheepTester May 5, 2024
4550fa0
Merge remote-tracking branch 'origin/main' into sean/fulfill
SheepTester May 14, 2024
2d5b880
always show fulfilled items
SheepTester May 14, 2024
36c1567
sorting things and other misc changes:
SheepTester May 14, 2024
dd810b1
exclude fulfilled items from item breakdown
SheepTester May 14, 2024
26fe66c
default checkboxes to unchecked
SheepTester May 14, 2024
45772bf
fix fulfilling selected. always indicate whether an item was fulfilled
SheepTester May 14, 2024
3f71919
Show fulfilled badges
SheepTester May 14, 2024
a664aea
Labels back to emoji, have checkbox select all of an entire variant a…
SheepTester Aug 19, 2024
cd164cb
Merge branch 'main' into sean/fulfill
SheepTester Aug 19, 2024
efc08fb
Display "fulfilled" on fulfilled items in fulfilled/partially fulfill…
SheepTester Aug 19, 2024
505140c
Make "picked up" badge green
SheepTester Aug 19, 2024
a5a1e96
Merge branch 'main' into sean/fulfill
SheepTester Aug 20, 2024
95d304f
Address feedback
SheepTester Aug 21, 2024
d7bd9d1
Merge branch 'sean/fulfill' of https://github.com/acmucsd/membership-…
SheepTester Aug 21, 2024
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
102 changes: 102 additions & 0 deletions src/components/admin/store/PickupOrder/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Button, Typography } from '@/components/common';
import { OrderStatusIndicator } from '@/components/store';
import { StoreAPI } from '@/lib/api';
import { UUID } from '@/lib/types';
import { PublicOrderWithItems } from '@/lib/types/apiResponses';
import { OrderStatus } from '@/lib/types/enums';
import { getOrderItemQuantities, itemToString, reportError } from '@/lib/utils';
import { useState } from 'react';
import styles from './style.module.scss';

interface PickupOrderProps {
token: string;
canFulfill: boolean;
order: PublicOrderWithItems;
onOrderUpdate: (orders: PublicOrderWithItems) => void;
}

const PickupOrder = ({ token, canFulfill, order, onOrderUpdate }: PickupOrderProps) => {
const itemQuantities = getOrderItemQuantities(order.items);
const [selected, setSelected] = useState(new Set<UUID>());

return (
<tr className={styles.row}>
<td>
<Typography variant="h5/regular">{`${order.user.firstName} ${order.user.lastName}`}</Typography>
<OrderStatusIndicator orderStatus={order.status} />
</td>
<td>
<ul className={styles.itemList}>
{itemQuantities.map(item => {
let badge = null;
if (
!item.fulfilled &&
(order.status === OrderStatus.FULFILLED ||
order.status === OrderStatus.PARTIALLY_FULFILLED)
) {
badge = (
<span className={styles.notFulfilled} title="Not fulfilled">
</span>
);
} else if (item.fulfilled) {
badge = (
<span className={styles.fulfilled} title="Fulfilled">
</span>
);
}
return (
<li key={item.uuids[0]}>
<label>
{canFulfill && !item.fulfilled ? (
<input
type="checkbox"
defaultChecked={false}
onChange={e => {
setSelected(
new Set(
e.currentTarget.checked
? [...Array.from(selected), ...item.uuids]
: Array.from(selected).filter(uuid => !item.uuids.includes(uuid))
)
);
}}
/>
) : null}
<Typography variant="h5/regular" component="span">
{`${item.quantity} x ${itemToString(item)}`} {badge}
</Typography>
</label>
</li>
);
})}
</ul>
{canFulfill ? (
<Button
size="small"
onClick={async () => {
try {
const items = order.items.filter(item => selected.has(item.uuid));
const newOrder = await StoreAPI.fulfillOrderPickup(token, order.uuid, items);
const itemUuids = items.map(item => item.uuid);
onOrderUpdate({
...newOrder,
items: order.items.map(item =>
itemUuids.includes(item.uuid) ? { ...item, fulfilled: true } : item
),
});
} catch (error: unknown) {
reportError('Failed to fulfill order', error);
}
}}
SheepTester marked this conversation as resolved.
Show resolved Hide resolved
>
Fulfill {selected.size} item{selected.size === 1 ? '' : 's'}
</Button>
) : null}
</td>
</tr>
);
};

export default PickupOrder;
35 changes: 35 additions & 0 deletions src/components/admin/store/PickupOrder/style.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@use 'src/styles/vars.scss' as vars;

.row {
border-top: 1px solid var(--theme-accent-line-2);

td {
padding: 1.5rem;
}

.itemList {
list-style-type: disc;

li {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 10px;

.notFulfilled {
color: vars.$red;
}

.fulfilled {
color: vars.$green-300;
}

[type='checkbox'] {
cursor: pointer;
height: 1rem;
margin-right: 5px;
width: 1rem;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export type Styles = {
breakdown: string;
container: string;
table: string;
fulfilled: string;
itemList: string;
notFulfilled: string;
row: string;
};

export type ClassNames = keyof Styles;
Expand Down
20 changes: 0 additions & 20 deletions src/components/admin/store/PickupOrdersFulfillDisplay/index.tsx

This file was deleted.

This file was deleted.

148 changes: 94 additions & 54 deletions src/components/admin/store/PickupOrdersPrepareDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,121 @@
import PickupOrder from '@/components/admin/store/PickupOrder';
import { Typography } from '@/components/common';
import { PublicOrderWithItems } from '@/lib/types/apiResponses';
import { OrderItemQuantity, getOrderItemQuantities } from '@/lib/utils';
import { OrderStatus } from '@/lib/types/enums';
import { OrderItemQuantity, getOrderItemQuantities, itemToString } from '@/lib/utils';
import { useMemo } from 'react';
import styles from './style.module.scss';

interface PickupOrdersDisplayPrepareProps {
token: string;
canFulfill: boolean;
orders: PublicOrderWithItems[];
onOrderUpdate: (orders: PublicOrderWithItems[]) => void;
}

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 PickupOrdersPrepareDisplay = ({
token,
canFulfill,
orders,
onOrderUpdate,
}: PickupOrdersDisplayPrepareProps) => {
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);
// Don't include already-fulfilled items from rescheduled partially fulfilled orders
return getOrderItemQuantities(allItems).filter(item => !item.fulfilled);
}, [orders]);

const sorted = useMemo(
() =>
[...orders].sort((a, b) => {
if (a.status !== b.status) {
// Sort by PLACED first
return (
(b.status === OrderStatus.PLACED ? 1 : 0) - (a.status === OrderStatus.PLACED ? 1 : 0)
);
}
// Get the latest pickup time. Default to order time.
const aTime = a.items.reduce((acc, curr) => {
if (!curr.fulfilledAt) {
return acc;
}
return Math.max(acc, new Date(curr.fulfilledAt).getTime());
}, new Date(a.orderedAt).getTime());
const bTime = b.items.reduce((acc, curr) => {
if (!curr.fulfilledAt) {
return acc;
}
return Math.max(acc, new Date(curr.fulfilledAt).getTime());
}, new Date(b.orderedAt).getTime());
// Sort by soonest first
return bTime - aTime;
}),
[orders]
);

return (
<div className={styles.container}>
<div className={styles.breakdown}>
<Typography variant="h3/bold">Item Breakdown</Typography>
<table className={styles.table}>
<th>
<Typography variant="h4/bold">Quantity</Typography>
</th>
<th>
<Typography variant="h4/bold">Item</Typography>
</th>
{itemBreakdown.map(item => {
return (
<tr key={item.uuids[0]}>
<td>
<Typography variant="h5/regular">{item.quantity}</Typography>
</td>
<td>
<Typography variant="h5/regular">{itemToString(item)}</Typography>
</td>
</tr>
);
})}
<thead>
<tr>
<th>
<Typography variant="h4/bold">Quantity</Typography>
</th>
<th>
<Typography variant="h4/bold">Item</Typography>
</th>
</tr>
</thead>
<tbody>
{itemBreakdown.map(item => {
return (
<tr key={item.uuids[0]}>
<td>
<Typography variant="h5/regular">{item.quantity}</Typography>
</td>
<td>
<Typography variant="h5/regular">{itemToString(item)}</Typography>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className={styles.breakdown}>
<Typography variant="h3/bold">User Breakdown</Typography>
<table className={styles.table}>
<th>
<Typography variant="h4/bold">User</Typography>
</th>
<th>
<Typography variant="h4/bold">Items</Typography>
</th>
{orders.map(order => {
const itemQuantities = getOrderItemQuantities(order.items);
return (
<tr key={order.uuid}>
<td>
<Typography variant="h5/regular">{`${order.user.firstName} ${order.user.lastName}`}</Typography>
</td>
<td>
<ul className={styles.itemList}>
{itemQuantities.map(item => (
<li key={item.uuids[0]}>
<Typography variant="h5/regular">{`${item.fulfilled ? '✅' : '❌'} ${
item.quantity
} x ${itemToString(item)}`}</Typography>
</li>
))}
</ul>
</td>
</tr>
);
})}
<thead>
<tr>
<th>
<Typography variant="h4/bold">User</Typography>
</th>
<th>
<Typography variant="h4/bold">Items</Typography>
</th>
</tr>
</thead>
<tbody>
{sorted.map(order => (
<PickupOrder
canFulfill={canFulfill && order.status === OrderStatus.PLACED}
order={order}
onOrderUpdate={newOrder =>
onOrderUpdate(
orders.map(
(order): PublicOrderWithItems =>
order.uuid === newOrder.uuid ? newOrder : order
)
)
}
token={token}
key={order.uuid}
/>
))}
</tbody>
</table>
</div>
</div>
Expand Down
Loading