Dashboard: accounts list in name order, counting the numbers in names

The accounts table kept the broker's order until the Account header was
clicked, and that sort compared names as plain text, so XXX10 came before
XXX2. It now opens sorted by name, comparing numbers as numbers (XXX1,
XXX2 … XXX10; Vest's padded -02 between -1 and -10), and the other
columns break ties by name so equal rows hold still between syncs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-30 16:55:36 -05:00
co-authored by Claude Opus 5.5
parent 8c863bdc1d
commit 618976bf5f
+19 -11
View File
@@ -21,6 +21,11 @@ function statusRank(account: AccountState): number {
return 3; // In Trade return 3; // In Trade
} }
/** Account names in counting order: XXX2 before XXX10, not after it. */
function byName(a: AccountState, b: AccountState): number {
return a.name.localeCompare(b.name, undefined, { numeric: true });
}
function fmt(value: number) { function fmt(value: number) {
return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
} }
@@ -98,7 +103,7 @@ function maskName(name: string, privacy: boolean): string {
function SortHeader({ label, col, sortKey, sortDir, onSort }: { function SortHeader({ label, col, sortKey, sortDir, onSort }: {
label: string; label: string;
col: SortKey; col: SortKey;
sortKey: SortKey | null; sortKey: SortKey;
sortDir: SortDir; sortDir: SortDir;
onSort: (col: SortKey) => void; onSort: (col: SortKey) => void;
}) { }) {
@@ -527,7 +532,7 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
firm: FirmConfig; firm: FirmConfig;
hideDead: boolean; hideDead: boolean;
privacy: boolean; privacy: boolean;
sortKey: SortKey | null; sortKey: SortKey;
sortDir: SortDir; sortDir: SortDir;
tradeSymbol: string; tradeSymbol: string;
onChanged: () => void; onChanged: () => void;
@@ -554,20 +559,22 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
} }
} }
const sortedAccounts = sortKey === null ? state.accounts : [...state.accounts].sort((a, b) => { // Ties on any other column fall back to name order, so equal rows don't
// shuffle with the broker's list between syncs.
const sortedAccounts = [...state.accounts].sort((a, b) => {
const dir = sortDir === 'asc' ? 1 : -1; const dir = sortDir === 'asc' ? 1 : -1;
switch (sortKey) { switch (sortKey) {
case 'name': return dir * a.name.localeCompare(b.name); case 'name': return dir * byName(a, b);
case 'balance': return dir * (a.amount - b.amount); case 'balance': return dir * (a.amount - b.amount) || byName(a, b);
case 'dayPnL': return dir * (a.realizedPnL - b.realizedPnL); case 'dayPnL': return dir * (a.realizedPnL - b.realizedPnL) || byName(a, b);
case 'daysTraded': return dir * (a.daysTraded - b.daysTraded); case 'daysTraded': return dir * (a.daysTraded - b.daysTraded) || byName(a, b);
case 'target': { case 'target': {
const at = a.effectiveProfitTarget ?? a.accountType?.profitTarget ?? 0; const at = a.effectiveProfitTarget ?? a.accountType?.profitTarget ?? 0;
const bt = b.effectiveProfitTarget ?? b.accountType?.profitTarget ?? 0; const bt = b.effectiveProfitTarget ?? b.accountType?.profitTarget ?? 0;
return dir * (at - bt); return dir * (at - bt) || byName(a, b);
} }
case 'status': return dir * (statusRank(a) - statusRank(b)); case 'status': return dir * (statusRank(a) - statusRank(b)) || byName(a, b);
default: return 0; default: return byName(a, b);
} }
}); });
@@ -732,7 +739,8 @@ export default function Home() {
const [catalog, setCatalog] = useState<{ lastSyncAt: string | null; lastError: string | null; masterUrl: string | null } | null>(null); const [catalog, setCatalog] = useState<{ lastSyncAt: string | null; lastError: string | null; masterUrl: string | null } | null>(null);
const [hideDead, setHideDead] = useState(false); const [hideDead, setHideDead] = useState(false);
const [privacy, setPrivacy] = useState(false); const [privacy, setPrivacy] = useState(false);
const [sortKey, setSortKey] = useState<SortKey | null>(null); // Name order until a header is clicked, so XXX1 always sits above XXX2.
const [sortKey, setSortKey] = useState<SortKey>('name');
const [sortDir, setSortDir] = useState<SortDir>('asc'); const [sortDir, setSortDir] = useState<SortDir>('asc');
// Trade controls // Trade controls