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<TData extends RowData, TValue> {
+        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 (
+        <div className={`w-full py-1 px-1 text-white rounded ${bgClass}`}
+            style={bgStyle}
+        >
+            <h2 className="text-xs font-semibold flex justify-between items-center">
+                <span className="text-left">User Wallet:</span>
+
+                {amount >= 0 ? (
+                    <span className='text-emerald-50'>
+                        {`${formatCentsToUSD(Math.abs(amount))}`}
+                    </span>
+                ) : (
+                    <span className='text-rose-50'>
+                        {`(${formatCentsToUSD(Math.abs(amount))})`}
+                    </span>
+                )}
+
+            </h2>
+        </div>
+    )
+};
+
+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 (
+        <div className="grid grid-cols-[auto_1fr] text-xs">
+            <div className="label text-left">Completes:</div>
+            <div className="value text-right">{survey_completes_f} surveys</div>
+
+            <div className="label text-left">Avg Payouts:</div>
+            <div className="value text-right">{formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)}</div>
+
+            <div className="label text-left">Survey Adjustments:</div>
+            <div className="value text-right">{survey_adjustments_f}</div>
+
+            <div className="label text-left">Adjustment Amount:</div>
+            <div className="value text-right">
+                {adj_total >= 0 ? (
+                    <span className='text-emerald-300'>
+                        {`${formatCentsToUSD(adj_total)}`}
+                    </span>
+                ) : (
+                    <span className='text-rose-300'>
+                        {`(${formatCentsToUSD(Math.abs(adj_total))})`}
+                    </span>
+                )}
+            </div>
+
+            <div className="label text-left">User Payments:</div>
+            <div className="value text-right">
+                {user_payments_total >= 0 ? (
+                    <span className='text-emerald-300'>
+                        {`${formatCentsToUSD(user_payments_total)}`}
+                    </span>
+                ) : (
+                    <span className='text-rose-300'>
+                        {`(${formatCentsToUSD(Math.abs(user_payments_total))})`}
+                    </span>
+                )}
+
+            </div>
+        </div >
+    )
+};
+
+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 (
+        <div className="w-full bg-blue-600 text-white rounded text-left">
+            <div className="hover:cursor-pointer hover:bg-blue-700 transition-colors duration-200"
+                onClick={() => setShowTx(!showTx)}>
+                    
+                <h2 className="text-xs font-semibold rounded flex justify-between
+            py-1 px-1"
+                    onClick={() => setShowTx(!showTx)}
+                >
+                    <span>Total Transactions:</span> <span>{txt_cnt_f}</span>
+                </h2>
+
+                <h4 className="text-[10px] text-center italic text-blue-300 py-1"
+                    onClick={() => setShowTx(!showTx)}
+                >
+                    (Click to {showTx ? 'hide' : 'show'})
+                </h4>
+
+            </div>
+
+            {showTx && (
+                <>
+                    <WalletTransactions />
+                </>
+            )}
+
+        </div>
+    )
+};
+
+
+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<UserLedgerTransactionsResponseTransactionsInner>()
+    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 (
+                    <span className={colorClass}>
+                        {`${emoji} ${formatCentsToUSD(Math.abs(val))}`}
+                    </span>
+                )
+            },
+            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 ? (
+                            <span className={colorClass}>
+                                {`${formatCentsToUSD(Math.abs(val))}`}
+                            </span>
+                        ) : (
+                            <span className={colorClass}>
+                                {`(${formatCentsToUSD(Math.abs(val))})`}
+                            </span>
+                        )}
+                    </>
+                )
+            },
+            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 (
+        <div className="w-full border-t-1 border-blue-800 p-2">
+            <table className="text-xs table-fixed border-collapse">
+                <thead className="font-bold">
+                    {table.getHeaderGroups().map((headerGroup) => (
+
+                        <tr key={headerGroup.id}>
+                            {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 (
+                                    <th
+                                        key={header.id}
+                                        className={`p-0 m-0 ${alignClass}`}
+                                        style={{
+                                            width: `${header.column.getSize()}px`,
+                                        }}
+                                    >
+                                        {flexRender(
+                                            header.column.columnDef.header,
+                                            header.getContext(),
+                                        )}
+                                    </th>
+                                )
+                            })}
+                        </tr>
+                    ))}
+                </thead>
+                <tbody>
+                    {table.getRowModel().rows.map((row) => (
+                        <tr key={row.id}>
+                            {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 (
+                                    <td key={cell.id}
+                                        className={`p-0 ${alignClass}`}
+                                        style={{
+                                            width: `${cell.column.getSize()}px`,
+                                        }}
+                                    >
+                                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                                    </td>
+                                )
+                            })}
+                        </tr>
+                    ))}
+                </tbody>
+
+                <tfoot>
+                    <tr>
+                        <td colSpan={columns.length} className="p-2">
+                            <div className="flex items-center justify-between text-xs">
+
+                                {/* Page info */}
+                                <div className="text-blue-50">
+                                    Page {table.getState().pagination.pageIndex + 1} of{' '}
+                                    {table.getPageCount()}
+                                </div>
+
+                                {/* Navigation buttons */}
+                                <div className="flex gap-8 text-xs">
+                                    <button
+                                        onClick={() => table.previousPage()}
+                                        disabled={!table.getCanPreviousPage()}
+                                        className="px-3 py-1 border border-blue-200 rounded
+                                        font-variant-emoji-text
+                                        hover:cursor-pointer hover:bg-blue-700
+                                        disabled:opacity-50 disabled:cursor-not-allowed"
+                                    >
+                                        {'⬅\uFE0E'}
+                                    </button>
+                                    <button
+                                        onClick={() => table.nextPage()}
+                                        disabled={!table.getCanNextPage()}
+                                        className="px-3 py-1 border border-blue-200 rounded
+                                        font-variant-emoji-text
+                                        hover:cursor-pointer hover:bg-blue-700
+                                        disabled:opacity-50 disabled:cursor-not-allowed"
+                                    >
+                                        {'⮕\uFE0E'}
+                                    </button>
+                                </div>
+
+                                {/* Page size selector */}
+                                <select
+                                    value={table.getState().pagination.pageSize}
+                                    onChange={e => table.setPageSize(Number(e.target.value))}
+                                    className="px-2 py-1 border border-blue-200 rounded
+                                    hover:cursor-pointer hover:bg-blue-700"
+                                >
+                                    {[10, 20, 30, 40].map(pageSize => (
+                                        <option key={pageSize} value={pageSize}>
+                                            Show {pageSize}
+                                        </option>
+                                    ))}
+                                </select>
+                            </div>
+                        </td>
+                    </tr>
+                </tfoot>
+
+            </table>
+        </div>
+    )
+};
+
+
+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 (
+        <div className="fixed top-2 left-2 p-2 rounded
+            bg-blue-500 text-white shadow-lg z-50
+            font-mono
+            max-h-[calc(100vh-2.5rem)] overflow-y-auto
+            max-w-[96vw]
+            ">
+            <WalletHeader />
+            <WalletSummary />
+            <WalletTransactionsHeader />
+        </div>
+    )
+};
+
+
+export default Wallet;
\ No newline at end of file
-- 
cgit v1.2.3

