From 3eaa56f0306ead818f64c3d99fc6d230d9b970a4 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Wed, 18 Feb 2026 20:42:03 -0500 Subject: HERE WE GO, HERE WE GO, HERE WE GO --- jb-ui/src/components/Wallet.tsx | 464 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 jb-ui/src/components/Wallet.tsx (limited to 'jb-ui/src/components/Wallet.tsx') diff --git a/jb-ui/src/components/Wallet.tsx b/jb-ui/src/components/Wallet.tsx new file mode 100644 index 0000000..e4fc694 --- /dev/null +++ b/jb-ui/src/components/Wallet.tsx @@ -0,0 +1,464 @@ +import { + UserLedgerTransactionsResponse, + UserLedgerTransactionsResponseTransactionsInner, + WalletApi +} from "@/api_fsb"; +import { useAppDispatch, useAppSelector } from "@/hooks"; +import { bpid, formatCentsToUSD, } from "@/lib/utils"; +import { + setTxPagination, setTxTotalItems, setTxTotalPages, + setUserLedgerSummary, + setUserLedgerTxs +} from "@/models/appSlice"; +import { + RowData, createColumnHelper, flexRender, getCoreRowModel, + useReactTable +} from "@tanstack/react-table"; +import moment from "moment"; +import { useEffect, useState } from "react"; + +declare module '@tanstack/react-table' { + interface ColumnMeta { + align?: 'left' | 'center' | 'right'; + } +} + +// Function to calculate background color based on balance +const getBackgroundColor = (balance: number | undefined): string => { + if (balance === undefined || balance === null) return 'bg-blue-600'; + + if (balance > 0) { + return 'bg-blue-600'; + } else if (balance <= -100) { + return 'bg-red-600'; + } else { + // Interpolate between blue and red for values between 0 and -100 + // progress goes from 0 (at balance = 0) to 1 (at balance = -100) + const progress = Math.abs(balance) / 100; + + // Blue RGB: (37, 99, 235) - Tailwind blue-600 + // Red RGB: (220, 38, 38) - Tailwind red-600 + const r = Math.round(37 + (220 - 37) * progress); + const g = Math.round(99 + (38 - 99) * progress); + const b = Math.round(235 + (38 - 235) * progress); + + return `rgb(${r}, ${g}, ${b})`; + } +}; + + +const WalletHeader = () => { + const amount = useAppSelector(state => state.app.userWalletBalance)?.amount ?? 0 + + const bgStyle = typeof getBackgroundColor(amount) === 'string' && + getBackgroundColor(amount).startsWith('bg-') + ? {} + : { backgroundColor: getBackgroundColor(amount) }; + + const bgClass = typeof getBackgroundColor(amount) === 'string' && + getBackgroundColor(amount).startsWith('bg-') + ? getBackgroundColor(amount) + : ''; + + return ( +
+

+ User Wallet: + + {amount >= 0 ? ( + + {`${formatCentsToUSD(Math.abs(amount))}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(amount))})`} + + )} + +

+
+ ) +}; + +const WalletSummary = () => { + const summary = useAppSelector(state => state.app.userLedgerSummary) + + const survey_completes = summary?.bp_payment?.entry_count ?? 0 + const survey_completes_f = (survey_completes ?? 0).toLocaleString('en-US') + + const survey_adjustments = summary?.bp_adjustment?.entry_count ?? 0 + const survey_adjustments_f = (survey_adjustments ?? 0).toLocaleString('en-US') + + const min_payout: number = summary?.bp_payment?.min_amount ?? 0 + const max_payout: number = summary?.bp_payment?.max_amount ?? 0 + const adj_total: number = summary?.bp_adjustment?.total_amount ?? 0 + const user_payments_total: number = (summary?.user_payout_request?.total_amount ?? 0) * -1 + + return ( +
+
Completes:
+
{survey_completes_f} surveys
+ +
Avg Payouts:
+
{formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)}
+ +
Survey Adjustments:
+
{survey_adjustments_f}
+ +
Adjustment Amount:
+
+ {adj_total >= 0 ? ( + + {`${formatCentsToUSD(adj_total)}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(adj_total))})`} + + )} +
+ +
User Payments:
+
+ {user_payments_total >= 0 ? ( + + {`${formatCentsToUSD(user_payments_total)}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(user_payments_total))})`} + + )} + +
+
+ ) +}; + +const WalletTransactionsHeader = () => { + const tx_cnt = useAppSelector(state => state.app.txTotalItems) + const txt_cnt_f = (tx_cnt ?? 0).toLocaleString('en-US') + + const [showTx, setShowTx] = useState(false); + + return ( +
+
setShowTx(!showTx)}> + +

setShowTx(!showTx)} + > + Total Transactions: {txt_cnt_f} +

+ +

setShowTx(!showTx)} + > + (Click to {showTx ? 'hide' : 'show'}) +

