217 lines
6.0 KiB
TypeScript
217 lines
6.0 KiB
TypeScript
import {
|
|
Alert,
|
|
Group,
|
|
SegmentedControl,
|
|
Stack,
|
|
Tabs,
|
|
Text,
|
|
} from "@mantine/core";
|
|
import { type FC, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import type { BrokerageAccount } from "@/entities/brokerage-account";
|
|
import type { Portfolio } from "@/entities/portfolio";
|
|
import type { InvestmentResourceMeta } from "@/shared/api";
|
|
|
|
import { BrokerageEventsPreview } from "@/entities/brokerage-event";
|
|
import { formatMoney } from "@/shared/lib";
|
|
import { SourceSyncStatus } from "@/shared/ui";
|
|
import { PortfolioStructure } from "@/widgets/portfolio-structure";
|
|
|
|
import type { Concentration as ConcentrationModel, Slice } from "../../../model/concentration";
|
|
import type { AccountPagePositionSection } from "../../../model/positions";
|
|
import type {
|
|
AccountPageIncomeSummary,
|
|
AccountPageRecentEvents,
|
|
AccountPageRecentOperations,
|
|
} from "../../../model/previews";
|
|
|
|
import classes from "../AccountPage.module.css";
|
|
import { AccountHeading } from "./AccountHeading.view";
|
|
import { Concentration } from "./Concentration/Concentration.view";
|
|
import { CurrencyCards } from "./CurrencyCards/CurrencyCards.view";
|
|
import { IncomeSummary } from "./IncomeSummary.view";
|
|
import { PositionSection } from "./PositionSection.view";
|
|
import { RecentOperations } from "./RecentOperations.view";
|
|
|
|
type AccountPageLoadedProps = {
|
|
account?: BrokerageAccount;
|
|
accountId?: string;
|
|
concentration: ConcentrationModel | null;
|
|
currency: null | string;
|
|
incomeSummary?: AccountPageIncomeSummary;
|
|
onCurrencyChange: (currency: null | string) => void;
|
|
onSliceChange: (slice: Slice) => void;
|
|
onTabChange: (tab: null | string) => void;
|
|
partial?: boolean;
|
|
portfolio: Portfolio;
|
|
positionSections: AccountPagePositionSection[];
|
|
recentEvents: AccountPageRecentEvents;
|
|
recentOperations: AccountPageRecentOperations;
|
|
slice: Slice;
|
|
source?: InvestmentResourceMeta;
|
|
tab: string;
|
|
unrealizedReturns: Portfolio["totalsByCurrency"];
|
|
};
|
|
|
|
const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
|
const { t } = useTranslation();
|
|
const [filter, setFilter] = useState<string>();
|
|
const tab = props.tab;
|
|
|
|
const onTabChange = props.onTabChange;
|
|
|
|
const activeSection = props.positionSections.find(
|
|
section => section.kind === filter,
|
|
) ?? props.positionSections[0];
|
|
|
|
const positionKindLabels: Record<AccountPagePositionSection["kind"], string> = {
|
|
bonds: t("portfolio.positionKinds.bonds"),
|
|
other: t("portfolio.positionKinds.other"),
|
|
stocks: t("portfolio.positionKinds.stocks"),
|
|
};
|
|
|
|
const filterOptions = props.positionSections.map(section => ({
|
|
label: positionKindLabels[section.kind],
|
|
value: section.kind,
|
|
}));
|
|
|
|
return (
|
|
<Stack gap="lg">
|
|
<AccountHeading account={props.account} accountId={props.accountId} />
|
|
|
|
<SourceSyncStatus
|
|
lastAttempt={props.source?.lastAttempt}
|
|
lastSuccessfulAt={props.source?.lastSuccessfulAt}
|
|
stale={props.source?.freshness === "stale"}
|
|
/>
|
|
|
|
{props.partial
|
|
? tab !== "concentration" && (
|
|
<Alert role="status">{t("investment.partial")}</Alert>
|
|
)
|
|
: <CurrencyCards amounts={props.portfolio.totalsByCurrency} />}
|
|
|
|
{!props.partial && props.unrealizedReturns.length > 0 && (
|
|
<Stack
|
|
aria-label={t("portfolio.unrealizedResult")}
|
|
component="section"
|
|
gap={4}
|
|
>
|
|
<Text fw={600}>{t("portfolio.unrealizedResult")}</Text>
|
|
|
|
<Group gap="md">
|
|
{props.unrealizedReturns.map(total => (
|
|
<Text key={total.currency}>{formatMoney(total)}</Text>
|
|
))}
|
|
</Group>
|
|
|
|
<Text c="dimmed" size="xs">
|
|
{t("portfolio.unrealizedResultCaption")}
|
|
</Text>
|
|
</Stack>
|
|
)}
|
|
|
|
{!props.partial && (
|
|
<PortfolioStructure
|
|
key={props.accountId}
|
|
structure={props.portfolio.structure}
|
|
/>
|
|
)}
|
|
|
|
<Tabs
|
|
classNames={{ list: classes.tabsList }}
|
|
keepMounted={false}
|
|
onChange={onTabChange}
|
|
value={tab}
|
|
>
|
|
<Tabs.List aria-label={t("portfolio.sections")} mb="md">
|
|
<Tabs.Tab value="positions">
|
|
{t("portfolio.tabs.positions")}
|
|
</Tabs.Tab>
|
|
|
|
{props.accountId !== "all" && (
|
|
<Tabs.Tab value="concentration">
|
|
{t("portfolio.concentration.title")}
|
|
</Tabs.Tab>
|
|
)}
|
|
|
|
<Tabs.Tab value="cash">{t("portfolio.tabs.cash")}</Tabs.Tab>
|
|
|
|
<Tabs.Tab value="operations">
|
|
{t("portfolio.tabs.operations")}
|
|
</Tabs.Tab>
|
|
|
|
<Tabs.Tab value="events">{t("portfolio.tabs.events")}</Tabs.Tab>
|
|
|
|
<Tabs.Tab value="income">{t("portfolio.tabs.income")}</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Panel value="positions">
|
|
<Stack gap="md">
|
|
{props.positionSections.length > 1 && (
|
|
<div className={classes.positionFilter}>
|
|
<SegmentedControl
|
|
aria-label={t("portfolio.positionFilter")}
|
|
data={filterOptions}
|
|
onChange={setFilter}
|
|
value={activeSection?.kind}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{activeSection && <PositionSection section={activeSection} />}
|
|
</Stack>
|
|
</Tabs.Panel>
|
|
|
|
{props.accountId !== "all" && (
|
|
<Tabs.Panel value="concentration">
|
|
{props.concentration && (
|
|
<Concentration
|
|
currency={props.currency}
|
|
key={`${props.slice}-${props.currency}`}
|
|
model={props.concentration}
|
|
onCurrencyChange={props.onCurrencyChange}
|
|
onSliceChange={props.onSliceChange}
|
|
partial={!!props.partial}
|
|
slice={props.slice}
|
|
/>
|
|
)}
|
|
</Tabs.Panel>
|
|
)}
|
|
|
|
<Tabs.Panel value="cash">
|
|
<CurrencyCards amounts={props.portfolio.cashBalances} />
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="operations">
|
|
<RecentOperations
|
|
allAccounts={props.accountId === "all"}
|
|
moreLabel={t("portfolio.more")}
|
|
operations={props.recentOperations}
|
|
/>
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="events">
|
|
<BrokerageEventsPreview
|
|
allAccounts={props.accountId === "all"}
|
|
events={props.recentEvents}
|
|
moreLabel={t("portfolio.more")}
|
|
/>
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="income">
|
|
{!!props.incomeSummary && (
|
|
<IncomeSummary
|
|
{...props.incomeSummary}
|
|
moreLabel={t("portfolio.more")}
|
|
/>
|
|
)}
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export { AccountPageLoaded };
|