170 lines
4.0 KiB
TypeScript
170 lines
4.0 KiB
TypeScript
import type { FC } from "react";
|
|
|
|
import { Paper, Stack, Text, Title } from "@mantine/core";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { formatMoney } from "@/shared/lib";
|
|
|
|
import type { IncomeDisplayModel } from "../AccountIncomePage.vm";
|
|
|
|
import classes from "../AccountIncomePage.module.css";
|
|
|
|
type ComparisonProps = NonNullable<IncomeDisplayModel["comparison"]> & {
|
|
title: string;
|
|
};
|
|
|
|
type ComparisonValuesProps = {
|
|
kind: "compared"
|
|
| "difference"
|
|
| "selected";
|
|
label: string;
|
|
percentages?: ComparisonProps["values"][number]["percentages"];
|
|
values: ComparisonProps["values"][number]["selected"];
|
|
};
|
|
|
|
type IncomeComparisonsProps = {
|
|
comparison: IncomeDisplayModel["comparison"];
|
|
notice: string;
|
|
title: string;
|
|
};
|
|
|
|
const IncomeComparisons: FC<IncomeComparisonsProps> = (props) => {
|
|
return (
|
|
<Stack>
|
|
<Title order={2}>{props.title}</Title>
|
|
|
|
{props.comparison
|
|
? (
|
|
<Comparison {...props.comparison} title={props.title} />
|
|
)
|
|
: (
|
|
<Paper className={classes.comparisonNotice} p="sm" radius="md">
|
|
<Text aria-live="polite" c="gray.7" role="status" size="sm">
|
|
{props.notice}
|
|
</Text>
|
|
</Paper>
|
|
)}
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
const Comparison: FC<ComparisonProps> = (props) => {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<section aria-label={props.title} className={classes.comparison}>
|
|
<Stack gap="md">
|
|
{props.values.map(value => (
|
|
<Stack
|
|
aria-label={value.currency}
|
|
gap="xs"
|
|
key={value.currency}
|
|
role="group"
|
|
>
|
|
<div className={classes.comparisonGrid}>
|
|
<ComparisonValues
|
|
kind="selected"
|
|
label={props.selectedPeriod}
|
|
values={value.selected}
|
|
/>
|
|
|
|
<ComparisonValues
|
|
kind="compared"
|
|
label={props.comparedPeriod}
|
|
values={value.compared}
|
|
/>
|
|
|
|
<ComparisonValues
|
|
kind="difference"
|
|
label={t("income.difference")}
|
|
percentages={value.percentages}
|
|
values={value.difference}
|
|
/>
|
|
</div>
|
|
</Stack>
|
|
))}
|
|
|
|
{props.values.length === 0 && (
|
|
<Text c="gray.7" size="sm">
|
|
{t("income.comparisonEmpty")}
|
|
</Text>
|
|
)}
|
|
</Stack>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const ComparisonValues: FC<ComparisonValuesProps> = (props) => {
|
|
const { t } = useTranslation();
|
|
|
|
const data_tone
|
|
= props.kind === "difference"
|
|
? differenceTone(props.values.total.value)
|
|
: undefined;
|
|
|
|
return (
|
|
<section
|
|
aria-label={props.label}
|
|
className={classes.comparisonPanel}
|
|
data-kind={props.kind}
|
|
data-tone={data_tone}
|
|
>
|
|
<Text className={classes.comparisonLabel} fw={600} size="sm">
|
|
{props.label}
|
|
</Text>
|
|
|
|
<dl className={classes.comparisonValues}>
|
|
{(["total", "coupon", "dividend"] as const).map((key) => {
|
|
const money
|
|
// eslint-disable-next-line security/detect-object-injection -- This lookup uses a fixed typed key set, not an arbitrary external property name.
|
|
= props.values[key];
|
|
|
|
const percentage
|
|
// eslint-disable-next-line security/detect-object-injection -- This lookup uses a fixed typed key set, not an arbitrary external property name.
|
|
= props.percentages?.[key];
|
|
|
|
const tone
|
|
= props.kind === "difference"
|
|
? differenceTone(money.value)
|
|
: undefined;
|
|
|
|
const appearanceClass
|
|
= key === "total"
|
|
? classes.comparisonTotal
|
|
: classes.comparisonDetail;
|
|
|
|
return (
|
|
<div className={appearanceClass} key={key}>
|
|
<dt>{t(`income.${key}`)}</dt>
|
|
|
|
<dd data-tone={tone}>
|
|
<span>
|
|
{tone === "positive" ? "+" : ""}
|
|
|
|
{formatMoney(money)}
|
|
</span>
|
|
|
|
{!!percentage && (
|
|
<span className={classes.comparisonPercent}>
|
|
{percentage}
|
|
</span>
|
|
)}
|
|
</dd>
|
|
</div>
|
|
);
|
|
})}
|
|
</dl>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
function differenceTone(value: string): "negative"
|
|
| "neutral"
|
|
| "positive" {
|
|
if (!/[1-9]/.test(value)) return "neutral";
|
|
|
|
return value.startsWith("-") ? "negative" : "positive";
|
|
}
|
|
|
|
export { IncomeComparisons };
|