Remove Investments tab, enhance fund tabs with full investment details and edit

- Removed standalone Investments tab — investment accounts now show only
  under their respective Operating/Reserve tabs with full detail columns
- Investment sub-tables now include: Principal, Current Value, Rate,
  Interest Earned, Maturity Value, Maturity Date, Days Remaining, plus
  summary cards (Total Principal, Total Current Value, Avg Rate)
- Added investment edit capability via modal (name, institution, type,
  fund, principal, current value, rate, purchase/maturity dates, notes)
- Fixed primary account star icon — now shows for all non-system accounts
  (was previously restricted to asset type only), allowing users to set
  default Operating and Reserve accounts regardless of account type
- Fixed Adjust Balance icon to also show for all non-system accounts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 08:53:04 -05:00
parent c68a7e21c3
commit 8ebd324e77

View File

@@ -117,7 +117,9 @@ const fmt = (v: string | number) =>
export function AccountsPage() {
const [opened, { open, close }] = useDisclosure(false);
const [adjustOpened, { open: openAdjust, close: closeAdjust }] = useDisclosure(false);
const [invEditOpened, { open: openInvEdit, close: closeInvEdit }] = useDisclosure(false);
const [editing, setEditing] = useState<Account | null>(null);
const [editingInvestment, setEditingInvestment] = useState<Investment | null>(null);
const [adjustingAccount, setAdjustingAccount] = useState<Account | null>(null);
const [search, setSearch] = useState('');
const [filterType, setFilterType] = useState<string | null>(null);
@@ -277,7 +279,73 @@ export function AccountsPage() {
},
});
// ── Investment edit form ──
const invForm = useForm({
initialValues: {
name: '',
institution: '',
accountNumberLast4: '',
investmentType: 'cd',
fundType: 'operating',
principal: 0,
interestRate: 0,
maturityDate: null as Date | null,
purchaseDate: null as Date | null,
currentValue: 0,
notes: '',
},
validate: {
name: (v) => (v.length > 0 ? null : 'Required'),
principal: (v) => (v > 0 ? null : 'Required'),
},
});
const updateInvestmentMutation = useMutation({
mutationFn: (values: any) =>
api.put(`/investment-accounts/${editingInvestment!.id}`, {
name: values.name,
institution: values.institution || null,
account_number_last4: values.accountNumberLast4 || null,
investment_type: values.investmentType,
fund_type: values.fundType,
principal: values.principal,
interest_rate: values.interestRate || 0,
maturity_date: values.maturityDate ? values.maturityDate.toISOString().split('T')[0] : null,
purchase_date: values.purchaseDate ? values.purchaseDate.toISOString().split('T')[0] : null,
current_value: values.currentValue || values.principal,
notes: values.notes || null,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['investments'] });
notifications.show({ message: 'Investment updated', color: 'green' });
closeInvEdit();
setEditingInvestment(null);
invForm.reset();
},
onError: (err: any) => {
notifications.show({ message: err.response?.data?.message || 'Error updating investment', color: 'red' });
},
});
// ── Handlers ──
const handleEditInvestment = (inv: Investment) => {
setEditingInvestment(inv);
invForm.setValues({
name: inv.name,
institution: inv.institution || '',
accountNumberLast4: inv.account_number_last4 || '',
investmentType: inv.investment_type,
fundType: inv.fund_type,
principal: parseFloat(inv.principal || '0'),
interestRate: parseFloat(inv.interest_rate || '0'),
maturityDate: inv.maturity_date ? new Date(inv.maturity_date) : null,
purchaseDate: inv.purchase_date ? new Date(inv.purchase_date) : null,
currentValue: parseFloat(inv.current_value || inv.principal || '0'),
notes: '',
});
openInvEdit();
};
const handleEdit = (account: Account) => {
setEditing(account);
form.setValues({
@@ -436,7 +504,6 @@ export function AccountsPage() {
<Tabs.Tab value="reserve">
Reserve ({activeAccounts.filter(a => a.fund_type === 'reserve').length + reserveInvestments.length})
</Tabs.Tab>
<Tabs.Tab value="investments">Investments ({investments.filter(i => i.is_active).length})</Tabs.Tab>
{showArchived && archivedAccounts.length > 0 && (
<Tabs.Tab value="archived" color="gray">
Archived ({archivedAccounts.length})
@@ -465,7 +532,7 @@ export function AccountsPage() {
{operatingInvestments.length > 0 && (
<>
<Divider label="Operating Investment Accounts" labelPosition="center" my="xs" />
<InvestmentMiniTable investments={operatingInvestments} />
<InvestmentMiniTable investments={operatingInvestments} onEdit={handleEditInvestment} />
</>
)}
</Stack>
@@ -482,14 +549,11 @@ export function AccountsPage() {
{reserveInvestments.length > 0 && (
<>
<Divider label="Reserve Investment Accounts" labelPosition="center" my="xs" />
<InvestmentMiniTable investments={reserveInvestments} />
<InvestmentMiniTable investments={reserveInvestments} onEdit={handleEditInvestment} />
</>
)}
</Stack>
</Tabs.Panel>
<Tabs.Panel value="investments" pt="sm">
<InvestmentsTab investments={investments} isLoading={investmentsLoading} />
</Tabs.Panel>
{showArchived && (
<Tabs.Panel value="archived" pt="sm">
<AccountTable
@@ -693,6 +757,79 @@ export function AccountsPage() {
</form>
)}
</Modal>
{/* Investment Edit Modal */}
<Modal opened={invEditOpened} onClose={closeInvEdit} title="Edit Investment Account" size="md">
{editingInvestment && (
<form onSubmit={invForm.onSubmit((values) => updateInvestmentMutation.mutate(values))}>
<Stack>
<TextInput label="Investment Name" required {...invForm.getInputProps('name')} />
<TextInput label="Institution" placeholder="e.g. First National Bank" {...invForm.getInputProps('institution')} />
<Select
label="Investment Type"
required
data={[
{ value: 'cd', label: 'CD' },
{ value: 'money_market', label: 'Money Market' },
{ value: 'treasury', label: 'Treasury' },
{ value: 'savings', label: 'Savings' },
{ value: 'other', label: 'Brokerage / Other' },
]}
{...invForm.getInputProps('investmentType')}
/>
<Select
label="Fund Type"
required
data={[
{ value: 'operating', label: 'Operating' },
{ value: 'reserve', label: 'Reserve' },
]}
{...invForm.getInputProps('fundType')}
/>
<Divider label="Financial Details" labelPosition="center" />
<Group grow>
<NumberInput
label="Principal"
required
prefix="$"
decimalScale={2}
thousandSeparator=","
min={0}
{...invForm.getInputProps('principal')}
/>
<NumberInput
label="Current Value"
prefix="$"
decimalScale={2}
thousandSeparator=","
min={0}
{...invForm.getInputProps('currentValue')}
/>
</Group>
<NumberInput
label="Interest Rate (%)"
decimalScale={4}
suffix="%"
min={0}
max={100}
{...invForm.getInputProps('interestRate')}
/>
<Group grow>
<DateInput label="Purchase Date" clearable {...invForm.getInputProps('purchaseDate')} />
<DateInput label="Maturity Date" clearable {...invForm.getInputProps('maturityDate')} />
</Group>
<TextInput
label="Account # (last 4)"
placeholder="1234"
maxLength={4}
{...invForm.getInputProps('accountNumberLast4')}
/>
<Textarea label="Notes" autosize minRows={2} maxRows={4} {...invForm.getInputProps('notes')} />
<Button type="submit" loading={updateInvestmentMutation.isPending}>Update Investment</Button>
</Stack>
</form>
)}
</Modal>
</Stack>
);
}
@@ -776,7 +913,7 @@ function AccountTable({
</Table.Td>
<Table.Td>
<Group gap={4}>
{a.account_type === 'asset' && (
{!a.is_system && (
<Tooltip label={a.is_primary ? 'Primary account' : 'Set as Primary'}>
<ActionIcon
variant="subtle"
@@ -787,7 +924,7 @@ function AccountTable({
</ActionIcon>
</Tooltip>
)}
{a.account_type === 'asset' && (
{!a.is_system && (
<Tooltip label="Adjust Balance">
<ActionIcon variant="subtle" color="blue" onClick={() => onAdjustBalance(a)}>
<IconAdjustments size={16} />
@@ -819,14 +956,14 @@ function AccountTable({
);
}
// ── Investments Tab Component ──
// ── Investment Table for Operating/Reserve tabs ──
function InvestmentsTab({
function InvestmentMiniTable({
investments,
isLoading,
onEdit,
}: {
investments: Investment[];
isLoading: boolean;
onEdit: (inv: Investment) => void;
}) {
const totalPrincipal = investments.reduce((s, i) => s + parseFloat(i.principal || '0'), 0);
const totalValue = investments.reduce(
@@ -838,40 +975,20 @@ function InvestmentsTab({
? investments.reduce((s, i) => s + parseFloat(i.interest_rate || '0'), 0) / investments.length
: 0;
if (isLoading) {
return (
<Center h={200}>
<Loader />
</Center>
);
}
return (
<Stack>
<Stack gap="sm">
<SimpleGrid cols={{ base: 1, sm: 3 }}>
<Card withBorder p="md">
<Text size="xs" c="dimmed">
Total Principal
</Text>
<Text fw={700} size="xl">
{fmt(totalPrincipal)}
</Text>
<Card withBorder p="xs">
<Text size="xs" c="dimmed">Total Principal</Text>
<Text fw={700} size="sm">{fmt(totalPrincipal)}</Text>
</Card>
<Card withBorder p="md">
<Text size="xs" c="dimmed">
Total Current Value
</Text>
<Text fw={700} size="xl" c="green">
{fmt(totalValue)}
</Text>
<Card withBorder p="xs">
<Text size="xs" c="dimmed">Total Current Value</Text>
<Text fw={700} size="sm" c="teal">{fmt(totalValue)}</Text>
</Card>
<Card withBorder p="md">
<Text size="xs" c="dimmed">
Avg Interest Rate
</Text>
<Text fw={700} size="xl">
{avgRate.toFixed(2)}%
</Text>
<Card withBorder p="xs">
<Text size="xs" c="dimmed">Avg Interest Rate</Text>
<Text fw={700} size="sm">{avgRate.toFixed(2)}%</Text>
</Card>
</SimpleGrid>
@@ -881,51 +998,45 @@ function InvestmentsTab({
<Table.Th>Name</Table.Th>
<Table.Th>Institution</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Fund</Table.Th>
<Table.Th ta="right">Principal</Table.Th>
<Table.Th ta="right">Current Value</Table.Th>
<Table.Th ta="right">Rate</Table.Th>
<Table.Th ta="right">Interest Earned</Table.Th>
<Table.Th ta="right">Maturity Value</Table.Th>
<Table.Th ta="right">Days Remaining</Table.Th>
<Table.Th>Maturity Date</Table.Th>
<Table.Th ta="right">Days Remaining</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{investments.length === 0 && (
<Table.Tr>
<Table.Td colSpan={10}>
<Text ta="center" c="dimmed" py="lg">
No investments yet
</Text>
<Table.Td colSpan={11}>
<Text ta="center" c="dimmed" py="lg">No investment accounts</Text>
</Table.Td>
</Table.Tr>
)}
{investments.map((inv) => (
<Table.Tr key={inv.id}>
<Table.Td fw={500}>{inv.name}</Table.Td>
<Table.Td>{inv.institution}</Table.Td>
<Table.Td>{inv.institution || '-'}</Table.Td>
<Table.Td>
<Badge size="sm" variant="light">
<Badge size="sm" variant="light" color="teal">
{inv.investment_type}
</Badge>
</Table.Td>
<Table.Td>
<Badge size="sm" color={inv.fund_type === 'reserve' ? 'violet' : 'gray'} variant="light">
{inv.fund_type}
</Badge>
</Table.Td>
<Table.Td ta="right" ff="monospace">
{fmt(inv.principal)}
</Table.Td>
<Table.Td ta="right">
{parseFloat(inv.interest_rate || '0').toFixed(2)}%
</Table.Td>
<Table.Td ta="right" ff="monospace">{fmt(inv.principal)}</Table.Td>
<Table.Td ta="right" ff="monospace">{fmt(inv.current_value || inv.principal)}</Table.Td>
<Table.Td ta="right">{parseFloat(inv.interest_rate || '0').toFixed(2)}%</Table.Td>
<Table.Td ta="right" ff="monospace">
{inv.interest_earned !== null ? fmt(inv.interest_earned) : '-'}
</Table.Td>
<Table.Td ta="right" ff="monospace">
{inv.maturity_value !== null ? fmt(inv.maturity_value) : '-'}
</Table.Td>
<Table.Td>
{inv.maturity_date ? new Date(inv.maturity_date).toLocaleDateString() : '-'}
</Table.Td>
<Table.Td ta="right">
{inv.days_remaining !== null ? (
<Badge
@@ -940,79 +1051,11 @@ function InvestmentsTab({
)}
</Table.Td>
<Table.Td>
{inv.maturity_date ? new Date(inv.maturity_date).toLocaleDateString() : '-'}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
);
}
// ── Compact Investment Table for Operating/Reserve tabs ──
function InvestmentMiniTable({ investments }: { investments: Investment[] }) {
const totalValue = investments.reduce(
(s, i) => s + parseFloat(i.current_value || i.principal || '0'),
0,
);
return (
<Stack gap="xs">
<Group justify="space-between">
<Text size="sm" fw={600} c="dimmed">
{investments.length} investment{investments.length !== 1 ? 's' : ''} Total: {fmt(totalValue)}
</Text>
</Group>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Institution</Table.Th>
<Table.Th ta="right">Principal / Value</Table.Th>
<Table.Th ta="right">Rate</Table.Th>
<Table.Th ta="right">Interest Earned</Table.Th>
<Table.Th>Maturity</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{investments.map((inv) => (
<Table.Tr key={inv.id}>
<Table.Td fw={500}>{inv.name}</Table.Td>
<Table.Td>
<Badge size="sm" variant="light" color="teal">
{inv.investment_type}
</Badge>
</Table.Td>
<Table.Td>{inv.institution || '-'}</Table.Td>
<Table.Td ta="right" ff="monospace">
{fmt(inv.current_value || inv.principal)}
</Table.Td>
<Table.Td ta="right">
{parseFloat(inv.interest_rate || '0').toFixed(2)}%
</Table.Td>
<Table.Td ta="right" ff="monospace">
{inv.interest_earned !== null ? fmt(inv.interest_earned) : '-'}
</Table.Td>
<Table.Td>
{inv.maturity_date ? (
<Group gap={4}>
<Text size="sm">{new Date(inv.maturity_date).toLocaleDateString()}</Text>
{inv.days_remaining !== null && (
<Badge
size="xs"
color={inv.days_remaining <= 30 ? 'red' : inv.days_remaining <= 90 ? 'yellow' : 'gray'}
variant="light"
>
{inv.days_remaining}d
</Badge>
)}
</Group>
) : (
'-'
)}
<Tooltip label="Edit investment">
<ActionIcon variant="subtle" onClick={() => onEdit(inv)}>
<IconEdit size={16} />
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
))}