Skip to content

Commit 1bb8223

Browse files
6200 add order history page to the frontend (#28)
* hold * hold * (basic) working order history page * working basic * Multiple filters updates URL * sorting works in the url * format * lint * fmt * fix import * fix * fix * fmt * type * type * type * type * type * fmt * type * fmt * type * type * type * remove unused import * type * type * fix * fix * fix * fix * fix * revert * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * working, types not * Add row types * upgrade * Finally passing typing! * fmt * Update hooks.ts * working! * lint --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent b52068f commit 1bb8223

File tree

6 files changed

+344
-2
lines changed

6 files changed

+344
-2
lines changed

env/env.defaults

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66

77
# Bae URL of the Unified Ecommerce API Server
8-
NEXT_PUBLIC_UE_API_BASE_URL=http://ue.odl.local:9080
8+
NEXT_PUBLIC_UE_API_BASE_URL=http://host.docker.internal:9080

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@mui/material-nextjs": "^6.1.8",
2626
"@remixicon/react": "^4.2.0",
2727
"@tanstack/react-query": "^5.61.3",
28+
"@tanstack/react-table": "^8.20.6",
2829
"axios": "^1.7.7",
2930
"next": "15.1.3",
3031
"react": "19.0.0",

src/app/history/page.tsx

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
"use client";
2+
3+
import React, { useMemo, useEffect, useState } from "react";
4+
import {
5+
useReactTable,
6+
getCoreRowModel,
7+
getSortedRowModel,
8+
getFilteredRowModel,
9+
getPaginationRowModel,
10+
flexRender,
11+
Column,
12+
CellContext,
13+
} from "@tanstack/react-table";
14+
import { styled } from "@mitodl/smoot-design";
15+
import { Typography } from "@mui/material";
16+
import { UseQueryResult } from "@tanstack/react-query";
17+
import { useSearchParams, usePathname, useRouter } from "next/navigation";
18+
import { getCurrentSystem, getCurrentStatus } from "@/utils/system";
19+
import type {
20+
PaginatedOrderHistoryList,
21+
OrderHistory,
22+
} from "@/services/ecommerce/generated/v0";
23+
import { usePaymentsOrderHistory } from "@/services/ecommerce/payments/hooks";
24+
import { useMetaIntegratedSystemsList } from "@/services/ecommerce/meta/hooks";
25+
26+
const OrderHistoryContainer = styled.div(() => ({
27+
width: "100%",
28+
padding: "32px",
29+
backgroundColor: "#f9f9f9",
30+
borderRadius: "8px",
31+
}));
32+
33+
const TableContainer = styled.div`
34+
overflow-x: auto;
35+
margin-top: 16px;
36+
`;
37+
38+
const StyledTable = styled.table`
39+
width: 100%;
40+
border-collapse: collapse;
41+
background-color: #fff;
42+
`;
43+
44+
const StyledTh = styled.th`
45+
padding: 12px 16px;
46+
background-color: #f1f1f1;
47+
text-align: left;
48+
font-weight: bold;
49+
border-bottom: 1px solid #ddd;
50+
cursor: pointer;
51+
`;
52+
53+
const StyledTd = styled.td`
54+
padding: 12px 16px;
55+
border-bottom: 1px solid #ddd;
56+
`;
57+
58+
const FilterTd = styled.td`
59+
padding: 8px 16px;
60+
background-color: #f9f9f9;
61+
`;
62+
63+
interface DebouncedInputProps {
64+
value: string;
65+
onChange: (value: string) => void;
66+
debounce?: number;
67+
[key: string]: unknown;
68+
}
69+
70+
const DebouncedInput: React.FC<DebouncedInputProps> = ({
71+
value: initialValue,
72+
onChange,
73+
debounce = 500,
74+
...props
75+
}) => {
76+
const [value, setValue] = useState(initialValue);
77+
78+
useEffect(() => {
79+
setValue(initialValue);
80+
}, [initialValue]);
81+
82+
useEffect(() => {
83+
const timeout = setTimeout(() => {
84+
onChange(value);
85+
}, debounce);
86+
return () => clearTimeout(timeout);
87+
}, [debounce, onChange, value]);
88+
89+
return (
90+
<input
91+
{...props}
92+
value={value}
93+
onChange={(e) => setValue(e.target.value)}
94+
/>
95+
);
96+
};
97+
98+
interface FilterProps<TData> {
99+
column: Column<TData, unknown>;
100+
}
101+
102+
const Filter = <TData,>({ column }: FilterProps<TData>) => {
103+
const columnFilterValue = column.getFilterValue() as string | undefined;
104+
return (
105+
<DebouncedInput
106+
type="text"
107+
value={columnFilterValue ?? ""}
108+
onChange={(value) => column.setFilterValue(value)}
109+
placeholder={"Search..."}
110+
className="w-36 border shadow rounded"
111+
/>
112+
);
113+
};
114+
115+
const OrderHistory: React.FC = () => {
116+
const history =
117+
usePaymentsOrderHistory() as UseQueryResult<PaginatedOrderHistoryList>;
118+
const integratedSystemList = useMetaIntegratedSystemsList();
119+
const searchParams = useSearchParams();
120+
const router = useRouter();
121+
const pathName = usePathname();
122+
123+
const data = useMemo(() => {
124+
if (!history.data) return [];
125+
const specifiedSystem = getCurrentSystem(searchParams);
126+
const specifiedStatus = getCurrentStatus(searchParams);
127+
return history.data.results.filter((row) => {
128+
const system = String(row.lines[0]?.product.system);
129+
const status = row.state;
130+
return (
131+
(specifiedSystem ? system === specifiedSystem : true) &&
132+
(specifiedStatus ? status === specifiedStatus : true)
133+
);
134+
});
135+
}, [history.data, searchParams]);
136+
137+
const columns = useMemo(
138+
() => [
139+
{
140+
header: "Status",
141+
accessorKey: "state",
142+
enableSorting: true,
143+
enableFiltering: true,
144+
cell: (info: CellContext<OrderHistory, unknown>) => info.getValue(),
145+
},
146+
{
147+
header: "Reference Number",
148+
accessorKey: "reference_number",
149+
enableSorting: true,
150+
enableFiltering: true,
151+
},
152+
{
153+
header: "Number of Products",
154+
accessorFn: (row: OrderHistory) => row.lines.length,
155+
},
156+
{
157+
header: "System",
158+
accessorFn: (row: OrderHistory) => {
159+
const systemId = row.lines[0]?.product.system;
160+
const system = integratedSystemList.data?.results.find(
161+
(sys) => sys.id === systemId,
162+
);
163+
return system ? system.name : "N/A";
164+
},
165+
enableFiltering: true,
166+
},
167+
{
168+
header: "Total Price Paid",
169+
accessorFn: (row: OrderHistory) =>
170+
Number(row.total_price_paid).toFixed(2),
171+
},
172+
{
173+
header: "Created On",
174+
accessorFn: (row: OrderHistory) =>
175+
new Date(row.created_on).toLocaleString(),
176+
enableSorting: true,
177+
},
178+
],
179+
[integratedSystemList.data],
180+
);
181+
182+
const initialSorting = useMemo(() => {
183+
const sorting: { id: string; desc: boolean }[] = [];
184+
searchParams.forEach((value, key) => {
185+
if (key.startsWith("sort_")) {
186+
sorting.push({
187+
id: key.replace("sort_", ""),
188+
desc: value === "desc",
189+
});
190+
}
191+
});
192+
return sorting;
193+
}, [searchParams]);
194+
195+
const initialFilters = useMemo(() => {
196+
const filters: { id: string; value: string }[] = [];
197+
searchParams.forEach((value, key) => {
198+
if (key.startsWith("filter_")) {
199+
filters.push({
200+
id: key.replace("filter_", ""),
201+
value: String(value),
202+
});
203+
}
204+
});
205+
return filters;
206+
}, [searchParams]);
207+
208+
const table = useReactTable({
209+
data,
210+
columns,
211+
initialState: {
212+
sorting: initialSorting,
213+
columnFilters: initialFilters,
214+
},
215+
getCoreRowModel: getCoreRowModel(),
216+
getSortedRowModel: getSortedRowModel(),
217+
getFilteredRowModel: getFilteredRowModel(),
218+
getPaginationRowModel: getPaginationRowModel(),
219+
});
220+
221+
const tableSorting = table.getState().sorting;
222+
const tableFiltering = table.getState().columnFilters;
223+
224+
useEffect(() => {
225+
const params = new URLSearchParams();
226+
227+
tableSorting.forEach((sort) => {
228+
params.append(`sort_${sort.id}`, sort.desc ? "desc" : "asc");
229+
});
230+
231+
tableFiltering.forEach((filter) => {
232+
params.append(`filter_${filter.id}`, String(filter.value));
233+
});
234+
235+
const queryString = params.toString();
236+
router.push(`${pathName}?${queryString}`);
237+
}, [pathName, router, table, tableSorting, tableFiltering]);
238+
239+
return (
240+
<OrderHistoryContainer>
241+
<Typography variant="h4">Order History</Typography>
242+
<TableContainer>
243+
<StyledTable>
244+
<thead>
245+
{table.getHeaderGroups().map((headerGroup) => (
246+
<tr key={headerGroup.id}>
247+
{headerGroup.headers.map((header) => (
248+
<StyledTh
249+
key={header.id}
250+
onClick={header.column.getToggleSortingHandler()}
251+
>
252+
{flexRender(
253+
header.column.columnDef.header,
254+
header.getContext(),
255+
)}
256+
{header.column.getIsSorted() === "asc"
257+
? " 🔼"
258+
: header.column.getIsSorted() === "desc"
259+
? " 🔽"
260+
: null}
261+
</StyledTh>
262+
))}
263+
</tr>
264+
))}
265+
<tr>
266+
{table.getHeaderGroups()[0].headers.map((header) => (
267+
<FilterTd key={header.id}>
268+
{header.column.getCanFilter() ? (
269+
<Filter column={header.column} />
270+
) : null}
271+
</FilterTd>
272+
))}
273+
</tr>
274+
</thead>
275+
<tbody>
276+
{table.getRowModel().rows.map((row) => (
277+
<tr key={row.id}>
278+
{row.getVisibleCells().map((cell) => (
279+
<StyledTd key={cell.id}>
280+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
281+
</StyledTd>
282+
))}
283+
</tr>
284+
))}
285+
</tbody>
286+
</StyledTable>
287+
</TableContainer>
288+
</OrderHistoryContainer>
289+
);
290+
};
291+
292+
export default OrderHistory;

src/services/ecommerce/payments/hooks.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,21 @@ const usePaymentsCheckoutStartCheckout = () => {
7575
});
7676
};
7777

