feat: unify income payment cards and remove comparisons (WealthLedger/main#33) (#74)
feat: unify income payment cards and remove comparisons (WealthLedger/main#33)
This commit was merged in pull request #74.
This commit is contained in:
@@ -337,7 +337,7 @@ describe("stock page", () => {
|
||||
"/stocks/SBERP?period=year&events=trades",
|
||||
),
|
||||
);
|
||||
expect(await screen.findByText("Зачисление дивидендов")).toBeVisible();
|
||||
expect(await screen.findByText("Дивиденд")).toBeVisible();
|
||||
});
|
||||
it("offers search for ambiguous tickers without requesting personal data", async () => {
|
||||
const { requests } = setup({ ambiguousTicker: true, path: "/stocks/SAME" });
|
||||
@@ -368,18 +368,18 @@ describe("stock page", () => {
|
||||
screen.getByText("К закрытию предыдущего торгового дня, 09.09.2026"),
|
||||
).toBeVisible();
|
||||
expect(await screen.findByText("Зачисление дивидендов")).toBeVisible();
|
||||
expect(screen.getByText("Событие компании")).toBeVisible();
|
||||
expect(screen.getAllByText("на акцию").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("328,8 ₽")).toBeVisible();
|
||||
expect(screen.getByText("P/E · TTM")).toBeVisible();
|
||||
expect(screen.getByText("Описание компании")).toBeVisible();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "О компании" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByText("13.10.2026")).toBeVisible();
|
||||
expect(screen.getByText(/13\.10\.2026/)).toBeVisible();
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "История дивидендов компании" }),
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText("13.10.2025")).toBeVisible());
|
||||
await waitFor(() => expect(screen.getByText(/13\.10\.2025/)).toBeVisible());
|
||||
expect(screen.getByText("Текущая средняя покупка: 200 ₽")).toBeVisible();
|
||||
fireEvent.click(screen.getByRole("button", { name: /Цены в таблице/ }));
|
||||
const rows = within(await screen.findByRole("table")).getAllByRole("row");
|
||||
|
||||
@@ -63,8 +63,10 @@ const StockPageView: FC<StockPageViewProps> = (props) => {
|
||||
|
||||
<StockDividends
|
||||
dividends={vm.dividends}
|
||||
logoName={stock.logoName}
|
||||
pastDividends={vm.pastDividends}
|
||||
state={vm.dividendsState}
|
||||
ticker={stock.ticker}
|
||||
upcoming={vm.upcoming}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,198 +1,96 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
Alert,
|
||||
Badge,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { Accordion, Alert, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { StockDividends as Dividends } from "@/entities/stock";
|
||||
|
||||
import { InstrumentSection, type SectionState } from "@/features/instrument-details";
|
||||
import { formatDate, formatMoney, formatSnapshotTime } from "@/shared/lib";
|
||||
import { formatDate, formatSnapshotTime, instrumentLogoUrl } from "@/shared/lib";
|
||||
import { PayoutCard } from "@/shared/ui";
|
||||
|
||||
import classes from "../StockPage.module.css";
|
||||
type Dividend = Dividends["dividends"][number];
|
||||
|
||||
type DividendCardProps = {
|
||||
event: Dividend;
|
||||
logoName?: null | string;
|
||||
ticker?: null | string;
|
||||
};
|
||||
|
||||
type StockDividendsProps = {
|
||||
dividends?: Dividends;
|
||||
logoName?: null | string;
|
||||
pastDividends: Dividends["dividends"];
|
||||
state: SectionState;
|
||||
ticker?: null | string;
|
||||
upcoming: Dividends["dividends"];
|
||||
};
|
||||
|
||||
const DividendCard: FC<DividendCardProps> = (props) => {
|
||||
const { event } = props;
|
||||
const { t } = useTranslation();
|
||||
const title = `${t("events.type.dividend")} · ${props.ticker || t("stock.share")}`;
|
||||
|
||||
const metadata = [
|
||||
event.recordDate
|
||||
? `${t("stock.record")}: ${formatDate(event.recordDate)}`
|
||||
: t("stock.dateUnknown"),
|
||||
event.paymentDate ? `${t("stock.payment")}: ${formatDate(event.paymentDate)}` : null,
|
||||
].filter(Boolean).join(" · ");
|
||||
|
||||
const conditions = event.lastBuyDate
|
||||
? <Text c="dimmed" size="xs">{t("stock.lastBuy")}: {formatDate(event.lastBuyDate)}</Text>
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<PayoutCard
|
||||
conditions={conditions}
|
||||
estimate={event.estimatedAmount}
|
||||
imageSrc={instrumentLogoUrl(props.logoName)}
|
||||
initial={(props.ticker?.trim()[0] ?? "?").toUpperCase()}
|
||||
kind="schedule"
|
||||
metadata={metadata}
|
||||
perSecurity={event.perShare}
|
||||
perSecurityLabel={t("stock.perShare")}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StockDividends: FC<StockDividendsProps> = (props) => {
|
||||
const { dividends, pastDividends, state, upcoming } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
let upcomingContent;
|
||||
|
||||
if (dividends) {
|
||||
if (upcoming.length > 0) {
|
||||
upcomingContent = (
|
||||
upcoming.map(event => (
|
||||
<div
|
||||
className={classes.dividend}
|
||||
key={JSON.stringify([
|
||||
event.recordDate,
|
||||
event.paymentDate,
|
||||
event.perShare,
|
||||
])}
|
||||
>
|
||||
<Group align="start" justify="space-between">
|
||||
<div>
|
||||
<Badge color="green" variant="light">
|
||||
{t("stock.companyEvent")}
|
||||
</Badge>
|
||||
|
||||
<Text fw={650} mt="xs" size="xl">
|
||||
{formatMoney(event.perShare)}
|
||||
{" "}
|
||||
<Text c="dimmed" component="span" size="sm">
|
||||
{t("stock.perShare")}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{!!event.estimatedAmount && (
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.yourEstimate")}
|
||||
</Text>
|
||||
|
||||
<Text fw={600}>
|
||||
{formatMoney(event.estimatedAmount)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Group gap="xl" mt="md">
|
||||
{!!event.lastBuyDate && (
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.lastBuy")}
|
||||
</Text>
|
||||
|
||||
<Text>{formatDate(event.lastBuyDate)}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!event.recordDate && (
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.record")}
|
||||
</Text>
|
||||
|
||||
<Text>{formatDate(event.recordDate)}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!event.paymentDate && (
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.payment")}
|
||||
</Text>
|
||||
|
||||
<Text>{formatDate(event.paymentDate)}</Text>
|
||||
</div>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
}
|
||||
else {
|
||||
upcomingContent = (
|
||||
<Text c="dimmed">
|
||||
{t("stock.noUpcoming", { to: formatDate(dividends.to) })}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let pastDividendsContent;
|
||||
|
||||
if (pastDividends.length > 0) {
|
||||
pastDividendsContent = (
|
||||
<Stack gap="sm">
|
||||
{pastDividends.map(event => (
|
||||
<Paper
|
||||
key={JSON.stringify([
|
||||
event.recordDate,
|
||||
event.paymentDate,
|
||||
event.perShare,
|
||||
])}
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
>
|
||||
<Group align="start" justify="space-between">
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.record")}
|
||||
</Text>
|
||||
|
||||
<Text fw={600}>
|
||||
{event.recordDate
|
||||
? formatDate(event.recordDate)
|
||||
: t("stock.dateUnknown")}
|
||||
</Text>
|
||||
|
||||
{!!event.paymentDate && (
|
||||
<Text c="dimmed" mt="xs" size="xs">
|
||||
{t("stock.payment")}:{" "}
|
||||
{formatDate(event.paymentDate)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text fw={650} size="lg">
|
||||
{formatMoney(event.perShare)}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.perShare")}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
else {
|
||||
pastDividendsContent = <Text c="dimmed">{t("stock.noDividends")}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<InstrumentSection state={state} title={t("stock.upcoming")}>
|
||||
{!!dividends && (
|
||||
<Stack gap="md">
|
||||
{!!(
|
||||
dividends.estimateUnavailable || dividends.positionIsStale
|
||||
) && (
|
||||
{(dividends.estimateUnavailable || dividends.positionIsStale) && (
|
||||
<Alert aria-live="polite" color="orange" role="status">
|
||||
{t("stock.estimateUnavailable")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{upcomingContent}
|
||||
{upcoming.length > 0
|
||||
? upcoming.map(event => (
|
||||
<DividendCard
|
||||
event={event}
|
||||
key={JSON.stringify([
|
||||
event.recordDate,
|
||||
event.paymentDate,
|
||||
event.perShare,
|
||||
])}
|
||||
logoName={props.logoName}
|
||||
ticker={props.ticker}
|
||||
/>
|
||||
))
|
||||
: <Text c="dimmed">{t("stock.noUpcoming", { to: formatDate(dividends.to) })}</Text>}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.dividendNote")}
|
||||
</Text>
|
||||
<Text c="dimmed" size="xs">{t("stock.dividendNote")}</Text>
|
||||
|
||||
{!!dividends.positionAt && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.snapshotAt", {
|
||||
date: formatSnapshotTime(dividends.positionAt),
|
||||
})}
|
||||
{t("stock.snapshotAt", { date: formatSnapshotTime(dividends.positionAt) })}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -210,7 +108,24 @@ const StockDividends: FC<StockDividendsProps> = (props) => {
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{pastDividendsContent}
|
||||
{pastDividends.length > 0
|
||||
? (
|
||||
<Stack gap="sm">
|
||||
{pastDividends.map(event => (
|
||||
<DividendCard
|
||||
event={event}
|
||||
key={JSON.stringify([
|
||||
event.recordDate,
|
||||
event.paymentDate,
|
||||
event.perShare,
|
||||
])}
|
||||
logoName={props.logoName}
|
||||
ticker={props.ticker}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
: <Text c="dimmed">{t("stock.noDividends")}</Text>}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
|
||||
Reference in New Issue
Block a user