Files
frontend/src/pages/accounts/ui/AccountsPage/components/AccountsPageLoaded.view.tsx
T
ksv741 b40066838c
Frontend verify / commits (pull_request) Successful in 35s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 7m20s
refactor!: support RUB only (WealthLedger/main#55)
Adopt RUB-only API contract; remove currency cards, selectors,
screener currency filter and multi-currency branches.

BREAKING CHANGE: requires the RUB-only backend contract.
2026-11-11 08:22:44 +03:00

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 };