Skip to content

Commit aa7bff4

Browse files
authored
Merge pull request #1250 from jboolean/order-review-page
Merch order review page
2 parents f84d95a + 6db1879 commit aa7bff4

File tree

10 files changed

+309
-28
lines changed

10 files changed

+309
-28
lines changed

frontend/src/screens/App/screens/Admin/AdminRoutes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22

33
import { Route, Switch, useRouteMatch } from 'react-router-dom';
44
import LoginPage from './screens/LoginPage';
5+
import ReviewMerch from './screens/ReviewMerch';
56
import ReviewStories from './screens/ReviewStories';
67
import PrivateRoute from './shared/components/PrivateRoute';
78

@@ -12,6 +13,7 @@ export default function AdminRoutes(): JSX.Element {
1213
<Switch>
1314
<Route path={`${path}/login`} component={LoginPage} />
1415
<PrivateRoute path={`${path}/review-stories`} component={ReviewStories} />
16+
<PrivateRoute path={`${path}/review-merch`} component={ReviewMerch} />
1517
</Switch>
1618
);
1719
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
@import '../../../../../../shared/styles/colors.less';
2+
3+
.container {
4+
max-width: 1200px;
5+
margin: 0 auto;
6+
padding: 20px;
7+
}
8+
9+
.orders {
10+
display: flex;
11+
flex-direction: column;
12+
gap: 20px;
13+
}
14+
15+
.order {
16+
border: 1px solid @creme;
17+
padding: 20px;
18+
}
19+
20+
.orderHeader {
21+
display: flex;
22+
justify-content: space-between;
23+
align-items: flex-start;
24+
margin-bottom: 20px;
25+
gap: 20px;
26+
}
27+
28+
.metadata {
29+
flex: 1;
30+
}
31+
32+
.buttons {
33+
display: flex;
34+
gap: 10px;
35+
flex-shrink: 0;
36+
}
37+
38+
.items {
39+
border-top: 1px solid @creme;
40+
padding-top: 20px;
41+
}
42+
43+
.item {
44+
border: 1px solid @creme;
45+
padding: 15px;
46+
margin-bottom: 10px;
47+
}
48+
49+
.customization {
50+
margin-top: 10px;
51+
padding: 10px;
52+
background: @creme;
53+
}
54+
55+
.attentionSection {
56+
margin-top: 40px;
57+
}
58+
59+
@media (max-width: 768px) {
60+
.orderHeader {
61+
flex-direction: column;
62+
align-items: stretch;
63+
}
64+
65+
.buttons {
66+
justify-content: center;
67+
}
68+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React from 'react';
2+
import Button from 'shared/components/Button';
3+
import { Order, OrderItem } from 'shared/utils/merch/Order';
4+
import stylesheet from './ReviewMerch.less';
5+
import useReviewMerchStore from './stores/ReviewMerchStore';
6+
7+
const DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
8+
dateStyle: 'full',
9+
timeStyle: 'short',
10+
});
11+
12+
function OrderMetadataView({ order }: { order: Order }): JSX.Element {
13+
return (
14+
<div className={stylesheet.metadata}>
15+
<div>
16+
<strong>Order #{order.id}</strong>
17+
</div>
18+
<div>
19+
<time dateTime={order.createdAt}>
20+
{DATE_FORMATTER.format(Date.parse(order.createdAt))}
21+
</time>
22+
</div>
23+
{order.email && <div>Email: {order.email}</div>}
24+
<div>State: {order.state}</div>
25+
{order.fulfillmentState && (
26+
<div>Fulfillment: {order.fulfillmentState}</div>
27+
)}
28+
</div>
29+
);
30+
}
31+
32+
function OrderItemView({ item }: { item: OrderItem }): JSX.Element {
33+
return (
34+
<div className={stylesheet.item}>
35+
<div>Item #{item.id}</div>
36+
<div>Variant: {item.internalVariant}</div>
37+
<div>State: {item.state}</div>
38+
{item.printfileUrl && (
39+
<div>
40+
<a
41+
href={item.printfileUrl}
42+
target="_blank"
43+
rel="noreferrer"
44+
className={stylesheet.printfileLink}
45+
>
46+
View Printfile
47+
</a>
48+
</div>
49+
)}
50+
{item.customizationOptions && (
51+
<div className={stylesheet.customization}>
52+
<strong>Customization:</strong>
53+
<div>Style: {item.customizationOptions.style}</div>
54+
<div>Foreground: {item.customizationOptions.foregroundColor}</div>
55+
<div>Background: {item.customizationOptions.backgroundColor}</div>
56+
<div>
57+
Location: {item.customizationOptions.lat},{' '}
58+
{item.customizationOptions.lng}
59+
</div>
60+
</div>
61+
)}
62+
</div>
63+
);
64+
}
65+
66+
export default function ReviewMerch(): JSX.Element {
67+
const reviewMerchStore = useReviewMerchStore();
68+
69+
React.useEffect(() => {
70+
reviewMerchStore.loadOrders();
71+
reviewMerchStore.loadOrdersNeedingAttention();
72+
// eslint-disable-next-line react-hooks/exhaustive-deps
73+
}, []);
74+
75+
return (
76+
<div className={stylesheet.container}>
77+
<h1>Review Merch Orders</h1>
78+
79+
<div className={stylesheet.orders}>
80+
{reviewMerchStore.orders.map((order) => (
81+
<div key={order.id} className={stylesheet.order}>
82+
<div className={stylesheet.orderHeader}>
83+
<OrderMetadataView order={order} />
84+
85+
<div className={stylesheet.buttons}>
86+
<Button
87+
onClick={() => reviewMerchStore.fulfillOrder(order.id)}
88+
buttonStyle={'primary'}
89+
>
90+
Fulfill
91+
</Button>
92+
<Button
93+
onClick={() => reviewMerchStore.cancelOrder(order.id)}
94+
buttonStyle={'secondary'}
95+
>
96+
Cancel
97+
</Button>
98+
</div>
99+
</div>
100+
101+
<div className={stylesheet.items}>
102+
<h3>Items ({order.items.length})</h3>
103+
{order.items.map((item) => (
104+
<OrderItemView key={item.id} item={item} />
105+
))}
106+
</div>
107+
</div>
108+
))}
109+
</div>
110+
111+
{reviewMerchStore.ordersNeedingAttention.length > 0 && (
112+
<div className={stylesheet.attentionSection}>
113+
<h2>Orders Needing Attention</h2>
114+
<div className={stylesheet.orders}>
115+
{reviewMerchStore.ordersNeedingAttention.map((order) => (
116+
<div key={order.id} className={stylesheet.order}>
117+
<OrderMetadataView order={order} />
118+
<div className={stylesheet.items}>
119+
<h3>Items ({order.items.length})</h3>
120+
{order.items.map((item) => (
121+
<OrderItemView key={item.id} item={item} />
122+
))}
123+
</div>
124+
</div>
125+
))}
126+
</div>
127+
</div>
128+
)}
129+
</div>
130+
);
131+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { MerchOrderState, Order } from 'shared/utils/merch/Order';
2+
import {
3+
getOrdersForReview,
4+
getOrdersNeedingAttention,
5+
updateOrderState,
6+
} from 'shared/utils/merch/merchApi';
7+
import create from 'zustand';
8+
import { immer } from 'zustand/middleware/immer';
9+
10+
interface State {
11+
orders: Order[];
12+
ordersNeedingAttention: Order[];
13+
}
14+
15+
interface Actions {
16+
fulfillOrder(orderId: number): void;
17+
cancelOrder(orderId: number): void;
18+
loadOrders(): void;
19+
loadOrdersNeedingAttention(): void;
20+
}
21+
22+
const useReviewMerchStore = create(
23+
immer<State & Actions>((set) => ({
24+
orders: [],
25+
ordersNeedingAttention: [],
26+
loadOrders: async () => {
27+
const orders = await getOrdersForReview();
28+
set((state) => {
29+
state.orders = orders;
30+
});
31+
},
32+
loadOrdersNeedingAttention: async () => {
33+
const orders = await getOrdersNeedingAttention();
34+
set((state) => {
35+
state.ordersNeedingAttention = orders;
36+
});
37+
},
38+
fulfillOrder: async (orderId: number) => {
39+
await updateOrderState(
40+
orderId,
41+
MerchOrderState.SUBMITTED_FOR_FULFILLMENT
42+
);
43+
set((state) => {
44+
state.orders = state.orders.filter((order) => order.id !== orderId);
45+
});
46+
},
47+
cancelOrder: async (orderId: number) => {
48+
await updateOrderState(orderId, MerchOrderState.CANCELED);
49+
set((state) => {
50+
state.orders = state.orders.filter((order) => order.id !== orderId);
51+
});
52+
},
53+
}))
54+
);
55+
56+
export default useReviewMerchStore;

frontend/src/screens/App/screens/Merch/screens/Orders/components/CustomizeModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
setOverlay,
1111
} from 'screens/App/screens/MapPane/components/MainMap/overlays';
1212
import Button from 'shared/components/Button';
13+
import { MerchCustomizationOptions } from 'shared/utils/merch/Order';
1314
import useElementId from 'shared/utils/useElementId';
14-
import { MerchCustomizationOptions } from '../shared/utils/Order';
1515
import stylesheet from './CustomizeModal.less';
1616

