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:
@@ -117,7 +117,9 @@ const fmt = (v: string | number) =>
|
|||||||
export function AccountsPage() {
|
export function AccountsPage() {
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const [adjustOpened, { open: openAdjust, close: closeAdjust }] = 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 [editing, setEditing] = useState<Account | null>(null);
|
||||||
|
const [editingInvestment, setEditingInvestment] = useState<Investment | null>(null);
|
||||||
const [adjustingAccount, setAdjustingAccount] = useState<Account | null>(null);
|
const [adjustingAccount, setAdjustingAccount] = useState<Account | null>(null);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [filterType, setFilterType] = useState<string | null>(null);
|
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 ──
|
// ── 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) => {
|
const handleEdit = (account: Account) => {
|
||||||
setEditing(account);
|
setEditing(account);
|
||||||
form.setValues({
|
form.setValues({
|
||||||
@@ -436,7 +504,6 @@ export function AccountsPage() {
|
|||||||
<Tabs.Tab value="reserve">
|
<Tabs.Tab value="reserve">
|
||||||
Reserve ({activeAccounts.filter(a => a.fund_type === 'reserve').length + reserveInvestments.length})
|
Reserve ({activeAccounts.filter(a => a.fund_type === 'reserve').length + reserveInvestments.length})
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="investments">Investments ({investments.filter(i => i.is_active).length})</Tabs.Tab>
|
|
||||||
{showArchived && archivedAccounts.length > 0 && (
|
{showArchived && archivedAccounts.length > 0 && (
|
||||||
<Tabs.Tab value="archived" color="gray">
|
<Tabs.Tab value="archived" color="gray">
|
||||||
Archived ({archivedAccounts.length})
|
Archived ({archivedAccounts.length})
|
||||||
@@ -465,7 +532,7 @@ export function AccountsPage() {
|
|||||||
{operatingInvestments.length > 0 && (
|
{operatingInvestments.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider label="Operating Investment Accounts" labelPosition="center" my="xs" />
|
<Divider label="Operating Investment Accounts" labelPosition="center" my="xs" />
|
||||||
<InvestmentMiniTable investments={operatingInvestments} />
|
<InvestmentMiniTable investments={operatingInvestments} onEdit={handleEditInvestment} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -482,14 +549,11 @@ export function AccountsPage() {
|
|||||||
{reserveInvestments.length > 0 && (
|
{reserveInvestments.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider label="Reserve Investment Accounts" labelPosition="center" my="xs" />
|
<Divider label="Reserve Investment Accounts" labelPosition="center" my="xs" />
|
||||||
<InvestmentMiniTable investments={reserveInvestments} />
|
<InvestmentMiniTable investments={reserveInvestments} onEdit={handleEditInvestment} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="investments" pt="sm">
|
|
||||||
<InvestmentsTab investments={investments} isLoading={investmentsLoading} />
|
|
||||||
</Tabs.Panel>
|
|
||||||
{showArchived && (
|
{showArchived && (
|
||||||
<Tabs.Panel value="archived" pt="sm">
|
<Tabs.Panel value="archived" pt="sm">
|
||||||
<AccountTable
|
<AccountTable
|
||||||
@@ -693,6 +757,79 @@ export function AccountsPage() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</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>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -776,7 +913,7 @@ function AccountTable({
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
{a.account_type === 'asset' && (
|
{!a.is_system && (
|
||||||
<Tooltip label={a.is_primary ? 'Primary account' : 'Set as Primary'}>
|
<Tooltip label={a.is_primary ? 'Primary account' : 'Set as Primary'}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@@ -787,7 +924,7 @@ function AccountTable({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{a.account_type === 'asset' && (
|
{!a.is_system && (
|
||||||
<Tooltip label="Adjust Balance">
|
<Tooltip label="Adjust Balance">
|
||||||
<ActionIcon variant="subtle" color="blue" onClick={() => onAdjustBalance(a)}>
|
<ActionIcon variant="subtle" color="blue" onClick={() => onAdjustBalance(a)}>
|
||||||
<IconAdjustments size={16} />
|
<IconAdjustments size={16} />
|
||||||
@@ -819,14 +956,14 @@ function AccountTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Investments Tab Component ──
|
// ── Investment Table for Operating/Reserve tabs ──
|
||||||
|
|
||||||
function InvestmentsTab({
|
function InvestmentMiniTable({
|
||||||
investments,
|
investments,
|
||||||
isLoading,
|
onEdit,
|
||||||
}: {
|
}: {
|
||||||
investments: Investment[];
|
investments: Investment[];
|
||||||
isLoading: boolean;
|
onEdit: (inv: Investment) => void;
|
||||||
}) {
|
}) {
|
||||||
const totalPrincipal = investments.reduce((s, i) => s + parseFloat(i.principal || '0'), 0);
|
const totalPrincipal = investments.reduce((s, i) => s + parseFloat(i.principal || '0'), 0);
|
||||||
const totalValue = investments.reduce(
|
const totalValue = investments.reduce(
|
||||||
@@ -838,40 +975,20 @@ function InvestmentsTab({
|
|||||||
? investments.reduce((s, i) => s + parseFloat(i.interest_rate || '0'), 0) / investments.length
|
? investments.reduce((s, i) => s + parseFloat(i.interest_rate || '0'), 0) / investments.length
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Center h={200}>
|
|
||||||
<Loader />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="sm">
|
||||||
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
||||||
<Card withBorder p="md">
|
<Card withBorder p="xs">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">Total Principal</Text>
|
||||||
Total Principal
|
<Text fw={700} size="sm">{fmt(totalPrincipal)}</Text>
|
||||||
</Text>
|
|
||||||
<Text fw={700} size="xl">
|
|
||||||
{fmt(totalPrincipal)}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card withBorder p="md">
|
<Card withBorder p="xs">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">Total Current Value</Text>
|
||||||
Total Current Value
|
<Text fw={700} size="sm" c="teal">{fmt(totalValue)}</Text>
|
||||||
</Text>
|
|
||||||
<Text fw={700} size="xl" c="green">
|
|
||||||
{fmt(totalValue)}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card withBorder p="md">
|
<Card withBorder p="xs">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">Avg Interest Rate</Text>
|
||||||
Avg Interest Rate
|
<Text fw={700} size="sm">{avgRate.toFixed(2)}%</Text>
|
||||||
</Text>
|
|
||||||
<Text fw={700} size="xl">
|
|
||||||
{avgRate.toFixed(2)}%
|
|
||||||
</Text>
|
|
||||||
</Card>
|
</Card>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
@@ -881,51 +998,45 @@ function InvestmentsTab({
|
|||||||
<Table.Th>Name</Table.Th>
|
<Table.Th>Name</Table.Th>
|
||||||
<Table.Th>Institution</Table.Th>
|
<Table.Th>Institution</Table.Th>
|
||||||
<Table.Th>Type</Table.Th>
|
<Table.Th>Type</Table.Th>
|
||||||
<Table.Th>Fund</Table.Th>
|
|
||||||
<Table.Th ta="right">Principal</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">Rate</Table.Th>
|
||||||
<Table.Th ta="right">Interest Earned</Table.Th>
|
<Table.Th ta="right">Interest Earned</Table.Th>
|
||||||
<Table.Th ta="right">Maturity Value</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>Maturity Date</Table.Th>
|
||||||
|
<Table.Th ta="right">Days Remaining</Table.Th>
|
||||||
|
<Table.Th></Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{investments.length === 0 && (
|
{investments.length === 0 && (
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Td colSpan={10}>
|
<Table.Td colSpan={11}>
|
||||||
<Text ta="center" c="dimmed" py="lg">
|
<Text ta="center" c="dimmed" py="lg">No investment accounts</Text>
|
||||||
No investments yet
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
)}
|
)}
|
||||||
{investments.map((inv) => (
|
{investments.map((inv) => (
|
||||||
<Table.Tr key={inv.id}>
|
<Table.Tr key={inv.id}>
|
||||||
<Table.Td fw={500}>{inv.name}</Table.Td>
|
<Table.Td fw={500}>{inv.name}</Table.Td>
|
||||||
<Table.Td>{inv.institution}</Table.Td>
|
<Table.Td>{inv.institution || '-'}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Badge size="sm" variant="light">
|
<Badge size="sm" variant="light" color="teal">
|
||||||
{inv.investment_type}
|
{inv.investment_type}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td ta="right" ff="monospace">{fmt(inv.principal)}</Table.Td>
|
||||||
<Badge size="sm" color={inv.fund_type === 'reserve' ? 'violet' : 'gray'} variant="light">
|
<Table.Td ta="right" ff="monospace">{fmt(inv.current_value || inv.principal)}</Table.Td>
|
||||||
{inv.fund_type}
|
<Table.Td ta="right">{parseFloat(inv.interest_rate || '0').toFixed(2)}%</Table.Td>
|
||||||
</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">
|
<Table.Td ta="right" ff="monospace">
|
||||||
{inv.interest_earned !== null ? fmt(inv.interest_earned) : '-'}
|
{inv.interest_earned !== null ? fmt(inv.interest_earned) : '-'}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta="right" ff="monospace">
|
<Table.Td ta="right" ff="monospace">
|
||||||
{inv.maturity_value !== null ? fmt(inv.maturity_value) : '-'}
|
{inv.maturity_value !== null ? fmt(inv.maturity_value) : '-'}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{inv.maturity_date ? new Date(inv.maturity_date).toLocaleDateString() : '-'}
|
||||||
|
</Table.Td>
|
||||||
<Table.Td ta="right">
|
<Table.Td ta="right">
|
||||||
{inv.days_remaining !== null ? (
|
{inv.days_remaining !== null ? (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -940,79 +1051,11 @@ function InvestmentsTab({
|
|||||||
)}
|
)}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{inv.maturity_date ? new Date(inv.maturity_date).toLocaleDateString() : '-'}
|
<Tooltip label="Edit investment">
|
||||||
</Table.Td>
|
<ActionIcon variant="subtle" onClick={() => onEdit(inv)}>
|
||||||
</Table.Tr>
|
<IconEdit size={16} />
|
||||||
))}
|
</ActionIcon>
|
||||||
</Table.Tbody>
|
</Tooltip>
|
||||||
</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>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user