78+
const usePaymentsOrderHistory = (opts: ExtraQueryOpts = {}) =>
79+
useQuery({
80+
queryKey: ["paymentsOrders"],
81+
queryFn: async () => {
82+
const response = await paymentsApi.paymentsOrdersHistoryList();
83+
return response.data;
84+
},
85+
...opts,
86+
});
87+
7888
export {
7989
usePaymentsBasketList,
8090
usePaymentsBasketRetrieve,
8191
usePaymentsBaksetCreateFromProduct,
8292
usePaymentsBasketAddDiscount,
8393
usePaymentsCheckoutStartCheckout,
94+
usePaymentsOrderHistory,
8495
};

src/utils/system.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,22 @@ const getCurrentSystem = (urlParams: URLSearchParams) => {
1818
return system;
1919
};
2020

21-
export { getCurrentSystem };
21+
const getCurrentStatus = (urlParams: URLSearchParams) => {
22+
let status: string = "";
23+
24+
if (urlParams.has("fulfilled")) {
25+
status = "fulfilled";
26+
}
27+
28+
if (urlParams.has("pending")) {
29+
status = "pending";
30+
}
31+
32+
if (urlParams.has("status")) {
33+
status = encodeURIComponent(urlParams.get("status") as string);
34+
}
35+
36+
return status;
37+
};
38+
39+
export { getCurrentSystem, getCurrentStatus };