1717
const DEFAULT_LNG_LAT = {

frontend/src/screens/App/screens/Merch/screens/Orders/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import React, { useEffect } from 'react';
22

33
import { Link } from 'react-router-dom';
44
import absurd from 'shared/utils/absurd';
5-
import CustomizeModal from './components/CustomizeModal';
6-
import LoginModal from './components/LoginModal';
7-
import useOrdersStore from './shared/stores/OrdersStore';
85
import {
96
MerchInternalVariant,
107
MerchItemState,
118
MerchOrderFulfillmentState,
129
MerchOrderState,
1310
Order,
14-
} from './shared/utils/Order';
11+
} from 'shared/utils/merch/Order';
12+
import CustomizeModal from './components/CustomizeModal';
13+
import LoginModal from './components/LoginModal';
14+
import useOrdersStore from './shared/stores/OrdersStore';
1515

1616
import Button from 'shared/components/Button';
1717
import { ColorThemeContext } from 'shared/components/ColorThemeContext';

frontend/src/screens/App/screens/Merch/screens/Orders/shared/stores/OrdersStore.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import useLoginStore from 'shared/stores/LoginStore';
2+
import {
3+
MerchCustomizationOptions,
4+
Order,
5+
OrderItem,
6+
} from 'shared/utils/merch/Order';
7+
import * as merchApi from 'shared/utils/merch/merchApi';
28
import create from 'zustand';
39
import { immer } from 'zustand/middleware/immer';
4-
import { MerchCustomizationOptions, Order, OrderItem } from '../utils/Order';
5-
import * as merchApi from '../utils/merchApi';
610

