|
| 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; |
0 commit comments