Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 8s
Frontend verify / verify (pull_request) Successful in 5m25s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 5m20s
152 lines
3.6 KiB
TypeScript
152 lines
3.6 KiB
TypeScript
import type { FC } from "react";
|
|
|
|
import { Button, SimpleGrid, Stack, Text, Title } from "@mantine/core";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Link } from "react-router";
|
|
|
|
import type { InvestmentResourceMeta } from "@/shared/api";
|
|
|
|
import {
|
|
type BrokerageAccountCard,
|
|
BrokerageAccountList,
|
|
} from "@/entities/brokerage-account";
|
|
import { formatMoney } from "@/shared/lib";
|
|
import { SourceSyncStatus } from "@/shared/ui";
|
|
|
|
const DECIMAL_BASE = 10n;
|
|
const DECIMAL_PART_COUNT = 2;
|
|
const ZERO = 0n;
|
|
|
|
type AccountsPageLoadedProps = {
|
|
accounts: BrokerageAccountCard[];
|
|
meta: InvestmentResourceMeta;
|
|
};
|
|
|
|
type Money = { currency: string; value: string };
|
|
|
|
function accountTotals(accounts: BrokerageAccountCard[]) {
|
|
const values = new Map<string, string[]>();
|
|
let excludedCount = 0;
|
|
|
|
for (const { summary } of accounts) {
|
|
if (summary.state === "empty") continue;
|
|
|
|
if (summary.state === "unavailable") {
|
|
excludedCount += 1;
|
|
continue;
|
|
}
|
|
|
|
for (const total of summary.portfolio.totalsByCurrency) {
|
|
values.set(total.currency, [
|
|
...values.get(total.currency) ?? [],
|
|
total.value,
|
|
]);
|
|
}
|
|
}
|
|
|
|
const totals: Money[] = Array.from(values, ([currency, amounts]) => ({
|
|
currency,
|
|
value: sumDecimals(amounts),
|
|
}));
|
|
|
|
return { excludedCount, totals };
|
|
}
|
|
|
|
function sumDecimals(values: string[]) {
|
|
const scale = Math.max(
|
|
0,
|
|
...values.map(value =>
|
|
value.split(".", DECIMAL_PART_COUNT)[1]?.length ?? 0),
|
|
);
|
|
|
|
const factor = DECIMAL_BASE ** BigInt(scale);
|
|
let sum = ZERO;
|
|
|
|
for (const value of values) {
|
|
const isNegative = value.startsWith("-");
|
|
|
|
const [integer = "0", fraction = ""] = (isNegative
|
|
? value.slice(1)
|
|
: value).split(".", DECIMAL_PART_COUNT);
|
|
|
|
const units = BigInt(integer) * factor
|
|
+ BigInt(fraction.padEnd(scale, "0") || "0");
|
|
|
|
sum += isNegative ? -units : units;
|
|
}
|
|
|
|
const isNegative = sum < ZERO;
|
|
const digits = (isNegative ? -sum : sum).toString().padStart(scale + 1, "0");
|
|
const integer = scale ? digits.slice(0, -scale) : digits;
|
|
const fraction = scale ? `.${digits.slice(-scale)}` : "";
|
|
|
|
return `${isNegative ? "-" : ""}${integer}${fraction}`;
|
|
}
|
|
|
|
const AccountsPageLoaded: FC<AccountsPageLoadedProps> = (props) => {
|
|
const { t } = useTranslation();
|
|
const { excludedCount, totals } = accountTotals(props.accounts);
|
|
|
|
return (
|
|
<Stack gap="lg">
|
|
<Stack gap={4}>
|
|
<Title order={1}>{t("accounts.title")}</Title>
|
|
|
|
<Text c="dimmed" size="sm">
|
|
{t("accounts.subtitle")}
|
|
</Text>
|
|
</Stack>
|
|
|
|
<Stack gap="xs">
|
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="xs">
|
|
<Stack gap={4}>
|
|
<Text c="dimmed" size="sm">
|
|
{t("accounts.currentValue")}
|
|
</Text>
|
|
|
|
{totals.length > 0
|
|
? totals.map(total => (
|
|
<Text fw={700} key={total.currency} size="lg">
|
|
{formatMoney(total, { withRoundedCents: true })}
|
|
</Text>
|
|
))
|
|
: <Text c="dimmed">{t("accounts.summaryNoTotals")}</Text>}
|
|
</Stack>
|
|
|
|
<Stack gap={4}>
|
|
<Text fw={600}>{t("accounts.count", {
|
|
count: props.accounts.length,
|
|
})}
|
|
</Text>
|
|
|
|
{excludedCount > 0 && (
|
|
<Text c="dimmed" size="sm">
|
|
{t("accounts.summaryExcluded", { count: excludedCount })}
|
|
</Text>
|
|
)}
|
|
</Stack>
|
|
</SimpleGrid>
|
|
|
|
<SourceSyncStatus
|
|
lastAttempt={props.meta.lastAttempt}
|
|
lastSuccessfulAt={props.meta.lastSuccessfulAt}
|
|
stale={props.meta.freshness === "stale"}
|
|
/>
|
|
</Stack>
|
|
|
|
<Button
|
|
component={Link}
|
|
to="/accounts/all"
|
|
variant="light"
|
|
w="fit-content"
|
|
>
|
|
{t("accounts.openAll")}
|
|
</Button>
|
|
|
|
<BrokerageAccountList accounts={props.accounts} />
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export { AccountsPageLoaded };
|