711
interface State {
812
orders: Order[] | null;

frontend/src/screens/App/screens/Merch/screens/Orders/shared/utils/merchApi.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ export interface OrderItem {
4545
internalVariant: MerchInternalVariant;
4646
customizationOptions?: MerchCustomizationOptions;
4747
state: MerchItemState;
48+
printfileUrl?: string;
4849
}
4950

5051
export interface Order {
5152
id: number;
5253
createdAt: string;
54+
userId: number;
55+
email?: string;
5356
state: MerchOrderState;
5457
fulfillmentState?: MerchOrderFulfillmentState;
5558
items: OrderItem[];
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import api from 'shared/utils/api';
2+
import { MerchCustomizationOptions, MerchOrderState, Order } from './Order';
3+
4+
export async function getMyOrders(): Promise<Order[]> {
5+
const response = await api.get<Order[]>('/merch/orders');
6+
return response.data;
7+
}
8+
9+
export async function updateCustomizationOptions(
10+
itemId: number,
11+
customizationOptions: MerchCustomizationOptions
12+
): Promise<void> {
13+
await api.put(
14+
`/merch/items/${itemId}/customization-options`,
15+
customizationOptions
16+
);
17+
}
18+
19+
export async function finalizeCustomization(itemId: number): Promise<void> {
20+
await api.post(`/merch/items/${itemId}/finalize-customizations`);
21+
}
22+
23+
export async function getOrdersForReview(): Promise<Order[]> {
24+
const response = await api.get<Order[]>('/merch/orders/for-review');
25+
return response.data;
26+
}
27+
28+
export async function updateOrderState(
29+
orderId: number,
30+
state: MerchOrderState
31+
): Promise<void> {
32+
await api.patch(`/merch/orders/${orderId}/state`, { state });
33+
}
34+
35+
export async function getOrdersNeedingAttention(): Promise<Order[]> {
36+
const response = await api.get<Order[]>('/merch/orders/needs-attention');
37+
return response.data;
38+
}

0 commit comments

Comments
 (0)