Adopt RUB-only API contract; remove currency cards, selectors, screener currency filter and multi-currency branches. BREAKING CHANGE: requires the RUB-only backend contract.
134 lines
3.4 KiB
TypeScript
134 lines
3.4 KiB
TypeScript
import type { FC } from "react";
|
|
|
|
import { Button, Group, 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 { ReadRetry, type ReadRetryProps, SourceSyncStatus } from "@/shared/ui";
|
|
|
|
import classes from "./AccountsPageLoaded.module.css";
|
|
|
|
const DECIMAL_BASE = 10n;
|
|
const DECIMAL_PART_COUNT = 2;
|
|
const ZERO = 0n;
|
|
|
|
type AccountsPageLoadedProps = {
|
|
accounts: BrokerageAccountCard[];
|
|
meta: InvestmentResourceMeta;
|
|
readRetry?: ReadRetryProps;
|
|
};
|
|
|
|
function accountTotals(accounts: BrokerageAccountCard[]): null | string {
|
|
const values: string[] = [];
|
|
|
|
for (const { summary } of accounts) {
|
|
if (summary.state !== "available") continue;
|
|
|
|
values.push(summary.portfolio.totals.value);
|
|
}
|
|
|
|
return values.length > 0 ? sumDecimals(values) : null;
|
|
}
|
|
|
|
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 total = accountTotals(props.accounts);
|
|
|
|
const excludedCount = props.accounts.filter(
|
|
account => account.summary.state === "unavailable",
|
|
).length;
|
|
|
|
return (
|
|
<Stack gap="lg">
|
|
<Title order={1}>{t("accounts.title")}</Title>
|
|
|
|
<Stack className={classes.summary} gap="xs">
|
|
<Text c="dimmed" size="sm">
|
|
{t("accounts.currentValue")}
|
|
</Text>
|
|
|
|
{total === null
|
|
? (
|
|
<Text c="dimmed">{t("accounts.summaryNoTotals")}</Text>
|
|
)
|
|
: (
|
|
<Text className={classes.primaryTotal} fw={700}>
|
|
{formatMoney({ currency: "RUB", value: total }, { withRoundedCents: true })}
|
|
</Text>
|
|
)}
|
|
|
|
<Group className={classes.meta} gap="xs" wrap="wrap">
|
|
<Text c="dimmed" size="xs">
|
|
{t("accounts.count", { count: props.accounts.length })}
|
|
</Text>
|
|
|
|
<SourceSyncStatus
|
|
lastAttempt={props.meta.lastAttempt}
|
|
lastSuccessfulAt={props.meta.lastSuccessfulAt}
|
|
stale={props.meta.freshness === "stale"}
|
|
/>
|
|
</Group>
|
|
|
|
{excludedCount > 0 && (
|
|
<Text c="dimmed" size="sm">
|
|
{t("accounts.summaryExcluded", { count: excludedCount })}
|
|
</Text>
|
|
)}
|
|
|
|
{!!props.readRetry && <ReadRetry {...props.readRetry} />}
|
|
</Stack>
|
|
|
|
<Button
|
|
component={Link}
|
|
to="/accounts/all"
|
|
variant="light"
|
|
w="fit-content"
|
|
>
|
|
{t("accounts.openAll")}
|
|
</Button>
|
|
|
|
<BrokerageAccountList accounts={props.accounts} />
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export { AccountsPageLoaded };
|