feat: redesign year-over-year income comparison

This commit is contained in:
2026-09-09 18:28:15 +03:00
parent 451400b63b
commit 91eaeb5661
6 changed files with 398 additions and 59 deletions
@@ -11,6 +11,15 @@ type IncomeForm = IncomePeriod;
type IncomeDisplayModel = {
entries: { key: string; operation: IncomeAnalytics["entries"][number] }[];
comparison: {
selectedPeriod: string;
comparedPeriod: string;
values: (NonNullable<
IncomeAnalytics["yearComparison"]
>["totalsByCurrency"][number] & {
percentages: Record<"total" | "coupon" | "dividend", string | null>;
})[];
} | null;
};
type AccountIncomePageViewModel =
@@ -209,7 +218,60 @@ function toIncomeDisplayModel(
if (instrument.includedCount !== instrument.receiptCount) return null;
}
return { entries };
return {
entries,
comparison: analytics.yearComparison
? {
selectedPeriod: formatComparisonPeriod(analytics.period),
comparedPeriod: formatComparisonPeriod(
analytics.yearComparison.period,
),
values: analytics.yearComparison.totalsByCurrency.map((value) => ({
...value,
percentages: {
total: formatIncomeChange(
value.difference.total.value,
value.compared.total.value,
),
coupon: formatIncomeChange(
value.difference.coupon.value,
value.compared.coupon.value,
),
dividend: formatIncomeChange(
value.difference.dividend.value,
value.compared.dividend.value,
),
},
})),
}
: null,
};
}
function formatComparisonPeriod(period: IncomePeriod): string {
return new Intl.DateTimeFormat("ru-RU", {
dateStyle: "medium",
timeZone: "UTC",
}).formatRange(
new Date(`${period.from}T00:00:00Z`),
new Date(`${period.to}T00:00:00Z`),
);
}
function formatIncomeChange(
difference: string,
compared: string,
): string | null {
// Display-only ratio; server-provided Money values remain exact decimal strings.
const base = Number(compared);
if (!Number.isFinite(base) || base <= 0) return null;
const ratio = Number(difference) / base;
if (!Number.isFinite(ratio)) return null;
return new Intl.NumberFormat("ru-RU", {
style: "percent",
maximumFractionDigits: 1,
signDisplay: "exceptZero",
}).format(ratio);
}
function moscowDate(value: Date): string {
@@ -227,6 +289,7 @@ function moscowDate(value: Date): string {
export {
type AccountIncomePageViewModel,
formatDraftDate,
type IncomeDisplayModel,
type IncomeForm,
isDate,
moscowDate,