yarn.lock

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,6 +1978,25 @@ __metadata:
19781978
languageName: node
19791979
linkType: hard
19801980

1981+
"@tanstack/react-table@npm:^8.20.6":
1982+
version: 8.20.6
1983+
resolution: "@tanstack/react-table@npm:8.20.6"
1984+
dependencies:
1985+
"@tanstack/table-core": "npm:8.20.5"
1986+
peerDependencies:
1987+
react: ">=16.8"
1988+
react-dom: ">=16.8"
1989+
checksum: 10c0/3213dc146f647fbd571f4e347007b969320819e588439b2ee95dd3a65efcbe30d097c24426dd82617041ed1e186182a5b303382bcebed5d61a1c6045a55c58d3
1990+
languageName: node
1991+
linkType: hard
1992+
1993+
"@tanstack/table-core@npm:8.20.5":
1994+
version: 8.20.5
1995+
resolution: "@tanstack/table-core@npm:8.20.5"
1996+
checksum: 10c0/3c27b5debd61b6bd9bfbb40bfc7c5d5af90873ae1a566b20e3bf2d2f4f2e9a78061c081aacc5259a00e256f8df506ec250eb5472f5c01ff04baf9918b554982b
1997+
languageName: node
1998+
linkType: hard
1999+
19812000
"@testing-library/dom@npm:^10.4.0":
19822001
version: 10.4.0
19832002
resolution: "@testing-library/dom@npm:10.4.0"
@@ -9989,6 +10008,7 @@ __metadata:
998910008
"@swc/core": "npm:^1.9.3"
999010009
"@swc/jest": "npm:^0.2.37"
999110010
"@tanstack/react-query": "npm:^5.61.3"
10011+
"@tanstack/react-table": "npm:^8.20.6"
999210012
"@testing-library/dom": "npm:^10.4.0"
999310013
"@testing-library/jest-dom": "npm:^6.6.3"
999410014
"@testing-library/react": "npm:^16.0.1"

0 commit comments

Comments
 (0)