Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { PortfolioPage } from './pages/PortfolioPage.jsx'
import { RegisterPage } from './pages/RegisterPage.jsx'
import { StatementDetailPage } from './pages/StatementDetailPage.jsx'
import { StatementsPage } from './pages/StatementsPage.jsx'
import { TransactionsPage } from './pages/TransactionsPage.jsx'
import { defaultRouteForPrincipal } from './routes/defaultRoute.js'
import { RequireAuth, RequireOnboarded, RequireRole } from './routes/guards.jsx'

Expand Down Expand Up @@ -69,7 +68,6 @@ function AppShell() {
<Route path="/orders" element={<OrdersPage />} />
<Route path="/orders/new" element={<OrderNewPage />} />
<Route path="/orders/:orderId" element={<OrderDetailPage />} />
<Route path="/transactions" element={<TransactionsPage />} />
<Route path="/funding" element={<FundingPage />} />
<Route path="/lots" element={<LotsPage />} />
<Route path="/statements" element={<StatementsPage />} />
Expand Down
47 changes: 42 additions & 5 deletions frontend/src/features/onboarding/hooks/useIdentityStatus.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { identityApi } from '../api/identityApi.js'

const IDLE = { status: 'idle', kycStatus: null, accountApprovalStatus: null, error: null }

// A still-pending refresh triggers a live outbound Stripe Identity poll (identity.py's
// get_identity_status), not a cheap read, and shares a single 30/min-per-IP bucket with
// RequireOnboarded's own background calls on every protected-route navigation -- a customer
// repeatedly clicking "Refresh status" could burn through that shared budget for the whole app.
const REFETCH_COOLDOWN_MS = 10_000

// GET /identity/status/<customer_id>; pass null for non-customer principals.
export function useIdentityStatus(customerId) {
const [state, setState] = useState(IDLE)
const [cooldownRemainingMs, setCooldownRemainingMs] = useState(0)
const lastFetchedAtRef = useRef(0)

const refetch = useCallback(async () => {
const fetchNow = useCallback(async () => {
if (!customerId) {
setState(IDLE)
return
}
lastFetchedAtRef.current = Date.now()
setState((prev) => ({ ...prev, status: 'loading', error: null }))
try {
const data = await identityApi.getStatus(customerId)
Expand All @@ -26,9 +35,37 @@ export function useIdentityStatus(customerId) {
}
}, [customerId])

// The initial mount fetch below always bypasses this -- there is nothing to cool down from yet.
// Every other caller (the "Refresh status" button, KycStep's onCompleted, ErrorState's onRetry)
// goes through this throttled path, since a spammed retry deserves the same protection as a
// spammed manual refresh.
const refetch = useCallback(() => {
if (Date.now() - lastFetchedAtRef.current < REFETCH_COOLDOWN_MS) {
return
}
fetchNow()
}, [fetchNow])

useEffect(() => {
refetch()
}, [refetch])
fetchNow()
}, [fetchNow])

// Ticks the remaining cooldown down to 0 so the UI can show "Try again in Ns" and re-enable
// itself without polling Date.now() from the caller.
useEffect(() => {
if (state.status !== 'loaded' && state.status !== 'error') return
const update = () => {
setCooldownRemainingMs(Math.max(0, REFETCH_COOLDOWN_MS - (Date.now() - lastFetchedAtRef.current)))
}
update()
const interval = setInterval(update, 250)
return () => clearInterval(interval)
}, [state.status])

return { ...state, refetch }
return {
...state,
refetch,
canRefetch: cooldownRemainingMs === 0,
cooldownRemainingSeconds: Math.ceil(cooldownRemainingMs / 1000),
}
}
1 change: 0 additions & 1 deletion frontend/src/features/valuation/api/valuationApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ export const valuationApi = {
getBalance: () => apiClient.get('/valuation/balance'),
getReturns: (periodStart, periodEnd) =>
apiClient.get(`/valuation/returns?period_start=${periodStart}&period_end=${periodEnd}`),
getHistory: () => apiClient.get('/valuation/history'),
}
59 changes: 0 additions & 59 deletions frontend/src/features/valuation/components/TransactionTable.jsx

This file was deleted.

12 changes: 0 additions & 12 deletions frontend/src/features/valuation/entryTypeLabels.js

This file was deleted.

25 changes: 0 additions & 25 deletions frontend/src/features/valuation/hooks/useTransactionHistory.js

This file was deleted.

3 changes: 1 addition & 2 deletions frontend/src/pages/AppLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const CUSTOMER_NAV_GROUPS = [
key: 'money',
label: 'Money',
links: [
{ to: '/transactions', label: 'Transactions' },
{ to: '/funding', label: 'Funding' },
{ to: '/lots', label: 'Tax lots' },
{ to: '/statements', label: 'Statements' },
Expand Down Expand Up @@ -296,7 +295,7 @@ export function AppLayout() {
{/* Group labels show inline only for the staff console -- it's what makes 2
links read as a deliberate, named cluster rather than a leftover flat row.
Customer groups lean on spacing + dividers alone; "Dashboard/Portfolio/Orders"
vs. "Transactions/Funding/..." is self-evidently grouped without a caption. */}
vs. "Funding/Tax lots/..." is self-evidently grouped without a caption. */}
{group.label && variant === 'staff' && (
<span className="tu-app-layout__nav-group-label">{group.label}</span>
)}
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/pages/DashboardPage.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* BalanceCard/ReturnCard are content-sized Cards with no width of their own -- now that
PageLayout.css's .tu-page actually stretches to the full page width, a plain flex row would
just leave them small and left-aligned with the rest of the row empty. auto-fit lets each stat
grow to share the row evenly (2 columns once there's room for both at >=280px, 1 below that),
matching the repeat(auto-fill/auto-fit, minmax(...)) idiom already used in
features/portfolio/components/ModelDetail.css and AssignmentPrompt.css. */
.tu-dashboard-page__stats {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-4);
}
8 changes: 0 additions & 8 deletions frontend/src/pages/FundingPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@
}
}

/* Matches LotsPage.css's own fix: FundingHistoryTable is wide enough to need its own horizontal
scroll (Table.css), which only works if this page's width is pinned to the viewport rather than
growing to the table's unwrapped content width via PageLayout.css's auto-margin centering. */
.tu-page.tu-funding-page {
width: 100%;
min-width: 0;
}

/* This heading is only ever reached via OutstandingBalanceBanner's onMakeDeposit ->
focusDepositForm()'s programmatic .focus() call (tabIndex=-1 takes it out of the natural tab
order) — matching FeesPage.css's focusPaymentMethod() precedent, :focus (not :focus-visible) is
Expand Down
17 changes: 0 additions & 17 deletions frontend/src/pages/LotsPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,3 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

/* `.tu-page`'s own `margin: 0 auto` (PageLayout.css, centering it under `max-width`) is a
cross-axis auto margin on a flex item of `.tu-app-layout__main` (a column flex container) —
per the flexbox spec, a cross-axis auto margin opts the item OUT of stretch sizing entirely, so
`.tu-page` falls back to its own content-based width instead of the container's. With a wide
table inside, that content-based width is the table's full unwrapped min-content size, so the
whole page (not just the table) ends up wider than the viewport and the table's own
`.tu-table-wrapper` (Table.css's overflow-x: auto) never gets a narrow-enough box to actually
scroll inside. An explicit `width: 100%` gives the auto-margin centering something definite to
center within again; `min-width: 0` then lets that 100% actually shrink to the viewport instead
of being floored by the table's content size. Scoped to this page (the extra `tu-lots-page`
class) rather than touched at the shared PageLayout.css/Table.css source — every other `.tu-page`
user either has no table this wide or hasn't hit this at a narrow viewport yet. */
.tu-page.tu-lots-page {
width: 100%;
min-width: 0;
}
10 changes: 8 additions & 2 deletions frontend/src/pages/OnboardingPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ export function OnboardingPage() {
<div className="tu-onboarding-page__header">
<h1 className="tu-onboarding-page__title">Identity and funding status</h1>
{identity.status === 'loaded' && (
<Button variant="secondary" size="compact" onClick={identity.refetch}>
Refresh status
<Button
variant="secondary"
size="compact"
onClick={identity.refetch}
disabled={!identity.canRefetch}
title={identity.canRefetch ? undefined : `Try again in ${identity.cooldownRemainingSeconds}s`}
>
{identity.canRefetch ? 'Refresh status' : `Try again in ${identity.cooldownRemainingSeconds}s`}
</Button>
)}
</div>
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/pages/PageLayout.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
/* `.tu-page` is a flex item of `.tu-app-layout__main` (a column flex container). Its own
`margin: 0 auto` centering is a cross-axis auto margin -- per the flexbox spec, that opts the
item OUT of stretch sizing entirely, so without `width: 100%` it shrinks to its own content's
width instead of the container's. A page whose content happens to be a wide table or a filled
multi-column form never shows this (the content forces it back open), which is why it went
unnoticed until Lots/Funding/Orders/Portfolio/Dashboard were checked directly: confirmed live at
1280px, Orders list rendered at 293px, Portfolio at 307px, Dashboard at 510px. `min-width: 0`
then lets that `width: 100%` actually shrink on a narrow viewport instead of being floored by
any wide-content descendant (e.g. a table) the way `min-width: auto`'s default would. */
.tu-page {
max-width: var(--content-max-width);
width: 100%;
min-width: 0;
margin: 0 auto;
padding: var(--space-8) var(--space-6);
display: flex;
Expand Down
11 changes: 0 additions & 11 deletions frontend/src/pages/TransactionsPage.jsx

This file was deleted.

Loading