147 lines
3.8 KiB
TypeScript
147 lines
3.8 KiB
TypeScript
import type { FC } from "react";
|
|
|
|
import { Accordion, Alert, Badge, Group, Stack, Text } from "@mantine/core";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import type { BondCoupons as Coupons } from "@/entities/bond";
|
|
|
|
import { InstrumentSection, type SectionState } from "@/features/instrument-details";
|
|
import { formatDate, formatSnapshotTime, instrumentLogoUrl } from "@/shared/lib";
|
|
import { PayoutCard } from "@/shared/ui";
|
|
|
|
type BondCouponsProps = {
|
|
coupons?: Coupons;
|
|
logoName?: null | string;
|
|
pastCoupons: Coupons["coupons"];
|
|
state: SectionState;
|
|
ticker?: null | string;
|
|
upcoming: Coupons["coupons"];
|
|
};
|
|
|
|
const BondCoupons: FC<BondCouponsProps> = (props) => {
|
|
const { coupons, pastCoupons, state, upcoming } = props;
|
|
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<InstrumentSection
|
|
state={state}
|
|
title={t("bond.upcomingCoupons")}
|
|
>
|
|
{!!coupons && (
|
|
<Stack gap="md">
|
|
{!!(coupons.estimateUnavailable || coupons.positionIsStale) && (
|
|
<Alert aria-live="polite" color="orange" role="status">
|
|
{t("stock.estimateUnavailable")}
|
|
</Alert>
|
|
)}
|
|
|
|
{upcoming.length > 0
|
|
? (
|
|
upcoming.map(coupon => (
|
|
<Coupon
|
|
coupon={coupon}
|
|
key={`${coupon.number}:${coupon.paymentDate}`}
|
|
logoName={props.logoName}
|
|
ticker={props.ticker}
|
|
/>
|
|
))
|
|
)
|
|
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
|
|
|
<Text c="dimmed" size="xs">
|
|
{t("bond.couponWindow", {
|
|
from: formatDate(coupons.from),
|
|
to: formatDate(coupons.to),
|
|
})}
|
|
</Text>
|
|
|
|
<Text c="dimmed" size="xs">
|
|
{t("bond.couponNote")}
|
|
</Text>
|
|
|
|
{!!coupons.positionAt && (
|
|
<Text c="dimmed" size="xs">
|
|
{t("stock.snapshotAt", {
|
|
date: formatSnapshotTime(coupons.positionAt),
|
|
})}
|
|
</Text>
|
|
)}
|
|
|
|
<Accordion radius="md" variant="contained">
|
|
<Accordion.Item value="history">
|
|
<Accordion.Control>{t("bond.couponHistory")}</Accordion.Control>
|
|
|
|
<Accordion.Panel>
|
|
<Stack gap="sm">
|
|
{pastCoupons.length > 0
|
|
? (
|
|
pastCoupons.map(coupon => (
|
|
<Coupon
|
|
coupon={coupon}
|
|
key={`${coupon.number}:${coupon.paymentDate}`}
|
|
logoName={props.logoName}
|
|
ticker={props.ticker}
|
|
/>
|
|
))
|
|
)
|
|
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
|
</Stack>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
</Accordion>
|
|
</Stack>
|
|
)}
|
|
</InstrumentSection>
|
|
);
|
|
};
|
|
|
|
type CouponProps = {
|
|
coupon: Coupons["coupons"][number];
|
|
logoName?: null | string;
|
|
ticker?: null | string;
|
|
};
|
|
|
|
const Coupon: FC<CouponProps> = (props) => {
|
|
const { coupon } = props;
|
|
const { t } = useTranslation();
|
|
const title = `${t("events.type.coupon")} · ${props.ticker || t("bond.instrument")}`;
|
|
|
|
const metadata = [
|
|
coupon.paymentDate ? formatDate(coupon.paymentDate) : t("stock.dateUnknown"),
|
|
t("bond.couponNumber", { number: coupon.number ?? t("bond.unknown") }),
|
|
].join(" · ");
|
|
|
|
const conditions = (
|
|
<Group gap="md" mt="sm">
|
|
<Badge color="gray" variant="light">
|
|
{t(`bond.couponTypes.${coupon.couponType}`, { defaultValue: t("bond.unknown") })}
|
|
</Badge>
|
|
|
|
{coupon.periodDays != undefined && (
|
|
<Text c="dimmed" size="xs">{t("bond.couponDays", { count: coupon.periodDays })}</Text>
|
|
)}
|
|
|
|
{coupon.recordDate && (
|
|
<Text c="dimmed" size="xs">{t("stock.record")}: {formatDate(coupon.recordDate)}</Text>
|
|
)}
|
|
</Group>
|
|
);
|
|
|
|
return (
|
|
<PayoutCard
|
|
conditions={conditions}
|
|
estimate={coupon.estimatedAmount}
|
|
imageSrc={instrumentLogoUrl(props.logoName)}
|
|
initial={(props.ticker?.trim()[0] ?? "?").toUpperCase()}
|
|
kind="schedule"
|
|
metadata={metadata}
|
|
perSecurity={coupon.perBond}
|
|
perSecurityLabel={t("bond.perBond")}
|
|
title={title}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export { BondCoupons };
|