refactor: align frontend ownership boundaries
Close audit gaps across API drift, query policy, UI, and tests.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user