Files
frontend/src/pages/account-income/ui/components/IncomeTrend.view.tsx
T
ksv741 c97f62bd98 refactor: align frontend ownership boundaries
Close audit gaps across API drift, query policy, UI, and tests.
2026-09-06 18:20:05 +03:00

108 lines
3.3 KiB
TypeScript

import { SegmentedControl, Stack, Text, Title } from "@mantine/core";
import type { FC } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { formatMoney } from "@/shared/lib";
import styles from "../AccountIncomePage.module.css";
import type { AccountIncomePageViewModel } from "../AccountIncomePage.vm";
type LoadedProps = Extract<AccountIncomePageViewModel, { state: "loaded" }>;
type IncomeTrendProps = { trend: LoadedProps["display"]["trend"] };
const IncomeTrend: FC<IncomeTrendProps> = (props) => {
const currencies = props.trend.map(({ currency }) => currency);
const [currency, setCurrency] = useState(currencies[0] ?? "");
const { t } = useTranslation();
const buckets =
props.trend.find((item) => item.currency === currency)?.buckets ?? [];
const values = buckets.map((bucket) => ({
...bucket,
coupon: Number(bucket.total.coupon.value),
dividend: Number(bucket.total.dividend.value),
}));
const scale = Math.max(
1,
...values.map((value) =>
Math.max(
positive(value.coupon) + positive(value.dividend),
negative(value.coupon) + negative(value.dividend),
),
),
);
return (
<Stack>
<Title order={2}>{t("income.trend")}</Title>
{currencies.length > 1 ? (
<SegmentedControl
aria-label={t("income.currency")}
data={currencies}
onChange={setCurrency}
value={currency}
/>
) : null}
<div aria-hidden="true" className={styles.trendChart}>
{values.map((value) => (
<div className={styles.trendColumn} key={value.period.from}>
<span className={styles.baseline} />
<div className={styles.positiveStack}>
{value.coupon > 0 ? (
<span
className={styles.coupon}
data-testid={`trend-positive-coupon-${value.period.from}`}
style={{ flexBasis: chartHeight(value.coupon, scale) }}
/>
) : null}
{value.dividend > 0 ? (
<span
className={styles.dividend}
data-testid={`trend-positive-dividend-${value.period.from}`}
style={{ flexBasis: chartHeight(value.dividend, scale) }}
/>
) : null}
</div>
<div className={styles.negativeStack}>
{value.coupon < 0 ? (
<span
className={styles.coupon}
data-testid={`trend-negative-coupon-${value.period.from}`}
style={{ flexBasis: chartHeight(value.coupon, scale) }}
/>
) : null}
{value.dividend < 0 ? (
<span
className={styles.dividend}
data-testid={`trend-negative-dividend-${value.period.from}`}
style={{ flexBasis: chartHeight(value.dividend, scale) }}
/>
) : null}
</div>
</div>
))}
</div>
<Stack gap={2}>
{buckets.map((bucket) => (
<Text key={bucket.period.from} size="sm">
{bucket.period.from} — {bucket.period.to}: {t("income.coupon")}{" "}
{formatMoney(bucket.total.coupon)}, {t("income.dividend")}{" "}
{formatMoney(bucket.total.dividend)}
</Text>
))}
</Stack>
</Stack>
);
};
function positive(value: number): number {
return Math.max(0, value);
}
function negative(value: number): number {
return Math.max(0, -value);
}
function chartHeight(value: number, scale: number): string {
return `${(Math.abs(value) / scale) * 100}%`;
}
export { IncomeTrend };