import { Alert, Button, Card, Group, Loader, Paper, SimpleGrid, Stack, Text, Title, } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; import { useBrokerageOperations } from "@/entities/broker-operation"; import type { BrokerageAccount } from "@/entities/brokerage-account"; import { accountTypeKey } from "@/entities/brokerage-account"; import { type BrokerageEvent, useBrokerageEvents, } from "@/entities/brokerage-event"; import type { Portfolio } from "@/entities/portfolio"; import { formatMoney, formatShare, formatSnapshotTime, } from "@/entities/portfolio"; import { StateView } from "@/shared/ui"; import classes from "./AccountPage.module.css"; export function AccountPageLoading() { const { t } = useTranslation(); return ( ); } export function AccountPageError({ code, retry, }: { code?: string; retry: () => void; }) { const { t } = useTranslation(); if (code === "NOT_FOUND") return ( {t("portfolio.back")} } /> ); if (code === "TBANK_NOT_CONNECTED" || code === "INVALID_TBANK_TOKEN") return ( {t("accounts.openSettings")} } /> ); if (code === "TBANK_UNAVAILABLE") return ( {t("accounts.retry")}} /> ); return ( {t("accounts.retry")}} /> ); } export function AccountPageEmpty({ account, accountId, accessToken = null, refreshing = false, refreshError = false, retry, }: { account?: BrokerageAccount; accountId?: string; accessToken?: string | null; refreshing?: boolean; refreshError?: boolean; retry?: () => void; }) { const { t } = useTranslation(); return ( ); } export function AccountPageLoaded({ account, accountId, accessToken, portfolio, refreshing = false, refreshError = false, retry, }: { account?: BrokerageAccount; accountId?: string; accessToken?: string | null; portfolio: Portfolio; refreshing?: boolean; refreshError?: boolean; retry?: () => void; }) { const { t } = useTranslation(); const stocks = portfolio.positions.filter((p) => p.type === "STOCK"); const bonds = portfolio.positions.filter((p) => p.type === "BOND"); const other = portfolio.positions.filter( (p) => p.type !== "STOCK" && p.type !== "BOND", ); const denominators = new Map(); for (const position of portfolio.positions) { if (!position.currentValue) continue; const currency = position.currentValue.currency; denominators.set( currency, (denominators.get(currency) ?? 0) + Number(position.currentValue.value), ); } return ( {portfolio.isStale ? ( {t("portfolio.staleWarning")} ) : null} {t("portfolio.updatedAt", { date: formatSnapshotTime(portfolio.lastSuccessfulAt), })} {portfolio.totalsByCurrency.map((total) => ( {total.currency} {formatMoney(total)} ))} {stocks.length ? ( ) : null} {bonds.length ? ( ) : null} {other.length ? ( ) : null}
{portfolio.cashBalances.map((balance) => ( {balance.currency} {formatMoney(balance)} ))}
); } function PortfolioRefreshState({ refreshing, refreshError, retry, }: { refreshing: boolean; refreshError: boolean; retry?: () => void; }) { const { t } = useTranslation(); if (!refreshing && !refreshError) return null; return ( <> {refreshing ? ( {t("portfolio.refreshing")} ) : null} {refreshError ? ( {t("portfolio.refreshError")} ) : null} ); } function RecentOperations({ accountId, accessToken, }: { accountId: string | undefined; accessToken: string | null; }) { const { t } = useTranslation(); const operations = useBrokerageOperations(accountId, accessToken, { limit: 5, }); return (
{operations.isLoading ? ( {t("ledger.recentLoading")} ) : null} {operations.isError ? ( {t("ledger.recentError")} ) : null} {operations.data?.items.length === 0 ? ( {t("ledger.recentEmpty")} ) : null} {operations.data?.items.map((operation) => ( {operation.description || operation.type} {operation.payment ? formatMoney(operation.payment) : t("ledger.noMoney")} ))}
); } function RecentEvents({ accountId, accessToken, }: { accountId: string | undefined; accessToken: string | null; }) { const { t } = useTranslation(); const events = useBrokerageEvents(accountId, accessToken); return (
{events.isLoading ? ( {t("events.recentLoading")} ) : null} {events.isError ? ( {t("events.recentError")} ) : null} {events.data?.events.length === 0 ? ( {t("events.recentEmpty")} ) : null} {events.data?.events.slice(0, 5).map((event) => ( ))}
); } function RecentEvent({ event }: { event: BrokerageEvent }) { const { t } = useTranslation(); return ( {`${t(`events.type.${event.type}`)} ยท ${event.displayName}`} {event.amount && event.currency ? formatMoney({ currency: event.currency, value: event.amount }) : t("events.noAmount")} ); } function PositionSection({ title, positions, denominators, }: { title: string; positions: Portfolio["positions"]; denominators: Map; }) { return (
{positions.map((position) => ( ))}
); } function PositionCard({ position, denominators, }: { position: Portfolio["positions"][number]; denominators: Map; }) { const { t } = useTranslation(); const share = position.currentValue?.currency ? formatShare(position.currentValue, { currency: position.currentValue.currency, value: String(denominators.get(position.currentValue.currency) ?? 0), }) : null; const returnClass = position.absoluteReturn ? Number(position.absoluteReturn.value) > 0 ? classes.positive : Number(position.absoluteReturn.value) < 0 ? classes.negative : undefined : undefined; return ( {position.displayName} {position.isin ? ( {position.isin} ) : null} {t("portfolio.quantity", { value: position.quantity })} {position.currentValue ? ( {formatMoney(position.currentValue)} ) : null} {position.absoluteReturn ? ( {t("portfolio.return", { value: formatMoney(position.absoluteReturn), })} ) : null} {position.averagePurchasePrice || share ? ( {position.averagePurchasePrice ? ( {t("portfolio.avgPrice", { value: formatMoney(position.averagePurchasePrice), })} ) : null} {share ? ( {t("portfolio.share", { value: share })} ) : null} ) : null} ); } function AccountHeading({ account }: { account?: BrokerageAccount }) { const { t } = useTranslation(); return ( {account?.name ?? t("portfolio.title")} {account ? ( {t(accountTypeKey(account.type))} ) : null} ); } function Section({ title, children, }: { title: string; children: React.ReactNode; }) { return ( {title} {children} ); }