+ +
+ + {showTx && ( + <> + + + )} + +
+ ) +}; + + +const WalletTransactions = () => { + const dispatch = useAppDispatch() + const bpuid = useAppSelector(state => state.app.bpuid) + const userLedgerTxs = useAppSelector(state => state.app.userLedgerTxs); + + const txPagination = useAppSelector(state => state.app.txPagination); + const txTotalPages = useAppSelector(state => state.app.txTotalPages); + + useEffect(() => { + if (!bpuid) return; + + new WalletApi().getUserTransactionHistoryProductIdTransactionHistoryGet( + bpid, // productId + bpuid, // bpuid + undefined, // createdAfter + undefined, // createdBefore + "-created", // orderBy + txPagination.pageIndex + 1, // page + txPagination.pageSize // pageSize + ).then(res => { + const response = res.data as UserLedgerTransactionsResponse; + console.log("Wallet: fetched user ledger", response); + dispatch(setUserLedgerSummary(response.summary)); + + dispatch(setUserLedgerTxs(response.transactions ?? [])) + dispatch(setTxTotalItems(response.total!)) + dispatch(setTxTotalPages(response.pages!)) + }); + + }, [bpuid, txPagination.pageIndex, txPagination.pageSize]); + + const columnHelper = createColumnHelper() + const columns = [ + columnHelper.accessor('created', { + header: () => 'Date', + cell: (info) => moment(info.getValue()).format('M/D/YY H:mm'), + size: 110, + meta: { + align: 'left' + } + }), + columnHelper.accessor('description', { + header: () => 'Type', + cell: props => { + const desc = props.getValue(); + switch (desc) { + case 'Task Complete': + return 'Survey 🎉'; + case 'Task Adjustment': + return 'Reject ⚠️'; + case 'Compensation Bonus': + return 'Bonus 🎁'; + case 'HIT Bonus': + return 'Bonus'; + case 'HIT Reward': + return 'Assignment'; + default: + return desc; + } + }, + size: 80, + meta: { + align: 'left' + } + }), + columnHelper.accessor('amount', { + header: () => 'Amount', + cell: (props) => { + const val = props.renderValue() as number; + const isPositive = val >= 0; + const emoji = isPositive ? "⬆\uFE0E" : "⬇\uFE0E"; + const colorClass = isPositive ? 'font-variant-emoji-text text-emerald-300' : 'font-variant-emoji-text text-rose-300'; + + return ( + + {`${emoji} ${formatCentsToUSD(Math.abs(val))}`} + + ) + }, + size: 70, + meta: { + align: 'center' + } + }), + columnHelper.accessor('balance_after', { + header: () => 'Balance', + cell: (props) => { + const val = props.renderValue() as number; + const isPositive = val >= 0; + const colorClass = isPositive ? 'text-emerald-300' : 'text-rose-300'; + + return ( + <> + {isPositive ? ( + + {`${formatCentsToUSD(Math.abs(val))}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(val))})`} + + )} + + ) + }, + size: 70, + meta: { + align: 'center' + } + }), + ] + + const table = useReactTable({ + 'data': userLedgerTxs, + 'columns': columns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: txTotalPages, + onPaginationChange: (updater) => { + dispatch(setTxPagination( + typeof updater === 'function' ? updater(txPagination) : updater + )); + }, + state: { + pagination: txPagination, + }, + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + + {headerGroup.headers.map((header) => { + const align = header.column.columnDef.meta?.align || 'left'; + const alignClass = align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left'; + + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const align = cell.column.columnDef.meta?.align || 'left'; + const alignClass = align === 'center' ? 'text-center' : + align === 'right' ? 'text-right' : 'text-left'; + + return ( + + ) + })} + + ))} + + + + + + + + +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ + {/* Page info */} +
+ Page {table.getState().pagination.pageIndex + 1} of{' '} + {table.getPageCount()} +
+ + {/* Navigation buttons */} +
+ + +
+ + {/* Page size selector */} + +
+
+
+ ) +}; + + +const Wallet = () => { + const dispatch = useAppDispatch() + const bpuid = useAppSelector(state => state.app.bpuid) + const pagination = useAppSelector(state => state.app.txPagination); + + useEffect(() => { + if (!bpuid) return; + + new WalletApi().getUserTransactionHistoryProductIdTransactionHistoryGet( + bpid, // productId + bpuid, // bpuid + undefined, // createdAfter + undefined, // createdBefore + "-created", // orderBy + pagination.pageIndex + 1, // page + pagination.pageSize // pageSize + ).then(res => { + const response = res.data as UserLedgerTransactionsResponse; + dispatch(setUserLedgerSummary(response.summary)); + dispatch(setUserLedgerTxs(response.transactions ?? [])) + + dispatch(setTxPagination({ + pageIndex: (response.page ?? 1) - 1, + pageSize: response.size ?? 10, + })) + dispatch(setTxTotalItems(response.total ?? 0)) + dispatch(setTxTotalPages(response.pages ?? 9)) + }); + + }, [bpuid]) + + if (!bpuid) return null; + + return ( +
+ + + +
+ ) +}; + + +export default Wallet; \ No newline at end of file -- cgit v1.2.3