108 lines
3.3 KiB
TypeScript
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 };
|